[feature](be) Add cache-aware file range planning

### What problem does this PR solve?

Issue Number: #67611 (tracking)

Related PR: #67292, #67579, #67612

Problem Summary: Add cache-aware range planning and the shared read-ahead statistics contract. Coalesce sorted requested ranges, optionally complete high-coverage boundary blocks, and map exact inputs back to final buffers. Keep planning timers for column initialization, exact-row batches, physical-page advancement, window discard and extension in the shared contract; collection remains with the later consumers.

### Release note

Internal range planning and shared read-ahead Profile/bvar definitions.

### Check List (For Author)

- Test: Unit Test — component coverage is included with this change. The integrated source snapshot d5c427fe337 passed 198 ASAN tests from 11 suites via run-be-ut.sh -j100. This individual intermediate commit was not built separately; target-branch validation is recorded on the PR. Repository clang-format v16, build hygiene and diff checks passed on the integrated tree. No cluster or clang-tidy run.
- Behavior changed: Yes — introduces the opt-in component; this synchronization preserves existing defaults and IO/writeback semantics.
- Does this need documentation: Yes — component scope and current behavior are tracked in #67611 and the related PR descriptions.
This commit is contained in:
bobhan1 committed 2026-10-29 16:27:14 +08:00
1 parent 7654cf77cd
commit 90cf5b6ce6
6 files changed
+905

No files matched your search

