Skip to content

[feature](be) Support spill for non-streaming window functions - #68500

Merged
mrhhsg merged 18 commits into
apache:masterfrom
mrhhsg:window_spill
Oct 27, 2026
Merged

mrhhsg merged 18 commits into
apache:masterfrom
mrhhsg:window_spill

Conversation

@mrhhsg

@mrhhsg mrhhsg commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary:

Large non-streaming window partitions currently retain all input blocks until the partition closes. A sufficiently large partition can therefore exhaust BE memory even when query spill is enabled.

This change adds a capability-gated spill path for eligible non-streaming window functions:

  • keeps genuinely streaming windows on the existing in-memory path;
  • incrementally detects partition boundaries and stores the original input blocks in a batch store;
  • reduces full-partition aggregates while collecting input, and spills input blocks on memory revocation, on a configurable threshold, or in forced-spill mode;
  • records bounded peer-group metadata for percent_rank and cume_dist, with independent metadata spill;
  • publishes all partitions that end inside one input block as a single AnalyticSpillBatch (per-partition row ends, reduce results and ntile parameters; batch-relative peer group ends), so high-cardinality partition keys do not create per-partition descriptors, aggregate-state rebuilds or 1-row output blocks;
  • after a batch is spilled, coalesces small blocks into a write buffer and writes them once it reaches spill_buffer_size_bytes, flushing it on revoke and seal;
  • replays batches from the source operator in batch_size slices and tracks peer-group metadata with the query MemTracker; the source reserves replay memory only for the spilled files it is about to read (data file and peer-group sidecar of the queued batch, and the next data or sidecar record during replay), so in-memory batches are consumed without a disk-read reservation;
  • fills peer-function results once per peer/output intersection and checks cancellation before each fill, without fixed 4096-row splits;
  • keeps a sealed batch in memory unless it reaches spill_analytic_sink_mem_limit_bytes, so batches are not written to disk without memory pressure;
  • preserves the existing path when any function or frame is unsupported;
  • adds spill_analytic_sink_mem_limit_bytes and window-spill profile counters.

The initial supported set is:

  • full-partition sum/sum0/count/avg and fixed-length-state min/max, including OVER () / OVER (PARTITION BY ...) without ORDER BY, which Nereids plans as RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW with no order-by expressions (every row is a peer, so the frame is the whole partition);
  • ntile with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW;
  • percent_rank and cume_dist with RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

The new source file shifts the BE unity batches so that async_result_writer.cpp shares a translation unit with a file that includes local_exchanger.h. Both declare clear_blocks with a default argument, which fails with "redefinition of default argument"; the redeclaration in async_result_writer.cpp no longer repeats the default argument (its only call passes both arguments).

Performance (5M unique partition keys, sum(v) over (partition by k), SQL/query cache off): 0.14s with spill enabled vs 0.26s on the in-memory path; the source emits ~4K rows per block.

Coalescing spill writes (50M narrow rows, spill_analytic_sink_mem_limit_bytes=1MB, sum(v) over ()): spill blocks 6152 -> 96, serialize time 958ms -> 388ms, spill file 38MB -> 11.8MB, query 1.81s -> 1.26s.

TPC-DS SF10000 (single BE, exec_mem_limit=8GB): full-partition aggregates, ntile, percent_rank/cume_dist over store_sales (28.8B rows) and a 930M-row single partition all completed with the window sink peak at about 64MB per instance; 2.4B ticket-level partitions ran entirely through in-memory batches.

Release note

Eligible non-streaming window functions can spill partitions to local storage when query spill is enabled.

Check List (For Author)

  • Test
    • Regression test
      • New spill_p0/window_function_spill: 12 queries (full-partition aggregates with an all-NULL partition, skew, no partition, high cardinality, ntile, percent_rank/cume_dist, unsupported fallback, streaming) each run with spill disabled, spill enabled and forced spill; all three outputs are identical.
    • Unit Test
      • Earlier full-suite run: ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*' — 38/38 passed under ASAN, including write coalescing, replay slicing, revoke flush, peer-group sidecar files, replay reservation for in-memory / data-only / data-plus-sidecar batches and sidecar records read during replay, and the proactive spill limit.
      • Earlier full build: ./build.sh --be --fe — passed.
      • build-support/check-format.sh, build-support/check-build-hygiene.sh, git diff --check — passed.
    • Manual test
      • Profiles confirm sum(v) over () spilling 5M rows and partition by k using in-memory batches.
      • A TPC-DS style validation (store/ticket/date partitions compared with GROUP BY references) passed on a local scaled-down dataset with and without spill.
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason

Validation notes:

Latest follow-up (a225ec4bd70):

  • Peer replay checks cancellation before each peer/output fill; output is already bounded by batch_size, so there is no extra 4096-row split.

  • Added focused unit cases for cancellation before filling a large peer, filling a large peer in one iteration, and cancellation between uneven peers.

  • BUILD_TYPE=ASAN ENABLE_UNITY_BUILD=ON ./build.sh --be -j16 — passed.

  • ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.Spill*' -j16 — the changed production and test translation units compiled, but the test executable could not be rebuilt/run because of existing be/test/exprs/function/function_string_test.cpp:2971-2987 compilation errors (Field string construction and removed FunctionContext scope values). The earlier 38/38 result above does not cover this follow-up.

  • build-support/clang-format.sh, build-support/check-format.sh, build-support/check-build-hygiene.sh, and git diff --check — passed.

  • SQL regression was not rerun for this cancellation-check refactoring.

  • Clang-tidy cannot fully run on the local toolchain (stddef.h not found); reported diagnostics on the touched files are pre-existing cognitive-complexity warnings or false positives from the failed parse.

  • Behavior changed:

    • No.
    • Yes. Eligible non-streaming analytic partitions can spill; streaming and unsupported windows retain the existing behavior.
  • Does this need documentation?

    • No. The implementation design is included in this PR; no user-facing documentation change is required for this initial version.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@mrhhsg
mrhhsg force-pushed the window_spill branch 2 times, most recently from 0e969ba to 83fdbf9 Compare September 28, 2026 06:51
Comment thread be/src/exec/operator/analytic_spill.cpp Outdated
DCHECK_GT(block.rows(), 0);
_rows += block.rows();
if (_data_writer) {
return _data_writer->write_block(state, block);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Coalesce small blocks before writing analytic spill, following the hash join spill pattern

Once _data_writer exists, every incoming Block is immediately passed to write_block(), which serializes and ZSTD-compresses it and appends it to the file. _append_spill_rows() only splits oversized input blocks; it does not combine smaller ones. The initial spill() loop also writes each retained Block separately.

For a large window partition arriving in 128 KiB blocks with an 8 MiB spill buffer, this produces roughly 64 serialization/compression/append operations per 8 MiB of input, instead of one coalesced write. This is a call-count comparison, not a measured 64x slowdown; the extra per-block work can limit spill throughput for narrow rows or small input batches.

We already have a suitable pattern in the hash join implementation:

Please consider the same bounded buffering pattern here: combine small payload blocks in input order, flush at the size threshold, and flush the tail on revoke and before sealing/closing the writer. Include the buffer in memory tracking and revocable_mem_size(); already large blocks can retain the direct-write path. This can stay within the batch store without changing aggregate-update boundaries or peer offsets.

A focused test should cover multiple small appends, the final partial buffer, and forced reclamation. A small-block benchmark comparing SpillWriteBlockCount, serialization time, throughput, and peak memory would quantify the tradeoff.

mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
…spill

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: After a window function batch starts spilling, every appended Block was written to the spill file on its own, and each write serializes, ZSTD-compresses and indexes one Block. Input Blocks are usually far smaller than `spill_buffer_size_bytes` (8MB by default), so a large partition was written as many small spill blocks. On TPC-DS SF10000, one window sink instance wrote about 1.29M blocks of about 65KB each. In addition, `enable_force_spill` or a proactive threshold smaller than the buffer triggered a spill for every append after the first one.

Once a batch is spilled, small Blocks are now merged into a write buffer in input order and written when the buffer reaches `spill_buffer_size_bytes`. A Block that already reaches that size flushes the buffer first and is written directly. `spill()` (used by revoke) and `seal()` always flush the buffer, the buffer is part of `revocable_mem_size()`, and a spilled store no longer takes the proactive spill path. Because a replayed Block can now be much larger than the batch size, the source slices it into `batch_size` rows before computing window results.

Local benchmark with 50M narrow rows (two bigint columns), `spill_analytic_sink_mem_limit_bytes=1MB`:
- `sum(v) over ()`: spill blocks 6152 -> 96, sink serialize time 958ms -> 388ms, source deserialize time 273ms -> 145ms, spill file bytes 38MB -> 11.8MB, query 1.81s -> 1.26s; sink peak memory 1.06MB -> 4.76MB, bounded by the spill buffer size.
- `sum(v) over (partition by k % 8)`: spill blocks 6128 -> 192, spill file bytes 35.5MB -> 20.3MB, query 0.90s -> 0.76s.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test / Manual test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (34 passed), including new cases for coalescing small appends, the final partial buffer, forced flush on revoke, and replay slicing by batch size
    - `./run-regression-test.sh --run -d spill_p0` passed
    - Manual benchmark above; scaled-down TPC-DS validation passed with a 1MB spill threshold
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Three gaps in the window function spill path:

1. The analytic source had no reservation for reading spilled data. Before a replay reads a new spilled record it relied on the generic previous-call estimate (at least 4MB), while opening a batch reader allocates a buffer for the largest serialized record and reading deserializes a whole record, each up to about `spill_buffer_size_bytes` after small Blocks are coalesced. The source now adds one spill buffer to its reservation when it is about to read a record of a spilled batch, and two when it is about to open the next batch (reader buffer plus deserialized Block). In-memory batches and partially consumed Blocks add nothing.
2. The peer group end buffers of the batch store and of the sealed batch used `std::vector`, which bypasses the query MemTracker, although they hold up to `spill_buffer_size_bytes` per task. They now use `DorisVector`.
3. After a batch was spilled the store no longer forced a spill on every append, so no test created the peer group sidecar file any more. New tests cover sidecar creation on revoke with two metadata Blocks, sidecar flushing at the 1MB spill buffer threshold while the rows stay in memory, and the replay reservation.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (36 passed)
    - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Before publishing an in-memory batch to the source, the window function sink spilled it whenever its revocable memory exceeded `spill_min_revocable_mem`, regardless of memory pressure. FE sends 4MB for that option by default, and with high-cardinality partitions every input Block that ends a partition is sealed as a batch, so rows with a wide column (for example a ~1KB STRING, about 16MB per 8160-row Block) wrote and read back every Block, turning an in-memory query into a disk-bound one. `spill_min_revocable_mem` is only a "worth revoking" threshold elsewhere; the per-batch in-memory limit of this operator is `spill_analytic_sink_mem_limit_bytes` (64MB by default).

A sealed batch is now spilled only when it reaches `spill_analytic_sink_mem_limit_bytes`, the same limit that triggers the proactive spill of the open partition, so memory that cannot be revoked after publication stays bounded by that limit plus one Block.

Also:
- add UTs that keep a sealed batch above `min_revocable_mem` but below the sink limit in memory, and that trigger the proactive spill through `spill_analytic_sink_mem_limit_bytes` without forced spill or revoke;
- assert the scheduler-facing `get_reserve_mem_size` of the analytic source in the replay reservation test;
- let `~SpillFileWriter` close writers left open by a failed or cancelled query so that close failures are logged instead of discarded.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed)
    - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
