HappenLee commented on code in PR #68500:
URL: https://github.com/apache/doris/pull/68500#discussion_r4358544108


##########
be/src/exec/operator/analytic_source_operator.cpp:
##########
@@ -17,27 +17,520 @@
 
 #include "exec/operator/analytic_source_operator.h"
 
+#include <algorithm>
 #include <cstddef>
+#include <cstdint>
+#include <ranges>
 #include <string>
+#include <utility>
 
+#include "common/config.h"
 #include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
 #include "exec/operator/operator.h"
+#include "exec/spill/spill_file.h"
+#include "exec/spill/spill_file_reader.h"
 #include "exprs/vectorized_agg_fn.h"
 
 namespace doris {
 
 AnalyticLocalState::AnalyticLocalState(RuntimeState* state, OperatorXBase* 
parent)
-        : PipelineXLocalState<AnalyticSharedState>(state, parent) {}
+        : Base(state, parent) {}
 
 Status AnalyticLocalState::init(RuntimeState* state, LocalStateInfo& info) {
-    RETURN_IF_ERROR(PipelineXLocalState<AnalyticSharedState>::init(state, 
info));
+    RETURN_IF_ERROR(Base::init(state, info));
     SCOPED_TIMER(exec_time_counter());
     SCOPED_TIMER(_init_timer);
     _get_next_timer = ADD_TIMER(custom_profile(), "GetNextTime");
     _filtered_rows_counter = ADD_COUNTER(custom_profile(), "FilteredRows", 
TUnit::UNIT);
+    _partition_replay_timer = ADD_TIMER(custom_profile(), 
"PartitionReplayTime");
     return Status::OK();
 }
 
+Status AnalyticLocalState::close(RuntimeState* state) {
+    if (_closed) {
+        return Status::OK();
+    }
+    _finish_spill_batch();
+    return Base::close(state);
+}
+
+void AnalyticLocalState::_finish_spill_batch() {
+    if (_batch_reader) {
+        auto st = _batch_reader->close();
+        LOG_IF(WARNING, !st.ok()) << "close analytic spill batch reader 
failed: " << st;
+        _batch_reader.reset();
+    }
+    if (_peer_group_reader) {
+        auto st = _peer_group_reader->close();
+        LOG_IF(WARNING, !st.ok()) << "close analytic spill peer group reader 
failed: " << st;
+        _peer_group_reader.reset();
+    }
+    COUNTER_UPDATE(_memory_used_counter, -_in_memory_batch_bytes);
+    _in_memory_batch_bytes = 0;
+    _current_batch.reset();
+    _replay_block.clear();
+    _replay_block_position = 0;
+    _peer_group_block.clear();
+}
+
+Status AnalyticLocalState::_open_spill_batch(RuntimeState* state,
+                                             
std::shared_ptr<AnalyticSpillBatch> batch) {
+    DCHECK(batch != nullptr);
+    DCHECK_GT(batch->rows, 0);
+    DORIS_CHECK(!batch->partition_ends.empty());
+    DORIS_CHECK_EQ(batch->partition_ends.back(), batch->rows);
+    DORIS_CHECK_EQ(batch->strategies.size(), batch->result_types.size());
+    DORIS_CHECK_EQ(batch->strategies.size(), batch->peer_functions.size());
+    DORIS_CHECK_EQ(batch->strategies.size(), batch->partition_results.size());
+    DORIS_CHECK_EQ(batch->strategies.size(), 
batch->function_parameters.size());
+    DORIS_CHECK_EQ(batch->strategies.size(), 
batch->change_to_nullable_flags.size());
+    _current_batch = std::move(batch);
+    _in_memory_block_index = 0;
+    _batch_output_position = 0;
+    _partition_index = 0;
+    _partition_start = 0;
+    _partition_end = _current_batch->partition_ends[0];
+    _peer_group_start = 0;
+    _peer_group_end = 0;
+    _peer_group_block_position = 0;
+    _in_memory_peer_group_index = 0;
+    _peer_group_file_eos = false;
+    for (const auto& block : _current_batch->blocks) {
+        _in_memory_batch_bytes += block.allocated_bytes();
+    }
+    COUNTER_UPDATE(_memory_used_counter, _in_memory_batch_bytes);
+
+    if (_current_batch->data_file) {
+        _batch_reader = _current_batch->data_file->create_reader(state, 
operator_profile());
+        RETURN_IF_ERROR(_batch_reader->open());
+    }
+    if (_current_batch->peer_group_file) {
+        _peer_group_reader =
+                _current_batch->peer_group_file->create_reader(state, 
operator_profile());
+        RETURN_IF_ERROR(_peer_group_reader->open());
+    }
+
+    _has_peer_group_function =
+            std::ranges::any_of(_current_batch->strategies, 
[](WindowSpillStrategy strategy) {
+                return strategy == WindowSpillStrategy::PEER_GROUP;
+            });
+    if (_has_peer_group_function) {
+        RETURN_IF_ERROR(_next_peer_group_end(state));
+    }
+    return Status::OK();
+}
+
+Status AnalyticLocalState::_read_batch_block(RuntimeState* state, Block* 
block, bool* batch_eos) {
+    RETURN_IF_CANCELLED(state);
+    if (_batch_reader) {
+        return _batch_reader->read(block, batch_eos);
+    }
+    if (_in_memory_block_index >= _current_batch->blocks.size()) {
+        *batch_eos = true;
+        block->clear();
+        return Status::OK();
+    }
+    auto& next_block = _current_batch->blocks[_in_memory_block_index++];
+    const auto block_bytes = 
static_cast<int64_t>(next_block.allocated_bytes());
+    COUNTER_UPDATE(_memory_used_counter, -block_bytes);
+    _in_memory_batch_bytes -= block_bytes;
+    block->swap(std::move(next_block));
+    *batch_eos = false;
+    return Status::OK();
+}
+
+Status AnalyticLocalState::_next_replay_rows(RuntimeState* state, Block* 
block, bool* batch_eos) {
+    while (_replay_block_position >= _replay_block.rows()) {
+        _replay_block.clear();
+        _replay_block_position = 0;
+        RETURN_IF_ERROR(_read_batch_block(state, &_replay_block, batch_eos));
+        if (*batch_eos) {
+            return Status::OK();
+        }
+    }
+    *batch_eos = false;
+    // Spilled Blocks are coalesced up to the spill buffer size, so a replayed 
Block can be much
+    // larger than the batch size expected by downstream operators.
+    DCHECK_GT(state->batch_size(), 0);
+    const auto batch_size = static_cast<size_t>(state->batch_size());
+    const size_t rows = std::min(batch_size, _replay_block.rows() - 
_replay_block_position);
+    if (_replay_block_position == 0 && rows == _replay_block.rows()) {
+        block->swap(_replay_block);
+        _replay_block.clear();
+        return Status::OK();
+    }
+    Block slice;
+    for (const auto& column : _replay_block) {
+        slice.insert({column.column->cut(_replay_block_position, rows), 
column.type, column.name});
+    }
+    block->swap(slice);
+    _replay_block_position += rows;
+    return Status::OK();
+}
+
+size_t AnalyticLocalState::_spill_replay_reserve_bytes(RuntimeState* state) 
const {
+    if (!_shared_state->spill_enabled.load()) {
+        return 0;
+    }
+    const auto read_bytes = [state](AnalyticSpillBatch::RecordMemory memory,
+                                    const SpillFile& file) {
+        // The reader retains both a serialized window and PBlock; 
deserialization also owns
+        // the decoded Block and may temporarily allocate another copy of its 
columns. Local
+        // reads do not coalesce: a large configured spill buffer must not 
block a small file.
+        // The footer offsets also outlive the serialized read window. Remote 
reads may fetch
+        // a whole small part or coalesce adjacent records up to the 
configured read window.
+        const size_t coalesce_bytes =
+                file.is_remote() ? static_cast<size_t>(std::max<int64_t>(
+                                           0, 
std::min(config::spill_s3_read_coalesce_bytes,
+                                                       
state->spill_buffer_size_bytes())))
+                                 : 0;
+        return std::max({memory.serialized_bytes, memory.footer_bytes, 
coalesce_bytes}) +
+               memory.footer_bytes + memory.serialized_bytes + 2 * 
memory.decoded_bytes;
+    };
+    if (!_current_batch || _batch_output_position >= _current_batch->rows) {
+        // The next call opens the batch at the head of the queue: it opens 
the readers of its
+        // spilled files, reads the first peer group record and then the first 
data record.
+        LockGuard lock(_shared_state->buffer_mutex);
+        if (_replay_admission_checked && !_admitted_batch) {
+            return 0;
+        }
+        if (_shared_state->spill_batches.empty() && !_admitted_batch) {
+            return 0;
+        }
+        const auto& next_batch =
+                _admitted_batch ? _admitted_batch : 
_shared_state->spill_batches.front();
+        size_t reserve_bytes = 0;
+        if (next_batch->data_file) {
+            reserve_bytes += read_bytes(next_batch->data_record_memory, 
*next_batch->data_file);
+        }
+        if (next_batch->peer_group_file) {
+            reserve_bytes +=
+                    read_bytes(next_batch->peer_record_memory, 
*next_batch->peer_group_file);
+        }
+        return reserve_bytes;
+    }
+    size_t reserve_bytes = 0;
+    if (_batch_reader && _replay_block_position >= _replay_block.rows()) {
+        reserve_bytes += read_bytes(_current_batch->data_record_memory, 
*_current_batch->data_file);
+    }
+    if (_next_slice_reads_peer_group_record(state)) {
+        reserve_bytes +=
+                read_bytes(_current_batch->peer_record_memory, 
*_current_batch->peer_group_file);
+    }
+    return reserve_bytes;
+}
+
+size_t AnalyticLocalState::_next_output_reserve_bytes(RuntimeState* state) 
const {
+    const AnalyticSpillBatch* batch = _current_batch.get();
+    if (!batch || _batch_output_position >= batch->rows) {
+        LockGuard lock(_shared_state->buffer_mutex);
+        if (_replay_admission_checked && !_admitted_batch) {
+            return 0;
+        }
+        if (!_admitted_batch && _shared_state->spill_batches.empty()) {
+            return 0;
+        }
+        batch = (_admitted_batch ? _admitted_batch : 
_shared_state->spill_batches.front()).get();
+    }
+    size_t rows = static_cast<size_t>(std::min<int64_t>(state->batch_size(), 
batch->rows));
+    const Block* cut_block = nullptr;
+    size_t cut_start = 0;
+    if (batch == _current_batch.get()) {
+        rows = static_cast<size_t>(std::min<int64_t>(rows, batch->rows - 
_batch_output_position));
+        if (_replay_block_position < _replay_block.rows()) {
+            cut_block = &_replay_block;
+            cut_start = _replay_block_position;
+        } else if (!batch->data_file && _in_memory_block_index < 
batch->blocks.size()) {
+            // The next in-memory Block is moved into _replay_block in this 
call. Its first
+            // slice must be admitted too; a previous sort may supply a Block 
> batch_size.
+            cut_block = &batch->blocks[_in_memory_block_index];
+        }
+    } else if (!batch->blocks.empty()) {
+        cut_block = &batch->blocks.front();
+    }
+    if (cut_block) {
+        rows = std::min(rows, cut_block->rows() - cut_start);
+    }
+    size_t bytes = 0;
+    if (cut_block && (cut_start != 0 || rows != cut_block->rows())) {
+        // A retained Block is cut into the next output slice. Count the 
actual rows rather
+        // than carrying the previous whole-record read peak into every small 
slice.
+        for (const auto& column : *cut_block) {
+            for (size_t row = cut_start; row < cut_start + rows; ++row) {
+                bytes += 2 * column.column->serialize_size_at(row);
+            }
+        }
+    }
+    for (size_t i = 0; i < batch->result_types.size(); ++i) {
+        const auto& type = batch->result_types[i];
+        if (type->have_maximum_size_of_value()) {
+            bytes += rows * type->get_size_of_value_in_memory();
+        } else if (batch->partition_results[i]) {
+            // One partition result may be repeated for every output row.
+            bytes += rows * batch->partition_results[i]->byte_size();
+        }
+        if (batch->change_to_nullable_flags[i]) {
+            bytes += rows;
+        }
+    }
+    return bytes;
+}
+
+bool AnalyticLocalState::_next_slice_reads_peer_group_record(RuntimeState* 
state) const {
+    if (!_peer_group_reader) {
+        return false;
+    }
+    // The next call outputs at most batch_size rows starting at 
_batch_output_position and needs
+    // a new peer group record once one of them is not covered by the last 
group end read so far.
+    DCHECK_GT(_peer_group_block.rows(), 0);
+    const auto& column =
+            assert_cast<const 
ColumnInt64&>(*_peer_group_block.get_by_position(0).column);
+    const int64_t last_peer_group_end = column.get_data().back();
+    size_t rows = static_cast<size_t>(state->batch_size());
+    // A retained data Block ends this call even when batch_size extends into 
the next Block.
+    // For example, 2 remaining rows and batch_size=8 cannot read a peer 
record for row 3.
+    if (_replay_block_position < _replay_block.rows()) {
+        rows = std::min(rows, _replay_block.rows() - _replay_block_position);
+    }
+    const int64_t slice_end =
+            std::min<int64_t>(_batch_output_position + rows, 
_current_batch->rows);
+    return last_peer_group_end < slice_end;
+}
+
+Status AnalyticLocalState::_next_peer_group_end(RuntimeState* state) {
+    RETURN_IF_CANCELLED(state);
+    _peer_group_start = _peer_group_end;
+    if (!_peer_group_reader) {
+        DORIS_CHECK_LT(_in_memory_peer_group_index, 
_current_batch->peer_group_ends.size());
+        _peer_group_end = 
_current_batch->peer_group_ends[_in_memory_peer_group_index++];
+    } else {
+        while (_peer_group_block_position >= _peer_group_block.rows()) {
+            _peer_group_block.clear();
+            RETURN_IF_ERROR(_peer_group_reader->read(&_peer_group_block, 
&_peer_group_file_eos));
+            _peer_group_block_position = 0;
+            DORIS_CHECK(!_peer_group_file_eos || !_peer_group_block.empty());
+        }
+        const auto& column =
+                assert_cast<const 
ColumnInt64&>(*_peer_group_block.get_by_position(0).column);
+        _peer_group_end = column.get_data()[_peer_group_block_position++];
+    }
+    DORIS_CHECK_GT(_peer_group_end, _peer_group_start);
+    DORIS_CHECK_LE(_peer_group_end, _current_batch->rows);
+    return Status::OK();
+}
+
+void AnalyticLocalState::_next_spill_partition() {
+    ++_partition_index;
+    DORIS_CHECK_LT(_partition_index, _current_batch->partition_ends.size());
+    _partition_start = _partition_end;
+    _partition_end = _current_batch->partition_ends[_partition_index];
+    DORIS_CHECK_GT(_partition_end, _partition_start);
+}
+
+Status AnalyticLocalState::_append_spill_results(RuntimeState* state, Block* 
block) {
+    const auto rows = block->rows();
+    DCHECK_GT(rows, 0);
+    DCHECK_LE(_batch_output_position + static_cast<int64_t>(rows), 
_current_batch->rows);
+    const auto function_count = _current_batch->strategies.size();
+    std::vector<MutableColumnPtr> results(function_count);
+    std::vector<IColumn*> computed_result_columns(function_count, nullptr);
+    _create_spill_result_columns(rows, results, computed_result_columns);
+
+    size_t offset = 0;
+    while (offset < rows) {
+        while (_batch_output_position >= _partition_end) {
+            _next_spill_partition();
+        }
+        const size_t segment_rows =
+                std::min<size_t>(rows - offset, _partition_end - 
_batch_output_position);
+        _append_partition_results(segment_rows, computed_result_columns, 
results);
+        if (_has_peer_group_function) {
+            RETURN_IF_ERROR(
+                    _append_peer_group_results(state, segment_rows, 
computed_result_columns));
+        }
+        _batch_output_position += segment_rows;
+        offset += segment_rows;
+    }
+    _insert_spill_result_columns(block, results);
+    return Status::OK();
+}
+
+void AnalyticLocalState::_create_spill_result_columns(
+        size_t rows, std::vector<MutableColumnPtr>& results,
+        std::vector<IColumn*>& computed_result_columns) const {
+    const auto function_count = _current_batch->strategies.size();
+    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 ||
+            _current_batch->strategies[i] == WindowSpillStrategy::PEER_GROUP) {
+            if (_current_batch->result_types[i]->is_nullable()) {
+                auto* nullable = 
assert_cast<ColumnNullable*>(results[i].get());
+                nullable->get_null_map_data().resize_fill(rows, 0);
+                computed_result_columns[i] = &nullable->get_nested_column();
+            } else {
+                computed_result_columns[i] = results[i].get();
+            }
+        }
+    }
+}
+
+void AnalyticLocalState::_append_partition_results(
+        size_t rows, const std::vector<IColumn*>& computed_result_columns,
+        std::vector<MutableColumnPtr>& results) const {
+    const auto function_count = _current_batch->strategies.size();
+    for (size_t i = 0; i < function_count; ++i) {
+        switch (_current_batch->strategies[i]) {
+        case WindowSpillStrategy::PARTITION_REDUCE:
+            DCHECK(_current_batch->partition_results[i]);
+            
results[i]->insert_many_from(*_current_batch->partition_results[i], 
_partition_index,
+                                         rows);
+            break;
+        case WindowSpillStrategy::PARTITION_CARDINALITY:
+            _append_ntile_result(i, rows, computed_result_columns[i]);
+            break;
+        case WindowSpillStrategy::PEER_GROUP:
+            break;
+        case WindowSpillStrategy::UNSUPPORTED:
+            DORIS_CHECK(false);
+            break;
+        }
+    }
+}
+
+void AnalyticLocalState::_append_ntile_result(size_t function_index, size_t 
rows,
+                                              IColumn* result_column) const {
+    const int64_t bucket_num =
+            
_current_batch->function_parameters[function_index][_partition_index];
+    DORIS_CHECK_GT(bucket_num, 0);
+    const int64_t partition_size = _partition_end - _partition_start;
+    const int64_t small_bucket_size = partition_size / bucket_num;
+    const int64_t big_bucket_num = partition_size % bucket_num;
+    const int64_t first_small_bucket_row = big_bucket_num * (small_bucket_size 
+ 1);
+    const int64_t first_row_index = _batch_output_position - _partition_start;
+    auto& data = assert_cast<ColumnInt64&>(*result_column).get_data();
+    for (size_t row = 0; row < rows; ++row) {
+        const int64_t row_index = first_row_index + row;
+        const int64_t bucket =
+                row_index >= first_small_bucket_row
+                        ? big_bucket_num + 1 +
+                                  (row_index - first_small_bucket_row) / 
small_bucket_size
+                        : row_index / (small_bucket_size + 1) + 1;
+        data.push_back(bucket);
+    }
+}
+
+Status AnalyticLocalState::_append_peer_group_results(
+        RuntimeState* state, size_t rows, const std::vector<IColumn*>& 
computed_result_columns) {
+    const auto function_count = _current_batch->strategies.size();
+    for (size_t row = 0; row < rows; ++row) {
+        if ((row & 4095) == 0) {
+            RETURN_IF_CANCELLED(state);
+        }
+        const int64_t batch_row = _batch_output_position + row;
+        while (batch_row >= _peer_group_end) {
+            RETURN_IF_ERROR(_next_peer_group_end(state));
+        }
+        DCHECK_GE(_peer_group_start, _partition_start);
+        DCHECK_LE(_peer_group_end, _partition_end);
+        for (size_t i = 0; i < function_count; ++i) {
+            if (_current_batch->strategies[i] != 
WindowSpillStrategy::PEER_GROUP) {
+                continue;
+            }
+            assert_cast<ColumnFloat64&>(*computed_result_columns[i])
+                    .get_data()
+                    .push_back(_current_peer_group_result(i));

Review Comment:
   **[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:
   
   ```cpp
   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: 
https://github.com/apache/doris/blob/46e0bd813888309a7faaba3544fe2a98959063f4/be/src/exprs/aggregate/aggregate_function_window.h#L256



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to