+258
View File
@@ -0,0 +1,258 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "io/cache/file_range_planner.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <utility>
#include "common/logging.h"
#include "io/fs/read_ahead_metrics.h"
namespace doris::io {
Status FileRangePlanOptions::validate() const {
RETURN_IF_ERROR(coalesce_options.validate());
if (cache_block_size == 0) {
return Status::InvalidArgument("file range plan cache block size must be positive");
}
if (!std::isfinite(block_fill_min_coverage) || block_fill_min_coverage <= 0.0 ||
block_fill_min_coverage > 1.0) {
return Status::InvalidArgument(
"file range plan block fill coverage must be finite and in (0, 1]");
}
return Status::OK();
}
namespace {
struct BlockCandidate {
FileRange range;
size_t missing_base_bytes {0};
};
Status validate_input_ranges(const std::vector<FileRange>& input_ranges, size_t file_size) {
size_t previous_end = 0;
for (const auto& range : input_ranges) {
if (range.size == 0) {
return Status::InvalidArgument("input file range at offset {} is empty", range.offset);
}
if (range.offset > file_size || range.size > file_size - range.offset) {
return Status::InvalidArgument(
"input file range at offset {} with size {} exceeds file size {}", range.offset,
range.size, file_size);
}
if (range.offset < previous_end) {
return Status::InvalidArgument(
"input file ranges must be sorted and disjoint: offset {} follows end {}",
range.offset, previous_end);
}
previous_end = range.end();
}
return Status::OK();
}
FileRange block_range(size_t offset, size_t file_size, size_t block_size) {
const size_t block_offset = offset / block_size * block_size;
return {.offset = block_offset, .size = std::min(block_size, file_size - block_offset)};
}
size_t intersection_bytes(const FileRange& left, const FileRange& right) {
const size_t begin = std::max(left.offset, right.offset);
const size_t end = std::min(left.end(), right.end());
return begin < end ? end - begin : 0;
}
// Count coverage in `target` with a monotonic cursor over sorted, disjoint ranges. The caller must
// visit targets in ascending order.
size_t covered_bytes(const std::vector<FileRange>& ranges, const FileRange& target,
size_t* cursor) {
while (*cursor < ranges.size() && ranges[*cursor].end() <= target.offset) {
++*cursor;
}
size_t result = 0;
for (size_t index = *cursor; index < ranges.size() && ranges[index].offset < target.end();
++index) {
result += intersection_bytes(ranges[index], target);
}
return result;
}
void append_boundary_block(std::vector<FileRange>* blocks, const FileRange& range, size_t file_size,
size_t block_size) {
const FileRange block = block_range(range.offset, file_size, block_size);
if (!blocks->empty() && blocks->back().offset == block.offset) {
DORIS_CHECK(blocks->back() == block);
return;
}
DORIS_CHECK(blocks->empty() || blocks->back().end() <= block.offset);
blocks->push_back(block);
}
std::vector<BlockCandidate> find_block_candidates(const std::vector<FileRange>& input_ranges,
const std::vector<FileRange>& base_ranges,
size_t file_size,
const FileRangePlanOptions& options) {
// Interior blocks are already covered by a base read, so only its two boundary blocks can add
// bytes. This bounds the candidate count to twice the number of base ranges.
std::vector<FileRange> boundary_blocks;
boundary_blocks.reserve(base_ranges.size() * 2);
for (const auto& base : base_ranges) {
append_boundary_block(&boundary_blocks, base, file_size, options.cache_block_size);
append_boundary_block(&boundary_blocks, {.offset = base.end() - 1, .size = 1}, file_size,
options.cache_block_size);
}
std::vector<BlockCandidate> candidates;
candidates.reserve(boundary_blocks.size());
size_t input_cursor = 0;
size_t base_cursor = 0;
for (const auto& block : boundary_blocks) {
const size_t input_bytes = covered_bytes(input_ranges, block, &input_cursor);
const double coverage = static_cast<double>(input_bytes) / static_cast<double>(block.size);
if (coverage < options.block_fill_min_coverage) {
continue;
}
const size_t base_bytes = covered_bytes(base_ranges, block, &base_cursor);
DORIS_CHECK(base_bytes <= block.size);
if (base_bytes < block.size) {
candidates.push_back({.range = block, .missing_base_bytes = block.size - base_bytes});
}
}
std::ranges::sort(candidates, [](const BlockCandidate& left, const BlockCandidate& right) {
return std::pair {left.missing_base_bytes, left.range.offset} <
std::pair {right.missing_base_bytes, right.range.offset};
});
return candidates;
}
// Treat each base range as one component, then try boundary-block fills from the fewest added bytes
// to the most. An accepted fill joins only the components it overlaps and still obeys the physical
// range-size limit.
std::vector<FileRange> choose_components(const std::vector<FileRange>& input_ranges,
const std::vector<FileRange>& base_ranges,
size_t file_size, const FileRangePlanOptions& options) {
std::map<size_t, FileRange> components;
for (const auto& base : base_ranges) {
components.emplace(base.offset, base);
}
// Try the cheapest fills first. The interval map updates only components touched by a
// candidate instead of rebuilding every component after each attempt.
const auto candidates = find_block_candidates(input_ranges, base_ranges, file_size, options);
for (const auto& candidate : candidates) {
auto first = components.lower_bound(candidate.range.offset);
if (first != components.begin()) {
auto previous = first;
--previous;
if (previous->second.end() > candidate.range.offset) {
first = previous;
}
}
DORIS_CHECK(first != components.end());
DORIS_CHECK(first->second.offset < candidate.range.end());
size_t merged_offset = std::min(candidate.range.offset, first->second.offset);
size_t merged_end = candidate.range.end();
auto last = first;
while (last != components.end() && last->second.offset < candidate.range.end()) {
DORIS_CHECK(last->second.end() > candidate.range.offset);
merged_end = std::max(merged_end, last->second.end());
++last;
}
if (merged_end - merged_offset > options.coalesce_options.max_range_bytes) {
continue;
}
components.erase(first, last);
components.emplace(merged_offset,
FileRange {.offset = merged_offset, .size = merged_end - merged_offset});
}
std::vector<FileRange> result;
result.reserve(components.size());
for (const auto& [offset, component] : components) {
DORIS_CHECK(offset == component.offset);
result.push_back(component);
}
return result;
}
// Map every original page range back to the containing physical read with one monotonic scan.
FileRangePlan build_final_plan(const std::vector<FileRange>& input_ranges,
std::vector<FileRange> components) {
FileRangePlan result {.ranges = std::move(components), .input_locations = {}};
result.input_locations.reserve(input_ranges.size());
size_t range_index = 0;
for (const auto& input : input_ranges) {
while (range_index < result.ranges.size() &&
result.ranges[range_index].end() <= input.offset) {
++range_index;
}
DORIS_CHECK(range_index < result.ranges.size());
const auto& range = result.ranges[range_index];
DORIS_CHECK(range.offset <= input.offset);
DORIS_CHECK(input.end() <= range.end());
result.input_locations.push_back(
{.range_index = range_index, .buffer_offset = input.offset - range.offset});
}
return result;
}
} // namespace
Status FileRangePlanner::plan(const std::vector<FileRange>& input_ranges, size_t file_size,
const FileRangePlanOptions& options, FileRangePlan* const output_plan,
ReadAheadStatistics* statistics) {
DORIS_CHECK(output_plan != nullptr);
RETURN_IF_ERROR(options.validate());
RETURN_IF_ERROR(validate_input_ranges(input_ranges, file_size));
const auto base_ranges = FileRangeCoalescer::coalesce(input_ranges, options.coalesce_options);
auto components = choose_components(input_ranges, base_ranges, file_size, options);
*output_plan = build_final_plan(input_ranges, std::move(components));
if (statistics != nullptr) {
size_t input_bytes = 0;
size_t coalesced_bytes = 0;
size_t planned_bytes = 0;
for (const auto& range : input_ranges) {
input_bytes += range.size;
}
for (const auto& range : base_ranges) {
coalesced_bytes += range.size;
}
for (const auto& range : output_plan->ranges) {
planned_bytes += range.size;
}
COUNTER_UPDATE(&statistics->input_pages, static_cast<int64_t>(input_ranges.size()));
COUNTER_UPDATE(&statistics->input_bytes, static_cast<int64_t>(input_bytes));
COUNTER_UPDATE(&statistics->coalesced_ranges, static_cast<int64_t>(base_ranges.size()));
COUNTER_UPDATE(&statistics->coalesced_bytes, static_cast<int64_t>(coalesced_bytes));
COUNTER_UPDATE(&statistics->block_fill_bytes,
static_cast<int64_t>(planned_bytes - coalesced_bytes));
COUNTER_UPDATE(&statistics->planned_ranges,
static_cast<int64_t>(output_plan->ranges.size()));
COUNTER_UPDATE(&statistics->planned_bytes, static_cast<int64_t>(planned_bytes));
}
return Status::OK();
}
} // namespace doris::io
+72
View File
@@ -0,0 +1,72 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#pragma once
#include <cstddef>
#include <vector>
#include "common/status.h"
#include "io/fs/file_range_coalescer.h"
namespace doris::io {
struct ReadAheadStatistics;
struct FileRangePlanOptions {
/// Limits for the initial range coalescing pass.
FileRangeCoalesceOptions coalesce_options;
/// File Cache block size used when considering boundary-block completion.
size_t cache_block_size {1};
/// Minimum input-byte coverage required before completing a boundary block.
double block_fill_min_coverage {1.0};
Status validate() const;
};
/// The location of one input range in its final read buffer.
struct FileRangeLocation {
/// Index into FileRangePlan::ranges.
size_t range_index {0};
/// Offset of the original input's first byte within that physical range buffer.
size_t buffer_offset {0};
bool operator==(const FileRangeLocation&) const = default;
};
struct FileRangePlan {
/// Physical ranges to read after coalescing and optional cache-block completion.
std::vector<FileRange> ranges;
/// One location per input in caller-supplied order; every input is contained by that range.
std::vector<FileRangeLocation> input_locations;
bool operator==(const FileRangePlan&) const = default;
};
/// Builds a read-only plan without allocating buffers or performing file IO.
class FileRangePlanner {
public:
/// `input_ranges` must be sorted by offset and mutually disjoint. The planner first coalesces
/// them, then completes high-coverage cache blocks when the resulting physical ranges remain
/// within `max_range_bytes`. One input larger than that limit remains a standalone read. The
/// physical EOF block uses its valid file bytes as the coverage denominator.
static Status plan(const std::vector<FileRange>& input_ranges, size_t file_size,
const FileRangePlanOptions& options, FileRangePlan* output_plan,
ReadAheadStatistics* statistics = nullptr);
};
} // namespace doris::io
+97
View File
@@ -0,0 +1,97 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "io/fs/read_ahead_metrics.h"
namespace doris::io {
void ReadAheadStatistics::update_profile(RuntimeProfile* profile) {
DORIS_CHECK(profile != nullptr);
ADD_LABEL_COUNTER(profile, "PageReadAhead");
for (const char* group :
{"ReadAheadPlanning", "ReadAheadIO", "ReadAheadConsume", "ReadAheadWriteback"}) {
ADD_CHILD_COUNTER(profile, group, TUnit::NONE, "PageReadAhead");
}
const auto update = [&](const char* name, const char* parent,
RuntimeProfile::Counter& counter) {
const int64_t delta = counter.value();
COUNTER_UPDATE(ADD_CHILD_COUNTER(profile, name, counter.type(), parent), delta);
// Only the reporting thread drains counters. Subtract this snapshot so concurrent
// increments remain available for the next report.
COUNTER_UPDATE(&counter, -delta);
};
update("ReadAheadPlanCalls", "ReadAheadPlanning", plan_calls);
update("ReadAheadPlanTime", "ReadAheadPlanning", plan_time);
update("ReadAheadColumnPlanTime", "ReadAheadPlanning", column_plan_time);
update("ReadAheadColumnInitTime", "ReadAheadColumnPlanTime", column_init_time);
update("ReadAheadPageAdvanceTime", "ReadAheadColumnPlanTime", page_advance_time);
update("ReadAheadWindowDiscardTime", "ReadAheadColumnPlanTime", window_discard_time);
update("ReadAheadCurrentBatchPlanTime", "ReadAheadColumnPlanTime", current_batch_plan_time);
update("ReadAheadWindowExtendTime", "ReadAheadColumnPlanTime", window_extend_time);
update("ReadAheadCandidatePages", "ReadAheadPlanning", candidate_pages);
update("ReadAheadCandidateBytes", "ReadAheadPlanning", candidate_bytes);
update("ReadAheadPageCacheHits", "ReadAheadPlanning", page_cache_hits);
update("ReadAheadPageCacheHitBytes", "ReadAheadPlanning", page_cache_hit_bytes);
update("ReadAheadInputPages", "ReadAheadPlanning", input_pages);
update("ReadAheadInputBytes", "ReadAheadPlanning", input_bytes);
update("ReadAheadCoalescedRanges", "ReadAheadPlanning", coalesced_ranges);
update("ReadAheadCoalescedBytes", "ReadAheadPlanning", coalesced_bytes);
update("ReadAheadBlockFillBytes", "ReadAheadPlanning", block_fill_bytes);
update("ReadAheadPlannedRanges", "ReadAheadPlanning", planned_ranges);
update("ReadAheadPlannedBytes", "ReadAheadPlanning", planned_bytes);
update("ReadAheadSubmittedRanges", "ReadAheadIO", submitted_ranges);
update("ReadAheadSubmittedBytes", "ReadAheadIO", submitted_bytes);
update("ReadAheadRejectedBatches", "ReadAheadIO", rejected_batches);
update("ReadAheadQueryBudgetRejectedBatches", "ReadAheadIO", query_budget_rejected_batches);
update("ReadAheadBEBudgetRejectedBatches", "ReadAheadIO", be_budget_rejected_batches);
update("ReadAheadSubmitFailedRanges", "ReadAheadIO", submit_failed_ranges);
update("ReadAheadCompletedRanges", "ReadAheadIO", completed_ranges);
update("ReadAheadFailedRanges", "ReadAheadIO", failed_ranges);
update("ReadAheadCancelledRanges", "ReadAheadIO", cancelled_ranges);
update("ReadAheadQueueWaitTime", "ReadAheadIO", queue_wait_time);
update("ReadAheadReadTime", "ReadAheadIO", read_time);
update("ReadAheadSourceBytes", "ReadAheadIO", source_bytes);
update("ReadAheadSuccessfulSourceBytes", "ReadAheadIO", successful_source_bytes);
update("ReadAheadRemoteBytes", "ReadAheadIO", remote_bytes);
update("ReadAheadLocalBytes", "ReadAheadIO", local_bytes);
update("ReadAheadRemoteRequests", "ReadAheadIO", remote_requests);
update("ReadAheadReadyPageHits", "ReadAheadConsume", ready_page_hits);
update("ReadAheadWaitPageHits", "ReadAheadConsume", wait_page_hits);
update("ReadAheadWaitTime", "ReadAheadConsume", wait_time);
update("ReadAheadFallbackPages", "ReadAheadConsume", fallback_pages);
update("ReadAheadFallbackBytes", "ReadAheadConsume", fallback_bytes);
update("ReadAheadFallbackTime", "ReadAheadConsume", fallback_time);
update("ReadAheadConsumedPageBytes", "ReadAheadConsume", consumed_page_bytes);
update("ReadAheadConsumedRangeBytes", "ReadAheadConsume", consumed_range_bytes);
update("ReadAheadReleasedPages", "ReadAheadConsume", released_pages);
update("ReadAheadWritebackTime", "ReadAheadWriteback", writeback_time);
update("ReadAheadCompleteBlocksSubmitted", "ReadAheadWriteback", complete_blocks_submitted);
update("ReadAheadCompleteBlockBytes", "ReadAheadWriteback", complete_block_bytes);
update("ReadAheadPartialBlocksQueued", "ReadAheadWriteback", partial_blocks_queued);
update("ReadAheadPartialBlocksMerged", "ReadAheadWriteback", partial_blocks_merged);
update("ReadAheadPartialFragmentBytes", "ReadAheadWriteback", partial_fragment_bytes);
update("ReadAheadWritebackDeduplicatedBlocks", "ReadAheadWriteback",
writeback_deduplicated_blocks);
update("ReadAheadWritebackRejectedBlocks", "ReadAheadWriteback", writeback_rejected_blocks);
}
ReadAheadBvars& read_ahead_bvars() {
static ReadAheadBvars metrics;
return metrics;
}
} // namespace doris::io
+124
View File
@@ -0,0 +1,124 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#pragma once
#include <bvar/reducer.h>
#include <cstdint>
#include "runtime/runtime_profile.h"
namespace doris::io {
/// Shared by the reader and its asynchronous range handles. Counters are owned here rather than
/// by the caller's RuntimeProfile, so outstanding tasks can finish after the scanner is destroyed.
struct ReadAheadStatistics {
RuntimeProfile::Counter plan_calls {TUnit::UNIT};
/// Range planning and submission in apply_plans(), after physical-column planning.
RuntimeProfile::Counter plan_time {TUnit::TIME_NS};
/// Physical-column preparation, including initialization, validation and empty plans.
RuntimeProfile::Counter column_plan_time {TUnit::TIME_NS};
RuntimeProfile::Counter column_init_time {TUnit::TIME_NS};
/// Page-index driven window maintenance after scan initialization, excluding apply_plans().
RuntimeProfile::Counter page_advance_time {TUnit::TIME_NS};
/// Window maintenance sub-timers can overlap initialization and page advancement.
RuntimeProfile::Counter window_discard_time {TUnit::TIME_NS};
/// Exact-rowid planning for point reads and dependent nested data, including extensions.
RuntimeProfile::Counter current_batch_plan_time {TUnit::TIME_NS};
/// Shared sub-timer of initialization, page advancement and exact-rowid planning.
RuntimeProfile::Counter window_extend_time {TUnit::TIME_NS};
RuntimeProfile::Counter candidate_pages {TUnit::UNIT};
RuntimeProfile::Counter candidate_bytes {TUnit::BYTES};
RuntimeProfile::Counter page_cache_hits {TUnit::UNIT};
RuntimeProfile::Counter page_cache_hit_bytes {TUnit::BYTES};
RuntimeProfile::Counter input_pages {TUnit::UNIT};
RuntimeProfile::Counter input_bytes {TUnit::BYTES};
RuntimeProfile::Counter coalesced_ranges {TUnit::UNIT};
RuntimeProfile::Counter coalesced_bytes {TUnit::BYTES};
RuntimeProfile::Counter block_fill_bytes {TUnit::BYTES};
RuntimeProfile::Counter planned_ranges {TUnit::UNIT};
RuntimeProfile::Counter planned_bytes {TUnit::BYTES};
RuntimeProfile::Counter submitted_ranges {TUnit::UNIT};
RuntimeProfile::Counter submitted_bytes {TUnit::BYTES};
RuntimeProfile::Counter rejected_batches {TUnit::UNIT};
RuntimeProfile::Counter query_budget_rejected_batches {TUnit::UNIT};
RuntimeProfile::Counter be_budget_rejected_batches {TUnit::UNIT};
/// Executor rejection, including the rejected part of an otherwise accepted batch.
RuntimeProfile::Counter submit_failed_ranges {TUnit::UNIT};
RuntimeProfile::Counter completed_ranges {TUnit::UNIT};
RuntimeProfile::Counter failed_ranges {TUnit::UNIT};
RuntimeProfile::Counter cancelled_ranges {TUnit::UNIT};
RuntimeProfile::Counter queue_wait_time {TUnit::TIME_NS};
RuntimeProfile::Counter read_time {TUnit::TIME_NS};
RuntimeProfile::Counter source_bytes {TUnit::BYTES};
/// Successful full source reads, including buffers cancelled after the read returned.
RuntimeProfile::Counter successful_source_bytes {TUnit::BYTES};
RuntimeProfile::Counter remote_bytes {TUnit::BYTES};
RuntimeProfile::Counter local_bytes {TUnit::BYTES};
RuntimeProfile::Counter remote_requests {TUnit::UNIT};
RuntimeProfile::Counter ready_page_hits {TUnit::UNIT};
RuntimeProfile::Counter wait_page_hits {TUnit::UNIT};
RuntimeProfile::Counter wait_time {TUnit::TIME_NS};
/// Synchronous data-page read attempts and requested bytes; a retry counts again.
RuntimeProfile::Counter fallback_pages {TUnit::UNIT};
RuntimeProfile::Counter fallback_bytes {TUnit::BYTES};
RuntimeProfile::Counter fallback_time {TUnit::TIME_NS};
RuntimeProfile::Counter consumed_page_bytes {TUnit::BYTES};
RuntimeProfile::Counter consumed_range_bytes {TUnit::BYTES};
RuntimeProfile::Counter released_pages {TUnit::UNIT};
RuntimeProfile::Counter writeback_time {TUnit::TIME_NS};
RuntimeProfile::Counter complete_blocks_submitted {TUnit::UNIT};
RuntimeProfile::Counter complete_block_bytes {TUnit::BYTES};
RuntimeProfile::Counter partial_blocks_queued {TUnit::UNIT};
RuntimeProfile::Counter partial_blocks_merged {TUnit::UNIT};
RuntimeProfile::Counter partial_fragment_bytes {TUnit::BYTES};
RuntimeProfile::Counter writeback_deduplicated_blocks {TUnit::UNIT};
RuntimeProfile::Counter writeback_rejected_blocks {TUnit::UNIT};
/// Transfer accumulated deltas into PageReadAhead counters on the reporting thread. Repeated
/// calls add only new work, including when multiple scanners share the destination profile.
void update_profile(RuntimeProfile* profile);
};
/// BE-wide gauges and cumulative totals, without query/file/cache-path labels. Gauge updates
/// follow the same ownership transitions as the scheduler and hole-fill queue.
struct ReadAheadBvars {
bvar::Adder<int64_t> resident_bytes {"doris_read_ahead_resident_bytes"};
bvar::Adder<int64_t> inflight_ranges {"doris_read_ahead_inflight_ranges"};
bvar::Adder<int64_t> rejected_batches {"doris_read_ahead_rejected_batches"};
bvar::Adder<int64_t> failed_ranges {"doris_read_ahead_failed_ranges"};
// Updated on IO completion or page consumption, independently of Profile reporting.
bvar::Adder<int64_t> source_bytes {"doris_read_ahead_source_bytes"};
bvar::Adder<int64_t> successful_source_bytes {"doris_read_ahead_successful_source_bytes"};
bvar::Adder<int64_t> remote_bytes {"doris_read_ahead_remote_bytes"};
bvar::Adder<int64_t> remote_requests {"doris_read_ahead_remote_requests"};
bvar::Adder<int64_t> consumed_page_bytes {"doris_read_ahead_consumed_page_bytes"};
bvar::Adder<int64_t> hole_fill_pending_bytes {"doris_hole_fill_pending_bytes"};
bvar::Adder<int64_t> hole_fill_active_blocks {"doris_hole_fill_active_blocks"};
bvar::Adder<int64_t> hole_fill_remote_requests {"doris_hole_fill_remote_requests"};
bvar::Adder<int64_t> hole_fill_remote_bytes {"doris_hole_fill_remote_bytes"};
bvar::Adder<int64_t> hole_fill_remote_read_time_ns {"doris_hole_fill_remote_read_time_ns"};
bvar::Adder<int64_t> hole_fill_failed_blocks {"doris_hole_fill_failed_blocks"};
bvar::Adder<int64_t> hole_fill_write_submitted_blocks {
"doris_hole_fill_write_submitted_blocks"};
bvar::Adder<int64_t> hole_fill_dropped_blocks {"doris_hole_fill_dropped_blocks"};
};
ReadAheadBvars& read_ahead_bvars();
} // namespace doris::io
+222
View File
@@ -0,0 +1,222 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "io/cache/file_range_planner.h"
#include <gtest/gtest.h>
#include <limits>
#include <vector>
#include "io/fs/read_ahead_metrics.h"
namespace doris::io {
namespace {
FileRange file_range(size_t offset, size_t size) {
return {.offset = offset, .size = size};
}
FileRangePlanOptions options(size_t max_gap_bytes, size_t max_range_bytes,
double max_read_amplification_ratio, size_t cache_block_size,
double block_fill_min_coverage) {
return {.coalesce_options = {.max_gap_bytes = max_gap_bytes,
.max_range_bytes = max_range_bytes,
.max_read_amplification_ratio = max_read_amplification_ratio},
.cache_block_size = cache_block_size,
.block_fill_min_coverage = block_fill_min_coverage};
}
FileRangePlan plan(const std::vector<FileRange>& ranges, size_t file_size,
const FileRangePlanOptions& plan_options) {
FileRangePlan result;
EXPECT_TRUE(FileRangePlanner::plan(ranges, file_size, plan_options, &result).ok());
return result;
}
TEST(FileRangePlannerTest, EmptyInputClearsPlan) {
FileRangePlan result {.ranges = {file_range(0, 1)}, .input_locations = {{.range_index = 0}}};
ASSERT_TRUE(FileRangePlanner::plan({}, 0, options(0, 1, 1.0, 1, 1.0), &result).ok());
EXPECT_EQ(result, FileRangePlan {});
}
TEST(FileRangePlannerTest, CoalescesAndMapsSortedInputs) {
const auto result = plan({file_range(0, 100), file_range(120, 40), file_range(200, 100)}, 512,
options(20, 256, 2.0, 512, 1.0));
ASSERT_EQ(result.ranges.size(), 2);
EXPECT_EQ(result.ranges[0], file_range(0, 160));
EXPECT_EQ(result.ranges[1], file_range(200, 100));
EXPECT_EQ(result.input_locations,
(std::vector<FileRangeLocation> {{.range_index = 0, .buffer_offset = 0},
{.range_index = 0, .buffer_offset = 120},
{.range_index = 1, .buffer_offset = 0}}));
}
TEST(FileRangePlannerTest, CompletesBlockAtCoverageBoundary) {
const auto result = plan({file_range(0, 512)}, 1024, options(64, 2048, 2.0, 1024, 0.5));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(0, 1024));
}
TEST(FileRangePlannerTest, LeavesBlockPartialBelowCoverageBoundary) {
const auto result = plan({file_range(0, 511)}, 1024, options(64, 2048, 2.0, 1024, 0.5));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(0, 511));
}
TEST(FileRangePlannerTest, CompletesBlockAfterCoalescingInputGap) {
const auto result = plan({file_range(0, 300), file_range(350, 250)}, 1024,
options(64, 2048, 2.0, 1024, 0.5));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(0, 1024));
}
TEST(FileRangePlannerTest, SeparatesCoalescingAndBlockFillBytes) {
ReadAheadStatistics statistics;
FileRangePlan result;
ASSERT_TRUE(FileRangePlanner::plan({file_range(0, 300), file_range(350, 250)}, 1024,
options(64, 2048, 2.0, 1024, 0.5), &result, &statistics)
.ok());
EXPECT_EQ(statistics.input_pages.value(), 2);
EXPECT_EQ(statistics.input_bytes.value(), 550);
EXPECT_EQ(statistics.coalesced_ranges.value(), 1);
EXPECT_EQ(statistics.coalesced_bytes.value(), 600);
EXPECT_EQ(statistics.block_fill_bytes.value(), 424);
EXPECT_EQ(statistics.planned_ranges.value(), 1);
EXPECT_EQ(statistics.planned_bytes.value(), 1024);
}
TEST(FileRangePlannerTest, CompletedBlockConnectsSeparateBaseRanges) {
const auto result = plan({file_range(0, 400), file_range(500, 200)}, 1024,
options(64, 1024, 2.0, 1024, 0.5));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(0, 1024));
EXPECT_EQ(result.input_locations[0].range_index, 0);
EXPECT_EQ(result.input_locations[1].range_index, 0);
}
TEST(FileRangePlannerTest, UsesValidEofBlockSizeForCoverage) {
const auto result = plan({file_range(1100, 140)}, 1280, options(64, 2048, 2.0, 1024, 0.5));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(1024, 256));
EXPECT_EQ(result.input_locations[0],
(FileRangeLocation {.range_index = 0, .buffer_offset = 76}));
}
TEST(FileRangePlannerTest, ChoosesSmallerBlockFillWhenBothEdgesDoNotFit) {
const auto result = plan({file_range(70, 170)}, 300, options(0, 250, 1.0, 100, 0.3));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(70, 230));
}
TEST(FileRangePlannerTest, RejectsBlockFillThatWouldExceedFinalRangeLimit) {
const auto result = plan({file_range(0, 700), file_range(900, 400)}, 2000,
options(64, 1200, 2.0, 1000, 0.5));
ASSERT_EQ(result.ranges.size(), 2);
EXPECT_EQ(result.ranges[0], file_range(0, 700));
EXPECT_EQ(result.ranges[1], file_range(900, 400));
}
TEST(FileRangePlannerTest, KeepsInputLargerThanCoalesceLimit) {
const auto result =
plan({file_range(0, 300), file_range(320, 32)}, 512, options(64, 256, 2.0, 512, 1.0));
ASSERT_EQ(result.ranges.size(), 2);
EXPECT_EQ(result.ranges[0], file_range(0, 300));
EXPECT_EQ(result.ranges[1], file_range(320, 32));
EXPECT_EQ(result.input_locations[0],
(FileRangeLocation {.range_index = 0, .buffer_offset = 0}));
EXPECT_EQ(result.input_locations[1],
(FileRangeLocation {.range_index = 1, .buffer_offset = 0}));
}
TEST(FileRangePlannerTest, OversizedInputDoesNotPreventOtherBlockFill) {
const auto result =
plan({file_range(0, 300), file_range(400, 80)}, 512, options(64, 256, 2.0, 100, 0.8));
ASSERT_EQ(result.ranges.size(), 2);
EXPECT_EQ(result.ranges[0], file_range(0, 300));
EXPECT_EQ(result.ranges[1], file_range(400, 100));
}
TEST(FileRangePlannerTest, KeepsNaturallyCompleteBlock) {
const auto result = plan({file_range(1024, 1024)}, 3072, options(64, 2048, 2.0, 1024, 1.0));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(1024, 1024));
}
TEST(FileRangePlannerTest, AlignsCompleteBlocksWithoutOverflow) {
const size_t file_size = std::numeric_limits<size_t>::max();
const auto result = plan({file_range(file_size - 1, 1)}, file_size, options(0, 1, 1.0, 4, 1.0));
ASSERT_EQ(result.ranges.size(), 1);
EXPECT_EQ(result.ranges[0], file_range(file_size - 1, 1));
}
TEST(FileRangePlannerTest, PreservesOutputOnInvalidOptionsOrInput) {
const FileRangePlan original {.ranges = {file_range(7, 9)},
.input_locations = {{.range_index = 0}}};
auto result = original;
EXPECT_FALSE(FileRangePlanner::plan({}, 0, options(0, 1, 1.0, 0, 0.5), &result).ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(FileRangePlanner::plan({}, 0, options(0, 1, 1.0, 1, 0.0), &result).ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(
FileRangePlanner::plan(
{}, 0, options(0, 1, 1.0, 1, std::numeric_limits<double>::quiet_NaN()), &result)
.ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(FileRangePlanner::plan({}, 0, options(0, 0, 1.0, 1, 0.5), &result).ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(
FileRangePlanner::plan({file_range(5, 0)}, 10, options(0, 10, 1.0, 10, 0.5), &result)
.ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(
FileRangePlanner::plan({file_range(9, 2)}, 10, options(0, 10, 1.0, 10, 0.5), &result)
.ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(FileRangePlanner::plan({file_range(0, 80), file_range(40, 80)}, 120,
options(0, 100, 1.0, 100, 1.0), &result)
.ok());
EXPECT_EQ(result, original);
EXPECT_FALSE(FileRangePlanner::plan({file_range(std::numeric_limits<size_t>::max() - 1, 2)},
std::numeric_limits<size_t>::max(),
options(0, 2, 1.0, 1, 1.0), &result)
.ok());
EXPECT_EQ(result, original);
}
} // namespace
} // namespace doris::io
+132
View File
@@ -0,0 +1,132 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "io/fs/read_ahead_metrics.h"
#include <gtest/gtest.h>
#include <memory>
#include <thread>
namespace doris::io {
TEST(ReadAheadMetricsTest, ReportsOnlyNewDeltasAndAggregatesReaders) {
ReadAheadStatistics first;
ReadAheadStatistics second;
RuntimeProfile profile("SegmentIterator");
COUNTER_UPDATE(&first.input_pages, 2);
COUNTER_UPDATE(&first.input_bytes, 48);
COUNTER_UPDATE(&first.plan_time, 100);
COUNTER_UPDATE(&first.column_plan_time, 1000);
COUNTER_UPDATE(&first.column_init_time, 200);
COUNTER_UPDATE(&first.page_advance_time, 80);
COUNTER_UPDATE(&first.window_discard_time, 100);
COUNTER_UPDATE(&first.current_batch_plan_time, 600);
COUNTER_UPDATE(&first.window_extend_time, 50);
COUNTER_UPDATE(&first.candidate_bytes, 60);
COUNTER_UPDATE(&first.page_cache_hit_bytes, 12);
COUNTER_UPDATE(&first.query_budget_rejected_batches, 1);
COUNTER_UPDATE(&first.be_budget_rejected_batches, 2);
COUNTER_UPDATE(&first.submit_failed_ranges, 3);
COUNTER_UPDATE(&first.successful_source_bytes, 40);
COUNTER_UPDATE(&first.fallback_pages, 1);
COUNTER_UPDATE(&first.fallback_bytes, 8);
COUNTER_UPDATE(&first.fallback_time, 200);
first.update_profile(&profile);
first.update_profile(&profile);
COUNTER_UPDATE(&first.input_bytes, 12);
COUNTER_UPDATE(&second.input_bytes, 20);
COUNTER_UPDATE(&second.column_plan_time, 300);
first.update_profile(&profile);
second.update_profile(&profile);
EXPECT_EQ(profile.get_counter("PageReadAhead")->type(), TUnit::NONE);
EXPECT_EQ(profile.get_counter("ReadAheadInputPages")->value(), 2);
EXPECT_EQ(profile.get_counter("ReadAheadInputBytes")->value(), 80);
EXPECT_EQ(profile.get_counter("ReadAheadInputBytes")->type(), TUnit::BYTES);
EXPECT_EQ(profile.get_counter("ReadAheadPlanTime")->value(), 100);
EXPECT_EQ(profile.get_counter("ReadAheadPlanTime")->type(), TUnit::TIME_NS);
EXPECT_EQ(profile.get_counter("ReadAheadColumnPlanTime")->value(), 1300);
EXPECT_EQ(profile.get_counter("ReadAheadColumnInitTime")->value(), 200);
EXPECT_EQ(profile.get_counter("ReadAheadPageAdvanceTime")->value(), 80);
EXPECT_EQ(profile.get_counter("ReadAheadWindowDiscardTime")->value(), 100);
EXPECT_EQ(profile.get_counter("ReadAheadCurrentBatchPlanTime")->value(), 600);
EXPECT_EQ(profile.get_counter("ReadAheadWindowExtendTime")->value(), 50);
EXPECT_EQ(profile.get_counter("ReadAheadCandidateBytes")->value(), 60);
EXPECT_EQ(profile.get_counter("ReadAheadPageCacheHitBytes")->value(), 12);
EXPECT_EQ(profile.get_counter("ReadAheadQueryBudgetRejectedBatches")->value(), 1);
EXPECT_EQ(profile.get_counter("ReadAheadBEBudgetRejectedBatches")->value(), 2);
EXPECT_EQ(profile.get_counter("ReadAheadSubmitFailedRanges")->value(), 3);
EXPECT_EQ(profile.get_counter("ReadAheadSuccessfulSourceBytes")->value(), 40);
EXPECT_EQ(profile.get_counter("ReadAheadFallbackPages")->value(), 1);
EXPECT_EQ(profile.get_counter("ReadAheadFallbackBytes")->value(), 8);
EXPECT_EQ(profile.get_counter("ReadAheadFallbackTime")->value(), 200);
EXPECT_EQ(first.input_bytes.value(), 0);
EXPECT_EQ(second.input_bytes.value(), 0);
EXPECT_EQ(first.column_plan_time.value(), 0);
EXPECT_EQ(first.column_init_time.value(), 0);
EXPECT_EQ(first.page_advance_time.value(), 0);
EXPECT_EQ(first.window_discard_time.value(), 0);
EXPECT_EQ(first.current_batch_plan_time.value(), 0);
EXPECT_EQ(first.window_extend_time.value(), 0);
EXPECT_EQ(second.column_plan_time.value(), 0);
TRuntimeProfileTree tree;
profile.to_thrift(&tree);
const auto& children = tree.nodes.at(0).child_counters_map;
EXPECT_EQ(children.at("ReadAheadPlanning").count("ReadAheadColumnPlanTime"), 1);
EXPECT_EQ(children.at("ReadAheadColumnPlanTime").count("ReadAheadColumnInitTime"), 1);
EXPECT_EQ(children.at("ReadAheadColumnPlanTime").count("ReadAheadWindowDiscardTime"), 1);
EXPECT_EQ(children.at("ReadAheadColumnPlanTime").count("ReadAheadCurrentBatchPlanTime"), 1);
EXPECT_EQ(children.at("ReadAheadColumnPlanTime").count("ReadAheadPageAdvanceTime"), 1);
EXPECT_EQ(children.at("ReadAheadColumnPlanTime").count("ReadAheadWindowExtendTime"), 1);
}
TEST(ReadAheadMetricsTest, PreservesConcurrentUpdatesDuringReporting) {
auto statistics = std::make_shared<ReadAheadStatistics>();
RuntimeProfile profile("SegmentIterator");
constexpr int64_t updates = 50000;
std::thread worker([statistics]() {
for (int64_t index = 0; index < updates; ++index) {
COUNTER_UPDATE(&statistics->remote_bytes, 1);
}
});
for (int report = 0; report < 100; ++report) {
statistics->update_profile(&profile);
}
worker.join();
statistics->update_profile(&profile);
EXPECT_EQ(profile.get_counter("ReadAheadRemoteBytes")->value(), updates);
EXPECT_EQ(statistics->remote_bytes.value(), 0);
}
TEST(ReadAheadMetricsTest, CountersOutliveReportedProfile) {
auto statistics = std::make_shared<ReadAheadStatistics>();
{
RuntimeProfile profile("ClosedScanner");
COUNTER_UPDATE(&statistics->completed_ranges, 1);
statistics->update_profile(&profile);
}
std::thread worker([statistics]() { COUNTER_UPDATE(&statistics->completed_ranges, 1); });
worker.join();
RuntimeProfile next_report("RemainingSnapshot");
statistics->update_profile(&next_report);
EXPECT_EQ(next_report.get_counter("ReadAheadCompletedRanges")->value(), 1);
}
} // namespace doris::io