@mrhhsg

mrhhsg commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete static review of a1dcf05. I found two P2 memory-admission issues in the analytic source replay path, both commented inline. No existing P0/P1 inline finding still applies. The earlier P2 thread about small spill writes is addressed by the current write buffer and is not repeated.

Review checkpoints

  • Goal and tests: the eligible path incrementally reduces supported full-partition aggregates, records NTILE/peer metadata, spills input when required, and replays rows in order. Dedicated BE unit cases and a 12-query regression suite cover in-memory, forced-spill, boundaries, nullable values, and peer groups. This review was static: I did not run builds, tests, or a cluster, and the checked-in results are author artifacts rather than independent runtime proof.
  • Scope and reuse: the implementation uses the existing spill file and pipeline dependency interfaces. The async result writer declaration adjustment is explained by unity-build interaction. No unrelated change was identified.
  • Concurrency and lifecycle: sink and source pipeline tasks hand sealed batches through the shared queue under buffer_mutex; EOS uses sink_eos_lock. I found no additional lock-order or wakeup defect. Writers are sealed before readers open, and close paths release readers and batch references. The first inline finding concerns workload-group admission of a non-revocable in-memory batch; the second concerns simultaneous data and peer-sidecar read allocations.
  • Configuration and compatibility: FE forwards the new session option through optional Thrift field 233; BE supplies the 64 MiB default and clamps it like adjacent spill limits. This is a per-query option, with no dynamic process setting or storage-format change. Optional Thrift delivery preserves the older-peer path.
  • Parallel paths and conditions: spill-disabled, streaming, and unsupported function/frame cases retain the existing evaluator. Capability wrappers, frame gates, partition boundaries, nullable results, and NTILE's positive constant argument were traced; no separate correctness issue was substantiated.
  • Test results and observability: the 12 checked-in outputs match across disabled, enabled, and forced-spill modes. The new profile counters distinguish eligible, spilled, and in-memory partitions. Existing tests do not exercise workload-group admission or measure the peak allocation when both replay files are open; those are the two inline findings.
  • Persistence, writes, and performance: the change writes temporary spill files but does not alter table transactions, metadata persistence, or data visibility. Small-block coalescing now flushes on threshold, revoke, and seal, addressing the existing P2 performance thread. No other substantiated performance issue was found.

The supplied focus text is -light and names no additional code path; I still reviewed the full changed-file set. Two complete review rounds converged, and all accepted new findings are inline below.

if (_current_batch && _batch_output_position < _current_batch->rows) {
return _current_batch->data_file ? spill_buffer_bytes : 0;
}
return 2 * spill_buffer_bytes;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Inspect the queued batch before reserving spill-reader memory. This returns two spill buffers at every batch boundary, even when the next batch was kept entirely in memory below the sink spill threshold. After publication its bytes are no longer sink-revocable, yet PipelineTask must reserve these unused reader buffers before _get_spill_block() can dequeue it. If a workload group rejects that reservation at its high watermark while the query has no revocable task, the only consumer can remain paused and hit the paused-queue timeout although replay would open no file. Base the reservation on the queued batch's data_file state, or otherwise let in-memory batches be consumed without a disk-read reservation.

_batch_reader = _current_batch->data_file->create_reader(state, operator_profile());
RETURN_IF_ERROR(_batch_reader->open());
}
if (_current_batch->peer_group_file) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include the peer-group sidecar in replay memory reservation. A batch can have both data_file and peer_group_file; this opens both readers and _next_peer_group_end() retains a decoded sidecar Block before the same get_block() reads the data Block. Each reader also retains its own serialized buffer/PBlock, while _spill_replay_reserve_bytes() adds only two spill buffers for opening any batch. With many distinct peers, the sidecar Block alone can approach spill_buffer_size_bytes(), so the first replay can allocate beyond its reservation and hit normal memory limits. Account for the sidecar reader and Block when reserving replay memory.

@mrhhsg
mrhhsg marked this pull request as ready for review September 30, 2026 01:15
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Oct 9, 2026
… peer group sidecar

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Two gaps in the replay memory reservation of the analytic source:

1. At every batch boundary the source reserved two spill buffers before it knew whether the next batch was spilled. A batch sealed below `spill_analytic_sink_mem_limit_bytes` is published in memory, and its bytes are no longer revocable by the sink; if a workload group rejects that reservation at its high watermark while the query has no revocable task, the only consumer stays paused until the paused-queue timeout although replaying the batch would open no file. The source now inspects the batch at the head of the queue under `buffer_mutex` and reserves two spill buffers for each of its spilled files (`data_file`, `peer_group_file`), nothing for an in-memory batch and nothing when the queue is empty.
2. The reservation ignored the peer group sidecar. Opening a batch opens the sidecar reader and reads a sidecar Block before the first data record, and the sidecar is read again during replay; each record holds up to `spill_buffer_size_bytes` of group ends, so a replay could allocate well beyond its reservation. The sidecar now gets the same two buffers when the batch is opened, and during replay one buffer is reserved exactly when the next `batch_size` slice contains a row beyond the last group end read so far.

UTs assert the reservation for a data-only spilled batch, a batch with both files, an in-memory batch, and the slice that reads the second sidecar record while the rows stay in memory. The design document describes the reservation rule.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed)
    - `build-support/check-format.sh`, `build-support/check-build-hygiene.sh`, `git diff --check` passed
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR #68500 at head 088339d. Seven new P2 issues are inline. No P0/P1 existing inline finding still applies; the three existing P2 threads were treated as duplicate fences, and their specific coalescing, in-memory queue reservation, and sidecar reservation cases appear addressed on this head.

Review status: Incomplete convergence. The maximum three full review rounds were completed, but round 3 still produced two distinct findings. Every candidate identified in these rounds was validated, deduplicated or dismissed, and all seven accepted findings are inline; another round was not permitted.

Critical checkpoints:

  • Goal and tests: The new sink/store/source path supports partition-wide window reductions, ntile and peer results with in-memory and spilled replay. The inspected unit and regression cases cover those modes, and the 36 result sections are internally consistent across 12 queries and three modes. This review was static only: no build or test ran, and the regression fixture does not assert that physical spilling occurred. The memory-pressure cases in the inline findings remain untested here.
  • Scope and conditions: The changed paths form a coherent analytic-spill feature plus aggregate capability, query-option, test and documentation changes. I traced eligibility and frame gates, partition and peer boundaries, nullable result handling, and supported versus legacy analytic paths; no separate row-result error was substantiated.
  • Concurrency and lifecycle: Sink and source run in separate pipeline tasks; batch queue accesses use its mutex and dependency wakeups. M1 is a reservation-to-dequeue race despite individually locked accesses. I found no additional lock-order issue. Spill writers close before batches are published; reader, error, cancellation, LIMIT and temporary-file ownership paths were traced without a separate lifecycle finding. No cross-translation-unit static initialization was introduced.
  • Memory, writes and performance: M2 and M4-M7 cover record-size admission, write scratch, stale or missing result estimates, and hidden revocable buffers. The earlier small-Block write-amplification thread is already addressed by coalescing. Spill files are local temporary data; this PR does not alter table writes, committed-data visibility, EditLog or transactions.
  • Configuration and compatibility: The optional FE-to-BE query option, SQL/load forwarding, BE default and clamp, and fallback for unsupported frames were checked. No rolling-upgrade or dynamic-config defect was substantiated. Spill and memory profile counters provide diagnostic context; no separate observability issue was established.

The supplied -light focus was applied to prioritization while the complete changed-file inventory and the relevant upstream/downstream paths were reviewed.

