enhance: pin sealed read-snapshot view reads through frozen column - #53913
sre-ci-robot merged 2 commits into
Conversation
Perchunk chunk_data/chunk_view reads in the expression and chunk-reader hot loop still call segment accessors that re-capture the immutable PublishedSegmentState on every access. Phase 1 routed the metadata hot loop (chunk_size, num_rows_until_chunk, get_chunk_by_offset, num_chunk_data, get_row_count) through the request-scoped SegmentReadSnapshot, but the actual data and view reads kept paying one atomic_load plus two ref-count RMWs per chunk on sealed segments. Route the view family through the already-pinned column obtained from GetDataScanResources so every data read derives from the same frozen generation as the chunk boundaries, with zero atomics and zero ref-count churn: - SegmentChunkReader::ChunkData<T> / ChunkStringView - SegmentExpr::GetChunkData / GetChunkView / GetChunkViewsByOffsets / GetBatchViews / GetViewsByOffsets (including the Json conversion branch) Migrate the sealed hot-loop call sites: SegmentChunkReader.cpp, Expr.h, CompareExpr.h, UnaryExpr.cpp, and the group-by path (SearchGroupByOperator + StrictGroupFilteredSearch). PhySearchGroupByNode captures the request snapshot once in its constructor and threads it into SealedDataGetter, mirroring how segment_ and search_info_ are bound. Growing segments and non-pinned paths keep the existing per-call segment access through the same fallback helpers, so behavior is bit-for-bit identical; sealed segments now read the view family from the pinned snapshot with no per-chunk capture. Verified with the segcore unittest binary: SegmentChunkReader, group-by, sealed read-snapshot, expression, and chunked-sealed suites all pass. Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
|
[ci-v2-notice] To rerun ci-v2 checks, comment with:
If you have any questions or requests, please contact @zhikunyao. |
✅ CI Loop Results
|
| Stage | Result | Duration | Tests |
|---|---|---|---|
| ✅ Build | SUCCESS | 11.9min | - |
| ✅ Code-Check | SUCCESS | 5.4min | - |
| ✅ UT-Integration | SUCCESS | 38.3min | - |
| ✅ UT-GO | SUCCESS | 26.6min | - |
| ✅ UT-CPP-Cov | SUCCESS | 46.8min | 9705 total, 9705 passed, 0 failed |
Total: 92min | Pipeline | Artifacts
Overall Coverage: 77.0%
Diff Coverage: CPP 65.0% (104 hit, 56 miss, 160 measurable lines, 191 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 65.0% (104/160 measurable lines, 191 unmeasured)
| PinWrapper<Span<T>> | ||
| GetChunkData(FieldId field_id, int64_t chunk_id) const { | ||
| if (snapshot_) { | ||
| auto column = snapshot_->GetDataScanResources(field_id).first; |
There was a problem hiding this comment.
[P2] Avoid rebuilding skip-metrics resources for every data read
The new data helpers consume only .first, but SegmentReadSnapshot::GetDataScanResources also constructs a FieldSkipMetricsView: its implementation copies the column shared_ptr twice and calls GetSkipMetricsList() before the unused .second is destroyed. Thus every chunk/view read still performs two reference-count increment/decrement cycles, and lazy storage-v2 columns also pay an acquire load and metrics lookup, so the hot paths do not get the zero-refcount access this change is intended to provide. Add a column-only snapshot accessor that returns a borrowed pointer/reference (the snapshot already owns the published state) and use it throughout this helper family, SegmentChunkReader, and group-by.
| std::nullopt, | ||
| false); | ||
| false, | ||
| snapshot); |
There was a problem hiding this comment.
[P2] Carry the snapshot into strict phase-2 membership scans
The snapshot is passed only to the phase-1 data getter here. In the PerGroup branch, BuildGroupOffsets still creates a fresh SegmentChunkReader without calling SetSnapshot and reads chunk metadata directly from the segment, so its full raw-field scan continues to recapture published state for each chunk-size/data transition. Since this branch can scan total_data_cnt_ rows, it leaves the most expensive strict group-by path outside the optimization. Carry the snapshot in StrictGroupPhase2Context, pass it to BuildGroupOffsets, and bind both the reader and metadata lookups to that snapshot.
| std::optional<DataType> json_type = std::nullopt, | ||
| bool strict_cast = false) { | ||
| bool strict_cast = false, | ||
| const segcore::SegmentReadSnapshot* snapshot = nullptr) { |
There was a problem hiding this comment.
[P3] Place the common snapshot argument before JSON-only options
Appending snapshot after the three defaulted JSON-specific parameters forces every scalar branch to pass the same std::nullopt, std::nullopt, and false placeholders, expanding simple one-line cases into most of the new switch plumbing. This helper is file-local, so move snapshot before those optional parameters or capture it in a local factory; the scalar cases can then remain direct calls without dummy arguments, and the JSON cases keep supplying only their real options.
| if (snapshot_) { | ||
| column = snapshot_->GetColumn(field_id_); | ||
| } else { | ||
| column = segment_.GetChunkedColumn(field_id_).get(); |
There was a problem hiding this comment.
[P2] Keep the fallback column owner alive until pinning
When snapshot_ is null, GetChunkedColumn() returns a shared_ptr, but calling .get() here keeps only its raw pointer and destroys the temporary owner at the semicolon. A concurrent sealed-state publication can then retire that column before column->GetChunk() runs, causing a use-after-free in the supported non-pinned fallback. The previous implementation kept the shared_ptr alive through pinning. Retain an owning shared_ptr in the fallback branch until GetChunk() returns, and use a borrowed pointer only for the snapshot-backed branch.
| // segment contract); snapshot-present paths read from the frozen column. | ||
| template <typename ViewType> | ||
| PinWrapper<std::pair<std::vector<ViewType>, ValidityView>> | ||
| GetBatchViews(FieldId field_id, |
There was a problem hiding this comment.
[P3] Remove the duplicate sealed-view wrapper layer
GetBatchViews and GetViewsByOffsets only forward to the existing snapshot-aware GetChunkView and GetChunkViewsByOffsets helpers, or to segment aliases with the same behavior. All three call sites already execute only for sealed segments, so this additional API layer does not enforce a reachable contract and duplicates logic that must remain synchronized. Call the existing helpers directly at those sites, passing the range where needed, and remove both wrappers.
❌ CI Loop Results
|
| Stage | Result | Duration | Tests |
|---|---|---|---|
| ✅ Build | SUCCESS | 13.1min | - |
| ✅ Code-Check | SUCCESS | 5.4min | - |
| ❌ UT-Integration | FAILURE | 39.3min | - |
| ✅ UT-GO | SUCCESS | 24.0min | - |
| ✅ UT-CPP-Cov | SUCCESS | 55.0min | 9705 total, 9705 passed, 0 failed |
Total: 95min | Pipeline | Artifacts
Overall Coverage: 77.0%
Diff Coverage: CPP 67.3% (113 hit, 55 miss, 168 measurable lines, 202 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 67.3% (113/168 measurable lines, 202 unmeasured)
Failed Test Logs:
- UT-Integration: view log
0db229a to
fe6cb0f
Compare
| return true; | ||
| } | ||
| auto raw_chunk_count = segment.num_chunk_data(field_id); | ||
| auto raw_chunk_count = snapshot ? snapshot->num_chunk_data(field_id) |
There was a problem hiding this comment.
[P3] Reuse the reader for snapshot-aware metadata
The new membership path selects between snapshot and segment separately for chunk count, row prefixes, and every chunk size, then immediately creates a SegmentChunkReader and binds that same snapshot. Move reader construction and binding before these checks and use NumChunkData, NumRowsUntilChunk, and ChunkSize; this removes duplicate generation-selection logic and prevents the metadata scan from drifting from the accessor's source.
| const milvus::segcore::SegmentInternalInterface* segment_; | ||
| // Request-scoped sealed read snapshot, captured once in the constructor | ||
| // like segment_ / search_info_; null for growing / non-pinned paths. | ||
| const milvus::segcore::SegmentReadSnapshot* snapshot_{nullptr}; |
There was a problem hiding this comment.
[P3] Avoid caching a one-use snapshot alias
snapshot_ duplicates state already owned by query_context_ and is read only once in GetOutput, which can execute its output-producing path only once. Obtain a local shared snapshot from query_context_ at that use site and pass its pointer to SearchGroupBy; then remove this raw-pointer member and its constructor assignment so ownership and lifetime remain visible at the call.
Review feedback on the Phase-2 view-family pin found two hot paths that still paid per-chunk shared_ptr refcount churn, and one that bypassed the pinned snapshot entirely. GetDataScanResources constructs a FieldSkipMetricsView alongside the column, copying the column shared_ptr twice and touching the lazy storage-v2 metrics list on every data read. Add a column-only snapshot accessor that returns a borrowed pointer straight out of the frozen published state (zero ref-count traffic), and route the per-chunk data, view, validity, and group-by reads through it. The pair accessor stays for the skip-metrics and take/scan paths that need the metrics view. The strict group-by phase-2 membership scan created a SegmentChunkReader without binding the request snapshot and read chunk boundaries from the segment, so the PerGroup raw-field scan over total_data_cnt_ rows kept recapturing published state per transition. Carry the snapshot in StrictGroupPhase2Context and bind both the reader and the metadata lookups in BuildGroupOffsets/ScanRawField. Also move the snapshot argument before the JSON-only options in the file-local CreateFieldGetter so scalar branches no longer pass dummy placeholders. Verified with the segcore unittest binary: group-by, chunk-reader, sealed read-snapshot, expression, and chunked-sealed suites pass. Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
fe6cb0f to
94f9f18
Compare
| // snapshot column; growing and non-pinned paths fall back to the segment. | ||
| template <typename ViewType> | ||
| PinWrapper<std::pair<std::vector<ViewType>, ValidityView>> | ||
| GetChunkView(FieldId field_id, |
There was a problem hiding this comment.
[P3] Centralize the duplicated snapshot column adapters
The new GetChunkView and GetChunkViewsByOffsets blocks duplicate the type dispatch and JSON materialization already implemented by SegmentInternalInterface::chunk_view and chunk_views_by_offsets, while the same SpanBase conversion is repeated again in SegmentChunkReader and SealedDataGetter. This leaves multiple supported-type matrices that must stay synchronized and makes a new view type easy to add to one path but not the others. Extract the column-backed typed adapters once next to ChunkedColumnInterface, then let these callers only select the snapshot column or the existing segment fallback.
✅ CI Loop Results
|
| Stage | Result | Duration | Tests |
|---|---|---|---|
| ✅ Build | SUCCESS | 8.0min | - |
| ✅ Code-Check | SUCCESS | 6.7min | - |
| ✅ UT-Integration | SUCCESS | 39.8min | - |
| ✅ UT-GO | SUCCESS | 30.3min | - |
| ✅ UT-CPP-Cov | SUCCESS | 55.5min | 9705 total, 9705 passed, 0 failed |
Total: 98min | Pipeline | Artifacts
Overall Coverage: 77.0%
Diff Coverage: CPP 66.9% (111 hit, 55 miss, 166 measurable lines, 189 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 66.9% (111/166 measurable lines, 189 unmeasured)
✅ CI Loop Results
|
| Stage | Result | Duration | Tests |
|---|---|---|---|
| ✅ Build | SUCCESS | 5.5min | - |
| ✅ Code-Check | SUCCESS | 4.2min | - |
| ✅ UT-Integration | SUCCESS | 38.8min | - |
| ✅ UT-GO | SUCCESS | 11.5min | - |
| ✅ UT-CPP-Cov | SUCCESS | 46.5min | 9705 total, 9705 passed, 0 failed |
Total: 71min | Pipeline | Artifacts
Overall Coverage: 77.0%
Diff Coverage: CPP 66.5% (109 hit, 55 miss, 164 measurable lines, 189 unmeasured)
Diff Coverage HTML: view changed lines
Total Patch Coverage: 66.5% (109/164 measurable lines, 189 unmeasured)
|
/lgtm |
|
[approval-status] effective-owner-approvals=1 [liliu-z(comment)]; do-not-merge/disable-approve-self=not-required; do-not-merge/doc-need-two-approve=disabled; ignored=[none] |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: liliu-z The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Related to #53247
Perchunk chunk_data/chunk_view reads in the expression and chunk-reader hot loop still call segment accessors that re-capture the immutable PublishedSegmentState on every access. Phase 1 routed the metadata hot loop (chunk_size, num_rows_until_chunk, get_chunk_by_offset, num_chunk_data, get_row_count) through the request-scoped SegmentReadSnapshot, but the actual data and view reads kept paying one atomic_load plus two ref-count RMWs per chunk on sealed segments.
Route the view family through the already-pinned column obtained from GetDataScanResources so every data read derives from the same frozen generation as the chunk boundaries, with zero atomics and zero ref-count churn:
Migrate the sealed hot-loop call sites: SegmentChunkReader.cpp, Expr.h, CompareExpr.h, UnaryExpr.cpp, and the group-by path (SearchGroupByOperator + StrictGroupFilteredSearch). PhySearchGroupByNode captures the request snapshot once in its constructor and threads it into SealedDataGetter, mirroring how segment_ and search_info_ are bound.
Growing segments and non-pinned paths keep the existing per-call segment access through the same fallback helpers, so behavior is bit-for-bit identical; sealed segments now read the view family from the pinned snapshot with no per-chunk capture.
Verified with the segcore unittest binary: SegmentChunkReader, group-by, sealed read-snapshot, expression, and chunked-sealed suites all pass.