// spilled files, reads the first peer group record and then the first data record.
LockGuard lock(_shared_state->buffer_mutex);
if (_shared_state->spill_batches.empty()) {
return 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the next batch stable across replay admission. After this empty-queue check returns zero, the sink can publish a spilled batch before the same source call finishes its current batch and dequeues the new one. PipelineTask has already completed its memory reservation, so opening that batch allocates its reader buffers and first decoded records without admission under workload-group pressure. Defer a newly arrived batch to a later scheduling round, or reserve from a stable batch snapshot.

// allocates the buffer for the largest serialized record of the file and every read
// deserializes a whole record into a new Block. In-memory batches cost nothing to replay.
const auto spill_buffer_bytes = static_cast<size_t>(state->spill_buffer_size_bytes());
if (!_current_batch || _batch_output_position >= _current_batch->rows) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reserve for the actual spilled record size. The sink can write a record larger than spill_buffer_size_bytes: one wide String row is unsplittable, and two ordinary sub-buffer Blocks can coalesce before the post-merge flush. SpillFileReader opens a buffer sized to the file's actual maximum record and then deserializes it, but this estimates only two configured buffers for a data-only batch (and one on later reads). With an 8 MiB buffer, one incompressible 32 MiB row already exceeds the 16 MiB opening estimate with the read buffer alone. Bound record size at write time or carry its actual maximum into replay admission.

const int64_t last_peer_group_end = column.get_data().back();
const int64_t slice_end =
std::min<int64_t>(_batch_output_position + state->batch_size(), _current_batch->rows);
return last_peer_group_end < slice_end;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Limit sidecar lookahead to the next data Block. This uses batch_size even when _next_replay_rows will return fewer rows at the current data Block boundary. If the loaded sidecar record ends at that boundary, the next call needs no sidecar read, yet it reserves one full spill buffer and can pause the only consumer at the workload-group high watermark. Use the actual next output row count when deciding whether another peer record will be read.

local_state._reserve_mem_size = 0;
SCOPED_PEAK_MEM(&local_state._reserve_mem_size);
if (local_state._spill_enabled) {
return local_state._sink_spill(state, input_block, eos);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reserve spill-write scratch before the first sink call. This new branch can force-spill its first ordinary buffer-sized Block, but get_reserve_mem_size still returns only the prior call's measured peak, which starts at zero. PipelineTask then skips sink memory admission while SpillFileWriter allocates a PBlock and serialized/compressed buffers; a peer-sidecar flush can do the same without data spill. Under workload-group pressure this can hit the hard limit instead of pausing for revocation. Estimate the pending write from the current Block and spill settings before entering this branch.

size_t AnalyticSourceOperatorX::get_reserve_mem_size(RuntimeState* state) {
auto& local_state = get_local_state(state);
return OperatorX<AnalyticLocalState>::get_reserve_mem_size(state) +
local_state._spill_replay_reserve_bytes(state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Recompute admission for a retained replay Block. The first read of a coalesced spill Block records a large reader-open/deserialization peak in the base operator estimate. On later calls that only cut a small batch_size slice from _replay_block, this override still requests that prior peak even though _spill_replay_reserve_bytes adds zero and no disk read occurs. At a tight workload-group watermark the sole consumer can be paused before it can produce a smaller estimate. Size the next call from its actual slice and result allocations instead of reusing the previous read peak.

const auto bytes = local_state._batch_store->revocable_mem_size();
const auto min_revocable_mem = static_cast<size_t>(state->spill_min_revocable_mem());
return bytes > min_revocable_mem ? bytes : 0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Expose staged spill bytes to revocation below 32 MiB. After a partition begins spilling, the write buffer and peer-end buffer can retain several MiB yet stay below the default spill_min_revocable_mem threshold, so this returns zero. PipelineTask would revoke at 512 KiB, and spill() can flush those buffers. If workload-group admission then pauses this sink while the partition remains open, no batch is sealed for the source and the pause handler finds no revocable task; it can time out. Report flushable bytes at least down to the pipeline's revoke floor under pressure.

for (size_t i = 0; i < function_count; ++i) {
results[i] = _current_batch->result_types[i]->create_column();
results[i]->reserve(rows);
if (_current_batch->strategies[i] == WindowSpillStrategy::PARTITION_CARDINALITY ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Admit the first in-memory replay for its result columns. A queued batch with no spill files adds zero to source reservation, and the first call gets only the 4 MiB base minimum. With the legal batch_size of 65,535, one narrow input Block can remain in memory while 32 Int64 window results reserve about 16 MiB here before insertion. Under a tight memory limit this bypasses admission and can hit the hard limit. Include next-slice row count and result widths/function count in the source estimate even for in-memory batches.

@mrhhsg

mrhhsg commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

run buildall

mrhhsg added a commit to mrhhsg/doris that referenced this pull request Oct 9, 2026
…spill

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: After a window function batch starts spilling, every appended Block was written to the spill file on its own, and each write serializes, ZSTD-compresses and indexes one Block. Input Blocks are usually far smaller than `spill_buffer_size_bytes` (8MB by default), so a large partition was written as many small spill blocks. On TPC-DS SF10000, one window sink instance wrote about 1.29M blocks of about 65KB each. In addition, `enable_force_spill` or a proactive threshold smaller than the buffer triggered a spill for every append after the first one.

Once a batch is spilled, small Blocks are now merged into a write buffer in input order and written when the buffer reaches `spill_buffer_size_bytes`. A Block that already reaches that size flushes the buffer first and is written directly. `spill()` (used by revoke) and `seal()` always flush the buffer, the buffer is part of `revocable_mem_size()`, and a spilled store no longer takes the proactive spill path. Because a replayed Block can now be much larger than the batch size, the source slices it into `batch_size` rows before computing window results.

Local benchmark with 50M narrow rows (two bigint columns), `spill_analytic_sink_mem_limit_bytes=1MB`:
- `sum(v) over ()`: spill blocks 6152 -> 96, sink serialize time 958ms -> 388ms, source deserialize time 273ms -> 145ms, spill file bytes 38MB -> 11.8MB, query 1.81s -> 1.26s; sink peak memory 1.06MB -> 4.76MB, bounded by the spill buffer size.
- `sum(v) over (partition by k % 8)`: spill blocks 6128 -> 192, spill file bytes 35.5MB -> 20.3MB, query 0.90s -> 0.76s.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test / Manual test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (34 passed), including new cases for coalescing small appends, the final partial buffer, forced flush on revoke, and replay slicing by batch size
    - `./run-regression-test.sh --run -d spill_p0` passed
    - Manual benchmark above; scaled-down TPC-DS validation passed with a 1MB spill threshold
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Oct 9, 2026
Issue Number: None

Related PR: apache#68500

Problem Summary: Three gaps in the window function spill path:

1. The analytic source had no reservation for reading spilled data. Before a replay reads a new spilled record it relied on the generic previous-call estimate (at least 4MB), while opening a batch reader allocates a buffer for the largest serialized record and reading deserializes a whole record, each up to about `spill_buffer_size_bytes` after small Blocks are coalesced. The source now adds one spill buffer to its reservation when it is about to read a record of a spilled batch, and two when it is about to open the next batch (reader buffer plus deserialized Block). In-memory batches and partially consumed Blocks add nothing.
2. The peer group end buffers of the batch store and of the sealed batch used `std::vector`, which bypasses the query MemTracker, although they hold up to `spill_buffer_size_bytes` per task. They now use `DorisVector`.
3. After a batch was spilled the store no longer forced a spill on every append, so no test created the peer group sidecar file any more. New tests cover sidecar creation on revoke with two metadata Blocks, sidecar flushing at the 1MB spill buffer threshold while the rows stay in memory, and the replay reservation.

None

- Test: Regression test / Unit Test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (36 passed)
    - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Oct 9, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Before publishing an in-memory batch to the source, the window function sink spilled it whenever its revocable memory exceeded `spill_min_revocable_mem`, regardless of memory pressure. FE sends 4MB for that option by default, and with high-cardinality partitions every input Block that ends a partition is sealed as a batch, so rows with a wide column (for example a ~1KB STRING, about 16MB per 8160-row Block) wrote and read back every Block, turning an in-memory query into a disk-bound one. `spill_min_revocable_mem` is only a "worth revoking" threshold elsewhere; the per-batch in-memory limit of this operator is `spill_analytic_sink_mem_limit_bytes` (64MB by default).

A sealed batch is now spilled only when it reaches `spill_analytic_sink_mem_limit_bytes`, the same limit that triggers the proactive spill of the open partition, so memory that cannot be revoked after publication stays bounded by that limit plus one Block.

Also:
- add UTs that keep a sealed batch above `min_revocable_mem` but below the sink limit in memory, and that trigger the proactive spill through `spill_analytic_sink_mem_limit_bytes` without forced spill or revoke;
- assert the scheduler-facing `get_reserve_mem_size` of the analytic source in the replay reservation test;
- let `~SpillFileWriter` close writers left open by a failed or cancelled query so that close failures are logged instead of discarded.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed)
    - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold
- Behavior changed: No
- Does this need documentation: No

Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Oct 9, 2026
… peer group sidecar

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Two gaps in the replay memory reservation of the analytic source:

1. At every batch boundary the source reserved two spill buffers before it knew whether the next batch was spilled. A batch sealed below `spill_analytic_sink_mem_limit_bytes` is published in memory, and its bytes are no longer revocable by the sink; if a workload group rejects that reservation at its high watermark while the query has no revocable task, the only consumer stays paused until the paused-queue timeout although replaying the batch would open no file. The source now inspects the batch at the head of the queue under `buffer_mutex` and reserves two spill buffers for each of its spilled files (`data_file`, `peer_group_file`), nothing for an in-memory batch and nothing when the queue is empty.
2. The reservation ignored the peer group sidecar. Opening a batch opens the sidecar reader and reads a sidecar Block before the first data record, and the sidecar is read again during replay; each record holds up to `spill_buffer_size_bytes` of group ends, so a replay could allocate well beyond its reservation. The sidecar now gets the same two buffers when the batch is opened, and during replay one buffer is reserved exactly when the next `batch_size` slice contains a row beyond the last group end read so far.

UTs assert the reservation for a data-only spilled batch, a batch with both files, an in-memory batch, and the slice that reads the second sidecar record while the rows stay in memory. The design document describes the reservation rule.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed)
    - `build-support/check-format.sh`, `build-support/check-build-hygiene.sh`, `git diff --check` passed
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete at head 389d015ff948a26109e6ab5a02fcef935f55c2cc after two convergence rounds, a final 23-file sweep, and live-thread deduplication. One new P2 is inline below: a stale sink spill-write peak can keep an empty EOS call from publishing its final batch under sustained workload-group pressure. Seven still-applicable P2 admission issues already have inline threads (4226675241, 4226675247, 4226675253, 4226675257, 4226675264, 4226675269, 4226675278), so I have not reposted them. Earlier coalescing and initial queued-batch/sidecar reservation threads (4120354529, 4139868859, 4139868867) are addressed at this head. No existing P0/P1 inline finding remains.

Critical checkpoints:

  • Goal and tests: The code adds capability-gated spill for eligible non-streaming window partitions. Static inspection of the BE cases and all 12 regression result groups supports the covered sum/count/avg/min/max, NTILE, peer-function, fallback, and streaming cases; each .out group matches across spill-disabled, in-memory, and forced-spill modes. Those artifacts do not exercise the reported workload-group pause or prove runtime memory bounds.
  • Scope and reuse: The changes are focused on analytic sink/source, a separate batch store, aggregate capabilities, one query option, and tests/docs. They reuse the existing SpillFile, pipeline dependency, and aggregate interfaces; no unrelated product path was found in the authoritative diff.
  • Concurrency and locks: Sink and source run in separate pipeline tasks. Sealed batches are published and dequeued under buffer_mutex; EOS uses sink_eos_lock without a reverse acquisition path found. The source is signalled after publication, and the sink is signalled when the queue empties. The new P2 concerns admission before that publication, not a lock inversion.
  • Lifecycle and static initialization: The sink closes writers before publishing each immutable batch; source completion/close releases readers and batch ownership, and unfinished writer destruction attempts close. No new cross-translation-unit static initialization dependency was found. Failure/cancellation cleanup was inspected statically; no fault-injection run was performed.
  • Configuration and FE/BE compatibility: The session variable reaches ordinary SQL, INSERT SELECT, and CTAS through toThrift; BE clamps the optional field and uses the same 64 MiB default when it is absent. Broker-load coordinator defaults also leave query spill disabled. No persisted format or function symbol changed; the optional Thrift addition has a default for mixed versions. This session option is evaluated for each query, so no running-query dynamic refresh is expected.
  • Parallel paths and conditions: Spill eligibility is gated by every function's capability and the supported frame. Streaming and unsupported nodes retain the prior path; no-ORDER-BY RANGE CURRENT ROW is treated as a whole-partition frame. Partition, peer, nullable, NTILE, and aggregate reset paths were rechecked without a distinct semantic defect.
  • Results and coverage: The checked-in .out data is internally consistent across modes but its generation and correctness against an independent oracle were not verified here. BE tests cover in-memory/disk replay, multi-partition batches, nullable results, peer sidecars, revoke, and slicing. Workload-group admission, fault injection, and large-memory behavior need executed validation; the PR description's test and benchmark claims are author reports.
  • Observability and performance: Spill mode, partition, peer metadata, and common spill counters support diagnosis. Coalescing and batch-size slicing address obvious write/read amplification; existing inline threads and the new P2 identify the remaining admission risks. No benchmark was run in this review.
  • Persistence and writes: This change writes temporary spill files, not table data or EditLog state; transaction, visible-version, delete-bitmap, and failover checkpoints are not applicable. The temporary-file writer/reader lifecycle and error propagation were reviewed as above. No separate data-visibility issue was substantiated.
  • Other issues: No additional substantiated finding remained after deduplication and the final changed-file sweep.

The user focus file contained only -light and named no additional code concern; the complete PR was reviewed. Review work was static only, except the text-only check-build-hygiene.sh gate, which passed. No build, BE unit test, or regression test was run in this invocation.

}

Status AnalyticSinkLocalState::_finish_spill_sink_call(RuntimeState* state, bool eos) {
if (eos) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Recompute admission for the empty EOS call. A preceding non-EOS call can spill the open partition and leave a large PBlock/compression peak in _reserve_mem_size even after its buffers are flushed and revocable_mem_size() is zero. PipelineTask asks for that stale peak before entering this branch; if the workload group stays above its high watermark, the sink remains paused while the source waits for this unpublished batch, and the pause handler can cancel the query on timeout. Size the pending seal/close work from the current state instead of carrying the prior write peak into EOS.

@mrhhsg

mrhhsg commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.84% (2079/2671)
Line Coverage 66.02% (38155/57795)
Region Coverage 53.44% (35754/66907)
Branch Coverage 56.97% (11522/20223)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29366 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 389d015ff948a26109e6ab5a02fcef935f55c2cc, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17613	5682	5598	5598
q2	2099	301	247	247
q3	10263	1429	846	846
q4	4680	482	348	348
q5	7443	817	552	552
q6	185	189	159	159
q7	791	802	610	610
q8	9252	1342	1188	1188
q9	5761	4505	4433	4433
q10	6843	1311	1022	1022
q11	454	258	241	241
q12	634	406	292	292
q13	18074	3097	2391	2391
q14	274	283	241	241
q15	q16	750	733	692	692
q17	1221	740	586	586
q18	7093	6269	6214	6214
q19	1133	974	601	601
q20	398	335	228	228
q21	5131	2582	2617	2582
q22	409	336	295	295
Total cold run time: 100501 ms
Total hot run time: 29366 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6942	6692	6827	6692
q2	774	588	571	571
q3	5486	5805	5269	5269
q4	2082	2236	1589	1589
q5	5828	5763	5843	5763
q6	246	186	147	147
q7	2276	1958	1876	1876
q8	3136	2804	2773	2773
q9	8255	8258	7914	7914
q10	3677	3602	3248	3248
q11	588	416	379	379
q12	667	709	520	520
q13	2743	3087	2377	2377
q14	283	291	260	260
q15	q16	675	690	630	630
q17	7882	7019	6946	6946
q18	13099	12285	13114	12285
q19	922	829	852	829
q20	2188	2187	1947	1947
q21	5904	4751	4840	4751
q22	510	452	394	394
Total cold run time: 74163 ms
Total hot run time: 67160 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152378 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 389d015ff948a26109e6ab5a02fcef935f55c2cc, data reload: false

query5	4306	575	451	451
query6	432	210	187	187
query7	4801	440	228	228
query8	313	181	169	169
query9	8761	4022	4066	4022
query10	480	324	263	263
query11	5987	3442	3189	3189
query12	160	90	85	85
query13	1253	425	319	319
query14	6552	4804	4529	4529
query14_1	4227	4201	4268	4201
query15	206	204	187	187
query16	968	466	444	444
query17	1090	661	535	535
query18	2424	435	323	323
query19	199	174	136	136
query20	80	88	89	88
query21	211	136	113	113
query22	12951	12963	12829	12829
query23	13329	12664	12233	12233
query23_1	12402	12170	12249	12170
query24	7086	819	473	473
query24_1	461	514	465	465
query25	508	396	336	336
query26	1249	258	135	135
query27	2788	436	259	259
query28	4639	1938	1941	1938
query29	1553	579	426	426
query30	287	220	183	183
query31	887	764	640	640
query32	139	88	89	88
query33	506	317	232	232
query34	938	861	499	499
query35	739	753	651	651
query36	847	807	726	726
query37	131	106	82	82
query38	1764	1735	1685	1685
query39	748	713	696	696
query39_1	651	666	679	666
query40	214	112	98	98
query41	65	62	63	62
query42	86	81	80	80
query43	374	377	331	331
query44	1280	690	695	690
query45	200	183	161	161
query46	838	965	564	564
query47	2801	3001	2907	2907
query48	288	298	213	213
query49	561	393	295	295
query50	680	271	216	216
query51	10370	10181	10547	10181
query52	83	82	66	66
query53	184	204	151	151
query54	273	209	173	173
query55	70	65	63	63
query56	233	232	220	220
query57	1598	1582	1546	1546
query58	283	275	250	250
query59	2367	2386	2131	2131
query60	274	238	227	227
query61	148	147	143	143
query62	384	344	285	285
query63	190	162	164	162
query64	2732	904	764	764
query65	3446	3360	3356	3356
query66	1800	410	297	297
query67	20320	20285	20340	20285
query68	3211	976	605	605
query69	397	290	242	242
query70	945	839	843	839
query71	296	225	209	209
query72	2802	2653	2441	2441
query73	550	521	296	296
query74	4589	4486	4323	4323
query75	2379	2306	1958	1958
query76	2383	1026	585	585
query77	393	399	309	309
query78	8986	9023	8555	8555
query79	855	891	503	503
query80	542	446	373	373
query81	541	333	274	274
query82	321	136	116	116
query83	301	205	183	183
query84	267	131	104	104
query85	883	425	371	371
query86	333	236	234	234
query87	1991	1950	1856	1856
query88	3570	2686	2664	2664
query89	334	296	257	257
query90	1842	185	179	179
query91	165	146	121	121
query92	95	88	87	87
query93	979	982	548	548
query94	446	298	284	284
query95	607	404	320	320
query96	678	516	230	230
query97	2476	2420	2350	2350
query98	154	148	146	146
query99	778	762	669	669
Total cold run time: 233995 ms
Total hot run time: 152378 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.45 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 389d015ff948a26109e6ab5a02fcef935f55c2cc, data reload: false

query1	0.01	0.01	0.00
query2	0.10	0.05	0.04
query3	0.26	0.13	0.12
query4	1.62	0.12	0.12
query5	0.25	0.26	0.25
query6	1.18	0.68	0.69
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.42	0.36	0.35
query10	0.57	0.60	0.57
query11	0.21	0.15	0.14
query12	0.18	0.14	0.16
query13	0.49	0.49	0.50
query14	0.97	0.96	0.98
query15	0.62	0.59	0.63
query16	0.33	0.34	0.35
query17	1.16	1.09	1.12
query18	0.23	0.21	0.20
query19	2.11	1.96	1.96
query20	0.02	0.01	0.01
query21	15.50	0.25	0.15
query22	4.48	0.06	0.05
query23	16.23	0.32	0.12
query24	3.21	0.60	0.42
query25	0.10	0.06	0.05
query26	1.03	0.26	0.16
query27	0.05	0.04	0.04
query28	1.79	0.61	0.41
query29	12.50	4.12	3.25
query30	0.28	0.13	0.14
query31	2.78	0.59	0.34
query32	3.22	0.62	0.53
query33	3.18	3.17	3.20
query34	15.49	4.00	3.41
query35	3.35	3.33	3.34
query36	0.55	0.45	0.44
query37	0.10	0.07	0.06
query38	0.06	0.04	0.03
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.08	0.04	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 95.1 s
Total hot run time: 24.45 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 100.00% (2/2) 🎉
Increment coverage report
Complete coverage report

@mrhhsg

mrhhsg commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

run check_coverage

}
assert_cast<ColumnFloat64&>(*computed_result_columns[i])
.get_data()
.push_back(_current_peer_group_result(i));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore peer-range result filling in spill replay

This replaces the existing peer-range evaluation/fill pattern with per-row evaluation. In the original path, _get_next_for_unbounded_range() passes a peer/output-Block range to _insert_result_info(), and WindowFunctionPercentRank::insert_result_into_range() / WindowFunctionCumeDist::insert_result_into_range() compute the value once before filling that range. Here _current_peer_group_result(i) is called inside the row loop, repeating the function-kind branch and ratio calculation for every row, followed by an individual push_back().

Within a partition and peer group, _peer_group_start, _peer_group_end, _partition_start and _partition_end are unchanged, so both results are constant. With one million rows in one peer, output Blocks of 4096 rows, and both functions, the old path performs about 490 formula evaluations across 245 Blocks; this path performs two million source-level evaluations. These are code-level counts, not a measured query-time slowdown. The extra work also affects eligible queries that enable spill but never actually write to disk.

Please process each intersection of the current peer and output slice as a range, compute each peer-function result once, and reuse ColumnFloat64::insert_many_vals(value, count). _append_spill_results() already bounds this call by the partition. Inside this helper the next range length can be:

const int64_t pos = _batch_output_position + offset;
// Advance peer metadata while pos >= _peer_group_end.
const size_t count =
        std::min<size_t>(rows - offset, _peer_group_end - pos);

Fill that range and advance the local offset; keep _batch_output_position advancement in the existing caller. This preserves the current spill metadata and requires no new cross-call cache. Retain cancellation checks and validate groups crossing output Blocks and sidecar records. A focused benchmark with few large peers versus one-row peers would quantify the CPU/query-time effect.

Original batch-fill implementations:

void insert_result_into_range(ConstAggregateDataPtr __restrict place, IColumn& to,

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Window spill replay estimated a retained input slice from serialized row bytes. A NULL fixed-width nullable value serializes without its nested value, but ColumnNullable::cut copies both the null map and nested storage, so a wide all-NULL slice could be admitted with too little memory. Use the in-memory type width for non-constant fixed-width columns while preserving the existing variable-width and constant paths. Replay also evaluated PERCENT_RANK and CUME_DIST once per row even though each peer/output intersection has a single result; fill that intersection in one operation while retaining cancellation checks. Add focused storage-budget and cross-Block/sidecar peer tests.

### Release note

Window spill replay accounts for fixed-width nullable input slices and fills peer results by range.

### Check List (For Author)

- Test: Regression test and compile checks
    - Regression test: spill_p0/window_function_spill passed with an isolated ASAN BE and FE cluster.
    - Unit Test: focused test source compiled; execution was blocked by pre-existing function_string_test.cpp compile errors in the standard BE UT runner.
    - Manual test: None.
- Behavior changed: Yes (spill replay reservation and peer-result fill strategy; SQL results unchanged).
- Does this need documentation: No.
@mrhhsg

mrhhsg commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 27 changed files at af6a803 with two general-scope reviewers and a separate admission/concurrency reviewer. One new P2 is reported inline: a revocation test's input allocation reaches the direct-write threshold, leaving zero revocable bytes and invalidating its expectation. This is a static code-path proof; no build or runtime test was executed.

Four previously reported concerns still apply and are not reposted: retained NTILE parameter accounting, combined coalesced-write admission, the separate first-slice reservation test expectation, and a residual case of initial replay scheduling. The scheduler change covers the sink and later source submissions, but an initial source submission can occur before sink open sets spill_enabled; publication before that source reaches its dependency check can still permit synchronous replay I/O in the simple pool. There are no existing P0/P1 comments to carry forward. The current head addresses the previous adaptive-boundary-search, peer-range-fill, fixed-width nullable-cut, fixture-manager-lifetime and other concrete replay-admission reports.

Critical checkpoint conclusions:

  1. Goal and proof: eligible full-partition reductions, NTILE and peer functions use collection/sealed-batch/replay; streaming and unsupported windows retain the existing path. Positive tests support the intended behavior, subject to the inline test defect and existing concerns above.
  2. Scope and reuse: changes are focused on analytic spill, shared writer measurements, one query option, tests and design documentation; aggregate update/result and spill-file infrastructure are reused.
  3. Concurrency and locks: sink/source publication is protected by buffer_mutex, EOS by sink_eos_lock, and eligibility is atomic. I/O runs outside queue locks; no new lock inversion was verified. Per-task revoke gating prevents simultaneous store mutation. Initial scheduler classification retains the existing concern described above.
  4. Lifecycle and initialization: seal closes writers before publication; source owns immutable result descriptors and closes readers before releasing files. Store writers are destroyed before their files; the fixture manager now outlives fixture-owned state. No new ownership cycle or cross-TU initializer dependency was identified. NTILE metadata accounting remains an existing issue.
  5. Configuration: the session threshold is forwarded per query, defaults to 64 MiB and is clamped in BE to 1 MiB through 4 GiB; a newly submitted query observes its session value without a process restart.
  6. Compatibility: no durable storage or existing spill-record format changes; record/footer maxima are process-local descriptor metadata and existing block serialization remains in use.
  7. Thrift/protobuf: read TQueryOptions on HEAD and the current branch-3.1, branch-4.0, branch-4.1 and branch-4.2 refs. New field 237 is optional and unused on those releases; it introduces no field-id collision.
  8. Parallel paths: supported capabilities and both nullable adapters are covered; variable-length MIN/MAX, unsupported mixtures and streaming/offset frames fall back. Local and remote spill share the writer/reader implementation; remote coalescing is accounted separately.
  9. Conditions: frame selection, complete-key boundary search, spill thresholds and peer-range filling have concrete invariants/comments. Required upstream ordering makes complete-key equality contiguous, including composite and nullable keys.
  10. Test coverage: reviewed eligibility, reductions/NULLs, scalar parameters, cross-Block partitions/peers, coalescing, oversize records, revoke, replay slicing and admission cases, plus 15 regression queries in three modes. Runtime fault injection, remote/early-close cleanup, tracked-memory-drop and adversarial numeric/performance validation remain unexecuted or absent from the added tests.
  11. Expected results: all 45 regression output sections agree across modes; independent arithmetic checks agree with the displayed results. The inline staged-buffer test mismatch and already-reported replay-cut expectation need correction. Output-generation provenance was not independently established.
  12. Observability: mode/reason, partition counts/rows/buffered bytes, peer metadata and replay timing supplement existing spill counters and contextual error logs. Profile counters do not remedy missing MemTracker accounting.
  13. Transactions and persistence: no EditLog, commit, visible-version, delete-bitmap or master-failover protocol changes.
  14. Data writes and failures: writes are temporary query spill files; failed writes/reads propagate failure and unsuccessful seal prevents publication. Cleanup reuses existing SpillFile ownership/GC; committed table data is unchanged.
  15. FE/BE propagation: SessionVariable forwarding/toThrift, the optional query option and RuntimeState consumption are connected; point-query and constant-folding paths do not execute analytic windows.
  16. Performance: adaptive key search, range result filling, small-block coalescing and sliced replay were traced; no benchmark or runtime memory bound was measured. Combined-write admission remains the existing concern above.
  17. Additional review and user focus: rechecked boundary/NULL/scalar, snapshot/EOS, cancellation/LIMIT and ownership cases. No additional user focus was supplied and no other distinct issue was verified.

Static coverage is complete for this invocation. Runtime behavior, sanitizer results and performance are not certified by this review.

Review converged after 2 round(s).

Block second = ColumnHelper::create_block<DataTypeInt64>(values);
status = sink->sink(state.get(), &second, false);
ASSERT_TRUE(status.ok()) << status.to_string();
EXPECT_GE(sink->revocable_mem_size(state.get()), SpillFile::MIN_SPILL_WRITE_BATCH_MEM);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the staged test block below the spill-buffer threshold

ColumnHelper::create_column inserts these 100,000 Int64 values into a doubling PaddedPODArray, so the second Block allocates 1,048,576 bytes, exactly the configured 1 MiB spill buffer. The first call has already created the writer, so _write_data_block() takes its allocated_bytes() >= spill_buffer_bytes direct-write branch for this second Block. No data or peer buffer remains, revocable_mem_size() returns zero, and this assertion fails instead of exercising the intended 512 KiB revocation case. Use a 2 MiB spill buffer here, or a second Block whose allocation is at least 512 KiB and strictly below 1 MiB. This is separate from the already-reported first-slice reservation expectation at line 1832; the failure follows statically from the allocation and write branches.

@mrhhsg

mrhhsg commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

run buildall

1 similar comment
@shuke987

Copy link
Copy Markdown
Contributor

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 100.00% (2/2) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28344 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit af6a8034f5aafc2c4da5e36fa5cd074f04870ae5, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17606	5167	5129	5129
q2	2144	306	252	252
q3	10244	1392	804	804
q4	4678	462	334	334
q5	7432	784	532	532
q6	181	183	152	152
q7	732	799	604	604
q8	9482	1314	992	992
q9	5588	4179	4174	4174
q10	6879	1296	1034	1034
q11	445	244	227	227
q12	644	392	275	275
q13	18119	3540	3009	3009
q14	276	270	244	244
q15	q16	734	713	660	660
q17	1206	758	583	583
q18	7070	6192	6155	6155
q19	1130	958	588	588
q20	382	336	230	230
q21	5103	2115	2233	2115
q22	349	297	251	251
Total cold run time: 100424 ms
Total hot run time: 28344 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5618	5595	5639	5595
q2	661	556	551	551
q3	5243	5370	4928	4928
q4	1977	2162	1529	1529
q5	5247	5151	5628	5151
q6	262	203	164	164
q7	2300	2012	1863	1863
q8	3053	2641	2626	2626
q9	8038	7952	7816	7816
q10	3705	3668	3239	3239
q11	622	429	604	429
q12	692	708	526	526
q13	3465	3852	3264	3264
q14	325	306	275	275
q15	q16	719	754	638	638
q17	8208	7456	6853	6853
q18	13017	12260	12862	12260
q19	889	768	760	760
q20	2165	2143	1905	1905
q21	5525	4531	4832	4531
q22	502	448	390	390
Total cold run time: 72233 ms
Total hot run time: 65293 ms

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Window spill peer replay fills a constant result for each
peer/output intersection, but retained row-based cancellation checks by
splitting fills at 4096-row boundaries. Check cancellation before each fill
instead and let the peer and output boundaries determine the fill size.
Replay output is already bounded by the runtime batch size. Add unit coverage
for cancellation before filling, a large peer filled without artificial row
splits, and cancellation between uneven peers. SQL results are unchanged.

### Release note

None

### Check List (For Author)

- Test: Unit test compilation and ASAN BE build
    - Unit Test: Added two focused peer-fill tests. The modified source and test translation units compiled through run-be-ut.sh, but test execution was blocked by existing function_string_test.cpp compilation errors.
    - Manual test: ASAN ./build.sh --be -j16, clang-format v16, build hygiene, and git diff --check passed. clang-tidy was attempted but blocked by the toolchain's missing stddef.h resource header.
    - Regression test: Not run for this cancellation-check refactoring.
- Behavior changed: Yes (check cancellation before each peer/output fill instead of every 4096 output rows; SQL results unchanged).
- Does this need documentation: No.
@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150505 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit af6a8034f5aafc2c4da5e36fa5cd074f04870ae5, data reload: false

query5	4311	593	428	428
query6	422	205	188	188
query7	4808	445	245	245
query8	335	162	150	150
query9	8787	3081	3132	3081
query10	472	297	245	245
query11	6072	3870	3571	3571
query12	156	96	92	92
query13	1272	419	311	311
query14	6157	3934	3677	3677
query14_1	3429	3455	3390	3390
query15	203	198	181	181
query16	947	464	444	444
query17	865	659	552	552
query18	2419	434	309	309
query19	197	169	138	138
query20	87	84	83	83
query21	216	126	106	106
query22	12944	12991	12726	12726
query23	13435	12844	12362	12362
query23_1	12555	12386	12484	12386
query24	7366	802	452	452
query24_1	491	456	463	456
query25	508	396	339	339
query26	1258	253	132	132
query27	2781	428	266	266
query28	4636	1933	1928	1928
query29	1551	561	422	422
query30	295	209	187	187
query31	997	923	813	813
query32	150	87	88	87
query33	515	289	232	232
query34	909	835	483	483
query35	721	753	638	638
query36	838	827	714	714
query37	121	91	79	79
query38	1754	1721	1664	1664
query39	788	786	750	750
query39_1	732	725	743	725
query40	214	110	93	93
query41	67	62	63	62
query42	86	81	80	80
query43	343	362	307	307
query44	1291	693	688	688
query45	194	178	165	165
query46	819	956	575	575
query47	3043	2982	2846	2846
query48	289	309	215	215
query49	568	391	289	289
query50	658	264	205	205
query51	10520	10583	10765	10583
query52	75	89	68	68
query53	184	208	151	151
query54	272	215	200	200
query55	75	63	59	59
query56	228	226	204	204
query57	1640	1608	1558	1558
query58	284	257	244	244
query59	2131	2106	1956	1956
query60	283	254	228	228
query61	147	145	134	134
query62	386	328	281	281
query63	182	164	156	156
query64	2715	932	773	773
query65	3356	3295	3279	3279
query66	1773	414	316	316
query67	20302	20115	19895	19895
query68	2988	1006	620	620
query69	395	302	226	226
query70	823	778	819	778
query71	292	232	210	210
query72	2752	2541	2278	2278
query73	503	520	295	295
query74	4587	4511	4374	4374
query75	2318	2288	1937	1937
query76	2317	1019	578	578
query77	340	365	266	266
query78	9780	9682	9194	9194
query79	898	843	509	509
query80	510	415	354	354
query81	523	329	267	267
query82	579	127	104	104
query83	298	190	182	182
query84	302	116	103	103
query85	828	423	367	367
query86	314	236	227	227
query87	1974	1957	1810	1810
query88	3615	2683	2675	2675
query89	322	281	243	243
query90	1868	171	172	171
query91	161	159	125	125
query92	97	88	92	88
query93	1000	1002	599	599
query94	445	315	271	271
query95	577	333	361	333
query96	657	475	231	231
query97	2415	2419	2350	2350
query98	172	154	147	147
query99	775	756	649	649
Total cold run time: 233848 ms
Total hot run time: 150505 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.14 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit af6a8034f5aafc2c4da5e36fa5cd074f04870ae5, data reload: false

query1	0.01	0.01	0.00
query2	0.15	0.09	0.08
query3	0.36	0.12	0.13
query4	1.61	0.14	0.13
query5	0.26	0.25	0.25
query6	1.16	0.66	0.65
query7	0.04	0.01	0.01
query8	0.09	0.08	0.07
query9	0.44	0.36	0.36
query10	0.57	0.62	0.60
query11	0.34	0.18	0.18
query12	0.31	0.18	0.19
query13	0.51	0.52	0.51
query14	0.95	0.93	0.91
query15	0.65	0.57	0.57
query16	0.32	0.35	0.36
query17	1.02	1.06	1.00
query18	0.29	0.28	0.28
query19	1.93	1.83	1.86
query20	0.02	0.01	0.01
query21	15.38	0.34	0.30
query22	4.93	0.12	0.14
query23	15.84	0.46	0.29
query24	2.25	0.52	0.39
query25	0.15	0.11	0.10
query26	0.78	0.26	0.20
query27	0.10	0.10	0.09
query28	3.33	0.77	0.37
query29	12.58	4.29	3.38
query30	0.35	0.17	0.15
query31	2.76	0.56	0.33
query32	3.23	0.60	0.48
query33	2.93	3.09	3.02
query34	15.91	4.09	3.46
query35	3.38	3.38	3.38
query36	0.61	0.50	0.45
query37	0.11	0.09	0.10
query38	0.07	0.07	0.07
query39	0.06	0.06	0.06
query40	0.19	0.18	0.16
query41	0.11	0.05	0.06
query42	0.06	0.06	0.06
query43	0.06	0.06	0.05
Total cold run time: 96.2 s
Total hot run time: 25.14 s

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68500

Problem Summary: Window spill retained one identical NTILE bucket argument
for every partition even though FE requires a positive integer literal per
function. The nested standard vectors used O(functions * partitions)
untracked parameter storage and required per-partition append and lookup.
Cache each constant once on the first nonempty input, copy one allocator-aware
scalar array into each batch, and replay by function index. Remove the two
partition-parameter states and make the NTILE admission allowance independent
of the partition count. For 32 functions and 65,535 partitions, the parameter
payload per descriptor falls from approximately 16 MiB to 256 bytes; no
query-level performance benchmark is claimed. Partition boundaries and
per-partition reduction results are unchanged.

### Release note

None

### Check List (For Author)

- Test: ASAN BE build; 6 NTILE unit tests pass; window_function_spill regression
  generated and rerun on isolated ports, with 16 queries matching in no-spill,
  in-memory replay, and forced-spill modes; clang-format v16, build hygiene,
  and changed-line clang-tidy pass.
  - Broader analytic unit run: 62/65 pass; all three memory assertion failures
    reproduce with identical values on parent a225ec4. Standard full UT
    compilation is blocked by existing function_string_test.cpp API errors;
    analytic UTs use a temporary source filter with run-be-ut.sh, restored
    after testing.
- Behavior changed: No SQL result changes; remove redundant parameter storage.
- Does this need documentation: No external documentation; internal window
  spill design documentation updated.
### What problem does this PR solve?

Issue Number: N/A

Related PR: apache#68500

Problem Summary: The PARTITION_CARDINALITY name suggests support for any
window function that needs a partition's row count, but this replay path
implements NTILE only. Rename the strategy to NTILE and name its cached
parameters ntile_bucket_counts across sink, batch metadata, source, tests,
and design documentation. Preserve the enum values and replay behavior.

### Release note

None

### Check List (For Author)

- Test: ASAN BE build; focused unit tests; window_function_spill regression
    - Unit Test: 74/74 pass together with the following BE UT admission fix;
      rename alone reproduced the same three pre-existing admission failures
    - Regression test: all 48 existing query tags pass
- Behavior changed: No
- Does this need documentation: Yes (design document updated in this commit)
### What problem does this PR solve?

Issue Number: N/A

Related PR: apache#68500

Problem Summary: BE UT build 1081123 reported three window spill admission
failures. Fixed-width output reservations counted row payloads but missed
PaddedPODArray padding and power-of-two allocation rounding. For 65535 rows,
a null map needs 131072 bytes, exceeding even twice the row count. Reuse the
PODArray allocation-size calculation for fixed-width cuts, result columns,
and added null maps. Keep allocation behavior unchanged and test the size
calculation against real allocations around power-of-two boundaries.

Correct two stale test setups: use a 512 KiB second block to actually leave
staged data below the 1 MiB writer threshold, and account for both input
cuts as well as the result column during in-memory replay. Do not change
spill thresholds or relax the actual-allocation lower-bound assertions.

### Release note

Improve memory reservations for fixed-width window spill output buffers.

### Check List (For Author)

- Test: ASAN BE build; Unit Test; Regression test
    - Unit Test: 74/74 focused analytic, boundary and PODArray tests pass
    - Regression test: window_function_spill passes all 48 existing query tags
    - Full local BE UT source set not run; used a restored temporary focused
      source list due to pre-existing unrelated string-test API build errors
- Behavior changed: Yes (reserve padded allocation capacity before output)
- Does this need documentation: No
@mrhhsg

mrhhsg commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

run beut

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 48.81% (41/84) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 66.90% (32231/48176)
Line Coverage 51.70% (340691/658947)
Region Coverage 47.16% (276589/586476)
Branch Coverage 48.81% (130322/267004)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 98.81% (83/84) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.38% (36139/46706)
Line Coverage 62.60% (408271/652145)
Region Coverage 59.08% (345406/584689)
Branch Coverage 59.97% (159828/266511)

@mrhhsg

mrhhsg commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.00% (0/16) 🎉
Increment coverage report
Complete coverage report

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR #68500 at bef288e. One new P2 finding: replay under-admits cuts of arrays with nullable elements. Existing scalar fixed-width sizing is repaired, but the recursive complex-column fallback still uses NULL-eliding serialization size as a bound on copied storage.

Critical checkpoint conclusions:

  1. Purpose and evidence: the capability-gated collector/replayer implements the intended supported non-streaming windows; the added tests exercise in-memory and spilled execution. Memory admission remains incomplete as described inline and in the known threads below.
  2. Scope and reuse: the change is substantial but focused on analytic spill, shared aggregate capabilities, and its query option; it reuses Block and the existing spill infrastructure.
  3. Concurrency and locks: queue publication/dequeue use buffer_mutex, EOS uses sink_eos_lock, and execution mode is atomic. The reviewed buffer-to-EOS lock order is consistent. I/O occurs outside the handoff locks; task execution and revocation exclude concurrent mutation of sink-owned state. No additional race or deadlock was substantiated.
  4. Lifecycle and initialization: writers close before immutable batch publication; readers close on source cleanup. File ownership is acyclic, writer/file destruction order is sound, and fixture teardown now keeps SpillDataDir alive. No new cross-translation-unit static initialization dependency was found.
  5. Configuration: the forwardable per-query option defaults to 64 MiB and BE clamps it to 1 MiB through 4 GiB. A new query observes its session setting without a process restart.
  6. Compatibility: no persistent storage or spill-record format change was found. Older receivers can ignore the new optional query option, and absent values use the BE default. Final ELF/glibc compatibility was not assessed in this static-only review.
  7. Thrift/protobuf: complete TQueryOptions definitions were compared with the live branch-3.1, branch-4.0, branch-4.1 and branch-4.2 refs. Optional i64 field 237 is unused on those releases; no new collision was found. Historical mismatches in other fields predate this PR.
  8. Parallel paths: disabled, streaming and unsupported nodes retain the existing executor. Eligible in-memory batches and local/remote data/peer files were traced. The source snapshot pins the batch admitted before the later EOF transition; newly published unadmitted batches are deferred.
  9. Conditions and invariants: frame gates, full composite-key boundaries, final peer sealing and NTILE positive-literal assumptions have concrete upstream guarantees. I/O errors propagate instead of publishing partial batches.
  10. Tests: BE cases cover eligibility, NULL reductions, distinct NTILE constants across partitions/batches, adaptive boundaries, replay slicing, sidecars, revoke and cancellation boundaries. Regression coverage spans 16 query families in three modes. Direct remote analytic spill, fault-injected write/close/read/GC failures and actual tracked-memory decline after revoke remain test gaps.
  11. Expected results: all 48 checked-in regression sections were inspected; matching query families have identical outputs across modes. Prior fixture/threshold/reservation-expectation defects are addressed on this head. Output regeneration and passing runtime results were not verified here.
  12. Observability: spill counters plus mode/reason, partition counts, peak buffering, peer metadata and replay timing cover the new paths; no new hot per-row logging was found.
  13. Transactions and persistence: no EditLog, failover, committed-data visibility, visible-version or delete-bitmap path changes apply.
  14. Writes and crash behavior: only disposable query spill files are added; publication follows successful close, and cleanup uses the existing spill infrastructure. No new transactional data-write behavior applies.
  15. FE/BE propagation: SessionVariable.toThrift and RuntimeState are connected. Constant-folding and point-query paths do not execute this analytic operator.
  16. Performance and memory: adaptive complete-key search, coalesced writes and peer-range fills avoid the previous repeated work; scalar NTILE storage is compact. The new array-cut finding and already-known admission concerns remain; no measured performance claim is made by this review.
  17. Other concerns: nullable adapters, decimal/floating accumulation order, bucket counts exceeding partition size, no ORDER BY, multiple partitions per Block and early termination were rechecked without another verified correctness finding.

Existing review context: coalesced-write scratch (4348511978), sink partition-result capacity (4347489392), and the broader constant-argument materialization concern (4348010167) remain duplicate-fenced and are not reposted. The specific NTILE expansion/vector, fixed-width nullable slice, peer-range filling, adaptive boundary search, scheduler classification, fixture teardown and test-expectation examples have been addressed. No existing P0/P1 inline finding was present to carry forward.

User focus: no additional focus was supplied; all 29 changed paths and applicable skill checkpoints were reviewed. The review converged after three rounds, including an independent pass over the corrected frozen payload. The concrete inline trigger explicitly sets minimum_operator_memory_required_kb=4096; FE normally forwards 32000 KiB, whereas 4 MiB is only the BE fallback when the option is absent. No unresolved review candidate remains. Validation is static only: no builds, tests, ASAN execution, remote spill execution or final-binary ABI audit was performed.

Review converged after 3 round(s).

bytes += padded_rows * column.type->get_size_of_value_in_memory();
} else {
for (size_t row = cut_start; row < cut_start + rows; ++row) {
bytes += 2 * column.column->serialize_size_at(row);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Size array replay cuts from their nested storage

The complex-column fallback still omits storage for NULL array elements. DataTypeArray has no fixed maximum width, so an array of 4096 NULL BIGINTs is estimated as 2 * (4096 + 8) bytes per row. But IColumn::cut() calls ColumnArray::insert_range_from() and then ColumnNullable::insert_range_from(), which copy every nested Int64 as well as the null map.

This is reachable with ordinary 512-row input Blocks, a 64 MiB spill buffer, a 16 MiB analytic sink limit, minimum_operator_memory_required_kb=4096, and a stored nonconstant array payload selected alongside count(*) over (). The first Block proactively spills; two subsequent roughly 36 MiB allocated Blocks coalesce into a 1024-row record. On the second 512-row replay slice, no file-read reservation is added: this fallback plus the configured 4 MiB minimum and COUNT result admits about 8 MiB, while the copied input alone needs at least 18 MiB before capacity rounding. Under memory pressure the cut can exceed admission before another revoke opportunity.

Please size complex cuts recursively from their actual nested ranges/null maps and allocation capacities, with a retained ARRAY cut budget test. This is the recursive ARRAY case explicitly left outside the fixed-width-only fix in discussion 4358224133; that repaired scalar-width branch does not cover this fallback.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29215 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit bef288e685e5d940a92a9ef7a1877a67ea16190a, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17599	5212	5115	5115
q2	2129	336	328	328
q3	10160	1414	843	843
q4	4683	464	354	354
q5	7431	823	550	550
q6	188	189	162	162
q7	767	802	613	613
q8	9283	1372	1152	1152
q9	5591	4261	4227	4227
q10	6838	1304	1029	1029
q11	446	247	229	229
q12	632	405	291	291
q13	18099	3686	3097	3097
q14	277	273	249	249
q15	q16	739	717	677	677
q17	1211	759	577	577
q18	7127	6209	6129	6129
q19	1108	973	642	642
q20	394	347	229	229
q21	5662	2515	2453	2453
q22	388	347	269	269
Total cold run time: 100752 ms
Total hot run time: 29215 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6000	5965	6170	5965
q2	749	599	560	560
q3	5673	5782	5381	5381
q4	2054	2173	1596	1596
q5	5830	5955	5653	5653
q6	240	185	149	149
q7	2252	2076	1802	1802
q8	3044	2694	2742	2694
q9	8000	7545	7534	7534
q10	3636	3591	3171	3171
q11	587	428	378	378
q12	674	690	504	504
q13	3342	3666	3083	3083
q14	300	303	267	267
q15	q16	668	676	612	612
q17	7850	7235	6973	6973
q18	12990	12223	12911	12223
q19	937	808	807	807
q20	2159	2162	1935	1935
q21	5999	4892	4959	4892
q22	513	438	443	438
Total cold run time: 73497 ms
Total hot run time: 66617 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152235 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit bef288e685e5d940a92a9ef7a1877a67ea16190a, data reload: false

query5	4303	607	451	451
query6	422	210	197	197
query7	4817	456	232	232
query8	320	187	159	159
query9	8782	3179	3172	3172
query10	501	343	271	271
query11	6008	3885	3590	3590
query12	146	94	90	90
query13	1259	462	338	338
query14	6159	3993	3754	3754
query14_1	3519	3520	3467	3467
query15	215	198	189	189
query16	950	471	447	447
query17	908	666	546	546
query18	2437	444	321	321
query19	210	172	143	143
query20	89	88	88	88
query21	216	137	114	114
query22	13081	12990	12725	12725
query23	13540	12905	12531	12531
query23_1	12528	12577	12517	12517
query24	7357	882	480	480
query24_1	498	491	496	491
query25	539	443	375	375
query26	1278	265	155	155
query27	2747	451	279	279
query28	4611	1996	1981	1981
query29	1568	631	484	484
query30	318	237	192	192
query31	1009	947	830	830
query32	149	102	99	99
query33	534	313	256	256
query34	901	867	515	515
query35	755	783	662	662
query36	833	829	740	740
query37	130	108	83	83
query38	1816	1808	1665	1665
query39	817	786	760	760
query39_1	745	732	738	732
query40	225	121	108	108
query41	94	84	85	84
query42	102	93	83	83
query43	363	368	331	331
query44	1389	749	707	707
query45	181	174	168	168
query46	851	977	590	590
query47	3039	3057	2852	2852
query48	303	320	222	222
query49	588	401	300	300
query50	716	290	216	216
query51	10329	10262	10517	10262
query52	85	84	73	73
query53	193	210	163	163
query54	235	199	197	197
query55	73	73	68	68
query56	245	213	214	213
query57	1659	1610	1594	1594
query58	283	285	266	266
query59	2142	2180	1963	1963
query60	296	252	241	241
query61	149	149	150	149
query62	398	335	288	288
query63	196	165	165	165
query64	2789	998	782	782
query65	3327	3315	3279	3279
query66	1786	415	304	304
query67	20141	20148	20132	20132
query68	3061	1042	619	619
query69	376	297	253	253
query70	865	797	782	782
query71	289	247	220	220
query72	2776	2602	2310	2310
query73	547	552	305	305
query74	4612	4557	4368	4368
query75	2365	2303	1935	1935
query76	2337	1041	632	632
query77	352	397	284	284
query78	10017	9780	9280	9280
query79	1081	880	535	535
query80	1230	440	360	360
query81	587	328	277	277
query82	561	136	109	109
query83	319	210	185	185
query84	318	130	106	106
query85	885	432	366	366
query86	392	239	232	232
query87	1985	1962	1820	1820
query88	3843	2727	2699	2699
query89	337	286	246	246
query90	1863	183	176	176
query91	167	160	136	136
query92	103	80	84	80
query93	1156	1033	584	584
query94	626	316	263	263
query95	579	402	307	307
query96	689	514	235	235
query97	2446	2430	2329	2329
query98	170	158	150	150
query99	776	757	647	647
Total cold run time: 236720 ms
Total hot run time: 152235 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.59 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit bef288e685e5d940a92a9ef7a1877a67ea16190a, data reload: false

query1	0.01	0.00	0.01
query2	0.09	0.05	0.05
query3	0.27	0.08	0.10
query4	1.60	0.11	0.10
query5	0.24	0.25	0.25
query6	1.18	0.69	0.69
query7	0.04	0.00	0.00
query8	0.05	0.04	0.04
query9	0.40	0.36	0.36
query10	0.60	0.61	0.61
query11	0.21	0.15	0.15
query12	0.19	0.15	0.15
query13	0.49	0.48	0.51
query14	1.03	1.00	1.02
query15	0.65	0.62	0.61
query16	0.36	0.36	0.36
query17	1.14	1.12	1.17
query18	0.24	0.19	0.20
query19	2.08	1.92	2.03
query20	0.02	0.01	0.01
query21	15.57	0.21	0.14
query22	4.74	0.05	0.05
query23	16.27	0.34	0.12
query24	2.77	0.64	0.41
query25	0.09	0.07	0.06
query26	1.29	0.21	0.16
query27	0.06	0.04	0.05
query28	3.55	0.82	0.38
query29	12.50	4.10	3.30
query30	0.28	0.13	0.10
query31	2.77	0.54	0.32
query32	3.22	0.60	0.50
query33	3.28	3.18	3.27
query34	15.36	4.02	3.42
query35	3.39	3.37	3.35
query36	0.67	0.58	0.50
query37	0.09	0.08	0.08
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.14	0.15
query41	0.08	0.03	0.03
query42	0.03	0.03	0.03
query43	0.04	0.04	0.04
Total cold run time: 97.21 s
Total hot run time: 24.59 s

@HappenLee HappenLee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit bef288e685e5d940a92a9ef7a1877a67ea16190a.

The supported scope covers full-partition reductions, NTILE, and percent_rank/cume_dist with sliced replay. Existing discussions retain the P2 edge cases around replay-cut admission, coalesced-write admission, and initial replay scheduling for follow-up.

Validation: GitHub currently reports successful compile, BE UT, FE UT, and P0 regression checks. No additional local build or Doris runtime test was run during this review.

@mrhhsg
mrhhsg merged commit af9dad5 into apache:master Oct 27, 2026
37 checks passed
@mrhhsg
mrhhsg deleted the window_spill branch October 27, 2026 06:31
HappenLee pushed a commit that referenced this pull request Oct 29, 2026
…dows (#69686)

### What problem does this PR solve?

Issue Number: None

Related PR: #68500

Problem Summary: The non-spill analytic sink expanded each NTILE literal
bucket argument to every input row and appended it to a buffered
argument column. NTILE reads only the first value, so the extra
materialization and storage grew with the number of retained rows. Store
one bucket value per NTILE function from the first nonempty input Block
and retain it when old input rows are pruned. Other window functions
continue to use their row-wise argument columns. Query results are
unchanged.

### Release note

None

### Check List (For Author)

- Test:
- Unit Test: ASAN `AnalyticSinkOperatorTest.*` (65 tests), including the
new multi-Block, multi-partition pruning case.
- Regression test: `spill_p0/window_function_spill` and
`query_p0/sql_functions/window_functions/test_ntile_function` (one suite
each, zero failures).
- Build and static checks: ASAN BE/FE build (including FE Checkstyle),
clang-format, check-format, build hygiene, clang-tidy on changed lines,
and `git diff --check`.
- Behavior changed: No (SQL results are unchanged; NTILE argument
storage in the non-spill path is reduced).
- Does this need documentation: No
csun5285 added a commit to csun5285/doris that referenced this pull request Nov 3, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69658, apache#68500, apache#61325, apache#65553

Problem Summary: Release branches have been allocating thrift ids and enum
values on their own, and those ids later collide with master. Comparing
branch-4.0/4.1/4.2/master shows, for example:
- TQueryOptions 237 is paimon_blob_max_size on branch-4.1 (apache#69658, merged
  directly to branch-4.1) and spill_analytic_sink_mem_limit_bytes on master
  (apache#68500); both are i64, so a mixed cluster silently reads one as the other.
- TRLTaskTxnCommitAttachment 12 is firstErrorMsg on 4.0/4.1/4.2 but
  kinesisRLTaskProgress on master, because the backport of apache#65553 renumbered
  firstErrorMsg into the gap left by the unpicked apache#61325.
- TSchemaTableType values 62-72 differ between 4.0, 4.1/4.2 and master.

The existing rule only says an id must mean the same thing everywhere; it
does not say where an id is allocated, so each branch picks the next free
number. This adds the rule to gensrc/AGENTS.md (summarized in the root
AGENTS.md and the code-review skill): a new id or enum value is added on
master first, a release-only feature still reserves its id on master, picks
keep master's numbers, enum values are written explicitly, and a
release-branch PR that adds an id master does not have is not mergeable.

### Release note

None

### Check List (For Author)

- Test: No need to test (documentation-only change)
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants