github-actions[bot] commented on code in PR #66548:
URL: https://github.com/apache/doris/pull/66548#discussion_r3744234885
##########
be/src/io/cache/cached_remote_file_reader.cpp:
##########
@@ -107,6 +113,428 @@ bvar::Adder<uint64_t>
g_peer_cross_compute_group_read("peer_cross_compute_group_
bvar::Adder<uint64_t>
g_peer_same_compute_group_read("peer_same_compute_group_read");
bvar::Adder<uint64_t> g_peer_lazy_fetch_triggered("peer_lazy_fetch_triggered");
+FileScannerV2ReaderLocalCache::FileScannerV2ReaderLocalCache(
+ size_t capacity, std::shared_ptr<doris::MemTrackerLimiter>
query_mem_tracker)
+ : _capacity(capacity),
+ _query_mem_tracker(std::move(query_mem_tracker)),
+
_memory_tracker(std::make_shared<doris::MemTracker>("FileScannerV2ReaderLocalCache"))
{}
+
+FileScannerV2ReaderLocalCache::~FileScannerV2ReaderLocalCache() {
Review Comment:
[P1] Keep the cache destructor allocation-free
This destructor is implicitly `noexcept`, but `_file_caches()` allocates a
new vector and calls `reserve(_files.size())` before any of the exception-safe
drain logic runs. Under the same query/global memory pressure this cache is
designed to tolerate, `std::bad_alloc` here escapes the destructor and
terminates the BE. The registry also retains expired weak entries, so the
snapshot can allocate according to the scanner's file-count high-water mark
even when no live file cache remains. Please make teardown walk/drain the
registry without allocating, or contain allocation failure while still
releasing the budget, and add a teardown-failure regression test.
##########
be/src/io/cache/cached_remote_file_reader.cpp:
##########
@@ -1071,17 +1748,27 @@ Status
CachedRemoteFileReader::_read_from_indirect_cache(size_t offset, Slice re
stats.cache_get_or_set_timer += sw.elapsed_time();
auto empty_blocks = _collect_remote_read_blocks(holder, stats);
- size_t empty_start = 0;
- size_t empty_end = 0;
- PeerFetchedBlockSet peer_fetched_blocks;
- RETURN_IF_ERROR(_read_remote_blocks_into_cache(empty_blocks, offset,
bytes_req, already_read,
- result, is_dryrun, stats,
source_read_breakdown,
- io_ctx,
indirect_read_bytes, empty_start,
- empty_end,
peer_fetched_blocks));
+ PeerFetchedBlockSet fetched_blocks;
+ size_t run_start = 0;
+ for (size_t index = 1; index <= empty_blocks.size(); ++index) {
+ const bool end_of_run =
+ index == empty_blocks.size() ||
+ empty_blocks[index - 1]->range().right + 1 !=
empty_blocks[index]->range().left;
+ if (!end_of_run) {
+ continue;
+ }
+ // A cache hit is a hard merge boundary. Reading across it would
redownload resident data
+ // and violate the cache-aware miss coalescing invariant used by
StarRocks.
+ std::vector<FileBlockSPtr> contiguous_misses(empty_blocks.begin() +
run_start,
Review Comment:
[P2] Account for every split source request
This loop can now issue one peer/S3 call per contiguous miss run (the new
miss-hit-miss test observes two), but the shared stats retain only copied byte
totals and one last-writer-wins `from_peer_cache` bit. `_update_stats()`
therefore increments `NumRemoteIOTotal`/`NumPeerIOTotal` at most once, and a
later run can erase the source of a backward-aligned fill that copied no caller
bytes. That hides the request amplification this change is meant to control and
can omit mixed peer/S3 work. Please accumulate per-run peer/remote call counts
(and used-source state) and assert both counts in split and mixed-source tests.
##########
be/src/io/fs/buffered_reader.cpp:
##########
@@ -397,6 +494,39 @@ Status MergeRangeFileReader::_fill_box(int range_index,
size_t start_offset, siz
return Status::OK();
}
+void MergeRangeFileReader::_record_merged_read(int range_index, size_t
start_offset,
+ size_t bytes_read) {
+ if (bytes_read == 0) {
+ return;
+ }
+ if (range_index < 0) {
+ _statistics.merged_useful_bytes += bytes_read;
+ return;
+ }
+ const size_t read_end = start_offset + bytes_read;
+ size_t useful_bytes = 0;
+ size_t future_predicate_bytes = 0;
+ for (size_t index = static_cast<size_t>(range_index);
+ index < _random_access_ranges.size() &&
+ _random_access_ranges[index].start_offset < read_end;
+ ++index) {
+ const auto& range = _random_access_ranges[index];
+ const size_t overlap_start = std::max(start_offset,
range.start_offset);
+ const size_t overlap_end = std::min(read_end, range.end_offset);
+ if (overlap_start >= overlap_end) {
+ continue;
+ }
+ const size_t overlap = overlap_end - overlap_start;
+ useful_bytes += overlap;
Review Comment:
[P2] Count the union of eager-range overlaps
Eager native ranges are only sorted before this reader is constructed;
unlike deferred ranges, they are not coalesced. For pre-PARQUET-816 files, the
100-byte chunk padding can make adjacent ranges overlap, so this loop counts
those bytes twice and `bytes_read - useful_bytes` underflows. `MergedGapBytes`
can then become negative or enormous, breaking the new profile invariant.
Please normalize constructor ranges too, or compute the union of intersections
and guard the subtraction; add an eager-overlap accounting test.
##########
be/src/io/cache/cached_remote_file_reader.cpp:
##########
@@ -107,6 +113,428 @@ bvar::Adder<uint64_t>
g_peer_cross_compute_group_read("peer_cross_compute_group_
bvar::Adder<uint64_t>
g_peer_same_compute_group_read("peer_same_compute_group_read");
bvar::Adder<uint64_t> g_peer_lazy_fetch_triggered("peer_lazy_fetch_triggered");
+FileScannerV2ReaderLocalCache::FileScannerV2ReaderLocalCache(
+ size_t capacity, std::shared_ptr<doris::MemTrackerLimiter>
query_mem_tracker)
+ : _capacity(capacity),
+ _query_mem_tracker(std::move(query_mem_tracker)),
+
_memory_tracker(std::make_shared<doris::MemTracker>("FileScannerV2ReaderLocalCache"))
{}
+
+FileScannerV2ReaderLocalCache::~FileScannerV2ReaderLocalCache() {
+ auto files = _file_caches();
+ for (const auto& file : files) {
+ file->_drain(this);
+ }
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_memory_bytes == 0);
+ DORIS_CHECK(_reserved_bytes == 0);
+}
+
+std::shared_ptr<FileScannerV2ReaderLocalFileCache>
+FileScannerV2ReaderLocalCache::create_file_cache() {
+ if (_capacity == 0) {
+ return nullptr;
+ }
+ std::shared_ptr<FileScannerV2ReaderLocalFileCache> file_cache;
+ try {
+ file_cache = std::shared_ptr<FileScannerV2ReaderLocalFileCache>(
+ new FileScannerV2ReaderLocalFileCache(shared_from_this()));
+ } catch (const doris::Exception&) {
+ return nullptr;
+ } catch (const std::bad_alloc&) {
+ return nullptr;
+ }
+ {
+ std::lock_guard lock(_registry_mutex);
+ try {
+ std::erase_if(_files, [](const auto& file) { return
file.expired(); });
+ _files.emplace_back(file_cache);
+ } catch (const doris::Exception&) {
+ return nullptr;
+ } catch (const std::bad_alloc&) {
+ return nullptr;
+ }
+ }
+ return file_cache;
+}
+
+bool FileScannerV2ReaderLocalCache::_try_reserve(size_t bytes) {
+ std::lock_guard lock(_budget_mutex);
+ if (bytes > _capacity) {
+ return false;
+ }
+ if (_memory_bytes + _reserved_bytes + bytes > _capacity) {
+ return false;
+ }
+ if (_query_mem_tracker != nullptr && _query_mem_tracker->limit() >= 0 &&
+ _query_mem_tracker->consumption() + cast_set<int64_t>(_reserved_bytes
+ bytes) >
+ _query_mem_tracker->limit()) {
+ return false;
+ }
+ if (GlobalMemoryArbitrator::is_exceed_soft_mem_limit(
+ cast_set<int64_t>(_reserved_bytes + bytes))) {
+ return false;
+ }
+ _reserved_bytes += bytes;
+ return true;
+}
+
+bool FileScannerV2ReaderLocalCache::_reserve(size_t bytes,
+
FileScannerV2ReaderLocalFileCache* requester,
+ size_t* evicted) {
+ if (_try_reserve(bytes)) {
+ return true;
+ }
+ // A stream may recycle its own cold blocks, but it must never evict
another stream's hot
+ // block map. StarRocks gets the same isolation from
CacheInputStream::_block_map ownership.
+ while (requester->_evict_one()) {
+ ++*evicted;
+ if (_try_reserve(bytes)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void FileScannerV2ReaderLocalCache::_commit(size_t bytes) {
+ {
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_reserved_bytes >= bytes);
+ _reserved_bytes -= bytes;
+ _memory_bytes += bytes;
+ }
+ _memory_tracker->consume(cast_set<int64_t>(bytes));
+}
+
+void FileScannerV2ReaderLocalCache::_cancel_reservation(size_t bytes) {
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_reserved_bytes >= bytes);
+ _reserved_bytes -= bytes;
+}
+
+void FileScannerV2ReaderLocalCache::_release(size_t bytes) {
+ {
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_memory_bytes >= bytes);
+ _memory_bytes -= bytes;
+ }
+ _memory_tracker->release(cast_set<int64_t>(bytes));
+}
+
+std::vector<std::shared_ptr<FileScannerV2ReaderLocalFileCache>>
+FileScannerV2ReaderLocalCache::_file_caches() const {
+ std::vector<std::shared_ptr<FileScannerV2ReaderLocalFileCache>> files;
+ std::lock_guard lock(_registry_mutex);
+ files.reserve(_files.size());
+ for (const auto& file : _files) {
+ if (auto live_file = file.lock(); live_file != nullptr) {
+ files.push_back(std::move(live_file));
+ }
+ }
+ return files;
+}
+
+size_t FileScannerV2ReaderLocalCache::entry_count() const {
+ size_t count = 0;
+ for (const auto& file : _file_caches()) {
+ count += file->entry_count();
+ }
+ return count;
+}
+
+size_t FileScannerV2ReaderLocalCache::memory_usage() const {
+ std::lock_guard lock(_budget_mutex);
+ return _memory_bytes;
+}
+
+int64_t FileScannerV2ReaderLocalCache::tracked_memory() const {
+ return _memory_tracker->consumption();
+}
+
+FileScannerV2ReaderLocalFileCache::FileScannerV2ReaderLocalFileCache(
+ std::shared_ptr<FileScannerV2ReaderLocalCache> owner)
+ : _owner(std::move(owner)) {}
+
+FileScannerV2ReaderLocalFileCache::~FileScannerV2ReaderLocalFileCache() {
+ if (auto owner = _owner.lock(); owner != nullptr) {
+ _drain(owner.get());
+ }
+}
+
+void FileScannerV2ReaderLocalFileCache::_drain(FileScannerV2ReaderLocalCache*
owner) {
+ DORIS_CHECK(owner != nullptr);
+ size_t memory_bytes = 0;
+ size_t reserved_bytes = 0;
+ auto clear_entries = [&]() {
+ std::unique_lock lock(_mutex);
+ for (auto& [_, entry] : _entries) {
+ if (entry->data != nullptr) {
+ memory_bytes += entry->data->size();
+ }
+ reserved_bytes += entry->reserved_bytes;
+ }
+ _entries.clear();
+ _lru.clear();
+ };
+ try {
+ std::optional<SwitchThreadMemTrackerLimiter> switch_query_tracker;
+ if (owner->_query_mem_tracker != nullptr) {
+ switch_query_tracker.emplace(owner->_query_mem_tracker);
+ }
+ clear_entries();
+ } catch (...) {
+ // Destructors cannot propagate memory-tracker setup failures during
query cancellation.
+ clear_entries();
+ }
+ if (memory_bytes > 0) {
+ owner->_release(memory_bytes);
+ }
+ if (reserved_bytes > 0) {
+ owner->_cancel_reservation(reserved_bytes);
+ }
+}
+
+void FileScannerV2ReaderLocalFileCache::_touch_locked(const
std::shared_ptr<Entry>& entry) {
+ if (entry->in_lru) {
+ _lru.splice(_lru.begin(), _lru, entry->lru_position);
+ }
+}
+
+bool FileScannerV2ReaderLocalFileCache::_evict_one() {
+ const auto owner = _owner.lock();
+ if (owner == nullptr) {
+ return false;
+ }
+ std::shared_ptr<std::vector<char>> data;
+ {
+ std::lock_guard lock(_mutex);
+ size_t candidates = _lru.size();
+ while (candidates-- > 0 && !_lru.empty()) {
+ const size_t victim_offset = _lru.back();
+ const auto victim = _entries.find(victim_offset);
+ DORIS_CHECK(victim != _entries.end());
+ DORIS_CHECK(!victim->second->loading);
+ if (victim->second->data.use_count() > 1) {
+ // Keep pinned blocks discoverable so another reader cannot
start a duplicate fill.
+ _lru.splice(_lru.begin(), _lru, victim->second->lru_position);
+ continue;
+ }
+ data = std::move(victim->second->data);
+ _lru.pop_back();
+ _entries.erase(victim);
+ break;
+ }
+ }
+ if (data == nullptr) {
+ return false;
+ }
+ const size_t bytes = data->size();
+ // Eviction and destruction are noexcept cleanup paths. A stack guard
avoids a second heap
+ // allocation while releasing memory under pressure; if tracker switching
itself fails, the
+ // block is still released and the explicit cache budget remains
consistent.
+ try {
+ std::optional<SwitchThreadMemTrackerLimiter> switch_query_tracker;
+ if (owner->_query_mem_tracker != nullptr) {
+ switch_query_tracker.emplace(owner->_query_mem_tracker);
+ }
+ data.reset();
+ } catch (...) {
+ data.reset();
+ }
+ owner->_release(bytes);
+ return true;
+}
+
+void FileScannerV2ReaderLocalFileCache::_abort_load(size_t block_offset,
+ const
std::shared_ptr<Entry>& entry) {
+ size_t reserved_bytes = 0;
+ {
+ std::unique_lock lock(_mutex);
+ reserved_bytes = entry->reserved_bytes;
+ entry->reserved_bytes = 0;
+ entry->loading = false;
+ const auto it = _entries.find(block_offset);
+ if (it != _entries.end() && it->second == entry) {
+ _entries.erase(it);
+ }
+ }
+ if (reserved_bytes != 0) {
+ if (const auto owner = _owner.lock(); owner != nullptr) {
+ owner->_cancel_reservation(reserved_bytes);
+ }
+ }
+ // A loader must publish every exit, including allocation and tracker
exceptions, otherwise a
+ // same-block waiter can remain asleep after the scan has already fallen
back to FileCache.
+ entry->ready.notify_all();
+}
+
+bool FileScannerV2ReaderLocalFileCache::pin_if_present(size_t block_offset,
size_t read_offset,
+ size_t read_size,
LookupResult* lookup) {
+ DORIS_CHECK(lookup != nullptr);
+ *lookup = {};
+ std::shared_ptr<Entry> entry;
+ bool touch_lru = false;
+ {
+ std::shared_lock lock(_mutex);
+ const auto it = _entries.find(block_offset);
+ if (it == _entries.end() || it->second->loading ||
!it->second->load_status.ok() ||
+ it->second->data == nullptr || read_offset < block_offset ||
+ read_offset - block_offset > it->second->data->size() ||
+ read_size > it->second->data->size() - (read_offset -
block_offset)) {
+ return false;
+ }
+ entry = it->second;
+ lookup->data = entry->data;
+ lookup->admitted = true;
+ lookup->hit = true;
+ touch_lru =
+ entry->hit_count.fetch_add(1, std::memory_order_relaxed) %
LRU_TOUCH_INTERVAL == 0;
+ if (touch_lru) {
+ lookup->file_block_to_touch = entry->source_file_block.lock();
+ }
+ }
+ if (touch_lru) {
+ std::unique_lock lock(_mutex);
+ const auto it = _entries.find(block_offset);
+ if (it != _entries.end() && it->second == entry) {
+ _touch_locked(entry);
+ }
+ }
+ return true;
+}
+
+bool FileScannerV2ReaderLocalFileCache::read_if_present(size_t block_offset,
size_t read_offset,
+ Slice result,
LookupResult* lookup) {
+ if (!pin_if_present(block_offset, read_offset, result.size, lookup)) {
+ return false;
+ }
+ memcpy(result.data, lookup->data->data() + read_offset - block_offset,
result.size);
+ return true;
+}
+
+Status FileScannerV2ReaderLocalFileCache::get_or_load(size_t block_offset,
size_t block_size,
+ const FileBlockSPtr&
file_block,
+ size_t file_block_offset,
+ LookupResult* lookup) {
+ DORIS_CHECK(lookup != nullptr);
+ *lookup = {};
+ const auto owner = _owner.lock();
+ if (owner == nullptr) {
+ return Status::OK();
+ }
+ std::shared_ptr<Entry> entry;
+ bool load = false;
+ {
+ std::unique_lock lock(_mutex);
+ const auto it = _entries.find(block_offset);
+ if (it == _entries.end()) {
+ try {
+ entry = std::make_shared<Entry>();
+ } catch (const doris::Exception&) {
+ return Status::OK();
+ } catch (const std::bad_alloc&) {
+ return Status::OK();
+ }
+ try {
+ _entries.emplace(block_offset, entry);
+ } catch (const doris::Exception&) {
+ return Status::OK();
+ } catch (const std::bad_alloc&) {
+ return Status::OK();
+ }
+ load = true;
+ } else {
+ entry = it->second;
+ lookup->admitted = true;
+ if (entry->loading) {
+ lookup->waited = true;
+
TEST_SYNC_POINT("CachedRemoteFileReader::reader_local_cache_before_wait");
+ MonotonicStopWatch wait_watch;
+ wait_watch.start();
+ entry->ready.wait(lock, [&entry]() { return !entry->loading;
});
+ lookup->wait_time = wait_watch.elapsed_time();
+ }
+ RETURN_IF_ERROR(entry->load_status);
+ if (entry->data == nullptr || entry->data->size() < block_size) {
Review Comment:
[P2] Let a wider extent replace a short cache entry
This safely refuses to reuse a short promotion, but it leaves that entry in
`_entries` and `_lru`. If FileCache later recreates a wider cell with the same
left boundary, every wider read hits this same mismatch and falls back to disk
forever; the wider extent can never be promoted until unrelated capacity
eviction happens. Please retire/replace an unpinned undersized entry (or
include the extent in the key) while preserving single-flight behavior, and
cover short-cell-then-wider-cell repeated reads.
##########
be/benchmark/parquet/benchmark_parquet_decoder.hpp:
##########
@@ -626,7 +625,7 @@ inline void run_decoder(benchmark::State& state,
DecoderScenario scenario, int s
inline bool register_decoder_benchmarks() {
for (const auto& scenario : decoder_scenarios()) {
- for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+ for (const int selectivity : {0, 1, 5, 10, 50, 90, 100}) {
Review Comment:
[P2] Keep the benchmark validation guide in sync
This loop now registers 19 x 7 x 2 = 266 decoder cases, and the changed
invariant test expects 266, but `be/benchmark/parquet/AGENTS.md` still tells
reviewers to expect 228 in three places and still lists only six selectivities.
That guide is the mandatory validation contract for this directory, so its
prescribed registration check will reject the new matrix. Please update the
count and add the 5% axis there as part of this change.
##########
be/src/io/cache/cached_remote_file_reader.cpp:
##########
@@ -107,6 +113,428 @@ bvar::Adder<uint64_t>
g_peer_cross_compute_group_read("peer_cross_compute_group_
bvar::Adder<uint64_t>
g_peer_same_compute_group_read("peer_same_compute_group_read");
bvar::Adder<uint64_t> g_peer_lazy_fetch_triggered("peer_lazy_fetch_triggered");
+FileScannerV2ReaderLocalCache::FileScannerV2ReaderLocalCache(
+ size_t capacity, std::shared_ptr<doris::MemTrackerLimiter>
query_mem_tracker)
+ : _capacity(capacity),
+ _query_mem_tracker(std::move(query_mem_tracker)),
+
_memory_tracker(std::make_shared<doris::MemTracker>("FileScannerV2ReaderLocalCache"))
{}
+
+FileScannerV2ReaderLocalCache::~FileScannerV2ReaderLocalCache() {
+ auto files = _file_caches();
+ for (const auto& file : files) {
+ file->_drain(this);
+ }
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_memory_bytes == 0);
+ DORIS_CHECK(_reserved_bytes == 0);
+}
+
+std::shared_ptr<FileScannerV2ReaderLocalFileCache>
+FileScannerV2ReaderLocalCache::create_file_cache() {
+ if (_capacity == 0) {
+ return nullptr;
+ }
+ std::shared_ptr<FileScannerV2ReaderLocalFileCache> file_cache;
+ try {
+ file_cache = std::shared_ptr<FileScannerV2ReaderLocalFileCache>(
+ new FileScannerV2ReaderLocalFileCache(shared_from_this()));
+ } catch (const doris::Exception&) {
+ return nullptr;
+ } catch (const std::bad_alloc&) {
+ return nullptr;
+ }
+ {
+ std::lock_guard lock(_registry_mutex);
+ try {
+ std::erase_if(_files, [](const auto& file) { return
file.expired(); });
+ _files.emplace_back(file_cache);
+ } catch (const doris::Exception&) {
+ return nullptr;
+ } catch (const std::bad_alloc&) {
+ return nullptr;
+ }
+ }
+ return file_cache;
+}
+
+bool FileScannerV2ReaderLocalCache::_try_reserve(size_t bytes) {
+ std::lock_guard lock(_budget_mutex);
+ if (bytes > _capacity) {
+ return false;
+ }
+ if (_memory_bytes + _reserved_bytes + bytes > _capacity) {
+ return false;
+ }
+ if (_query_mem_tracker != nullptr && _query_mem_tracker->limit() >= 0 &&
+ _query_mem_tracker->consumption() + cast_set<int64_t>(_reserved_bytes
+ bytes) >
+ _query_mem_tracker->limit()) {
+ return false;
+ }
+ if (GlobalMemoryArbitrator::is_exceed_soft_mem_limit(
+ cast_set<int64_t>(_reserved_bytes + bytes))) {
+ return false;
+ }
+ _reserved_bytes += bytes;
+ return true;
+}
+
+bool FileScannerV2ReaderLocalCache::_reserve(size_t bytes,
+
FileScannerV2ReaderLocalFileCache* requester,
+ size_t* evicted) {
+ if (_try_reserve(bytes)) {
+ return true;
+ }
+ // A stream may recycle its own cold blocks, but it must never evict
another stream's hot
+ // block map. StarRocks gets the same isolation from
CacheInputStream::_block_map ownership.
+ while (requester->_evict_one()) {
+ ++*evicted;
+ if (_try_reserve(bytes)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void FileScannerV2ReaderLocalCache::_commit(size_t bytes) {
+ {
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_reserved_bytes >= bytes);
+ _reserved_bytes -= bytes;
+ _memory_bytes += bytes;
+ }
+ _memory_tracker->consume(cast_set<int64_t>(bytes));
+}
+
+void FileScannerV2ReaderLocalCache::_cancel_reservation(size_t bytes) {
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_reserved_bytes >= bytes);
+ _reserved_bytes -= bytes;
+}
+
+void FileScannerV2ReaderLocalCache::_release(size_t bytes) {
+ {
+ std::lock_guard lock(_budget_mutex);
+ DORIS_CHECK(_memory_bytes >= bytes);
+ _memory_bytes -= bytes;
+ }
+ _memory_tracker->release(cast_set<int64_t>(bytes));
+}
+
+std::vector<std::shared_ptr<FileScannerV2ReaderLocalFileCache>>
+FileScannerV2ReaderLocalCache::_file_caches() const {
+ std::vector<std::shared_ptr<FileScannerV2ReaderLocalFileCache>> files;
+ std::lock_guard lock(_registry_mutex);
+ files.reserve(_files.size());
+ for (const auto& file : _files) {
+ if (auto live_file = file.lock(); live_file != nullptr) {
+ files.push_back(std::move(live_file));
+ }
+ }
+ return files;
+}
+
+size_t FileScannerV2ReaderLocalCache::entry_count() const {
+ size_t count = 0;
+ for (const auto& file : _file_caches()) {
+ count += file->entry_count();
+ }
+ return count;
+}
+
+size_t FileScannerV2ReaderLocalCache::memory_usage() const {
+ std::lock_guard lock(_budget_mutex);
+ return _memory_bytes;
+}
+
+int64_t FileScannerV2ReaderLocalCache::tracked_memory() const {
+ return _memory_tracker->consumption();
+}
+
+FileScannerV2ReaderLocalFileCache::FileScannerV2ReaderLocalFileCache(
+ std::shared_ptr<FileScannerV2ReaderLocalCache> owner)
+ : _owner(std::move(owner)) {}
+
+FileScannerV2ReaderLocalFileCache::~FileScannerV2ReaderLocalFileCache() {
+ if (auto owner = _owner.lock(); owner != nullptr) {
+ _drain(owner.get());
+ }
+}
+
+void FileScannerV2ReaderLocalFileCache::_drain(FileScannerV2ReaderLocalCache*
owner) {
+ DORIS_CHECK(owner != nullptr);
+ size_t memory_bytes = 0;
+ size_t reserved_bytes = 0;
+ auto clear_entries = [&]() {
+ std::unique_lock lock(_mutex);
+ for (auto& [_, entry] : _entries) {
+ if (entry->data != nullptr) {
+ memory_bytes += entry->data->size();
+ }
+ reserved_bytes += entry->reserved_bytes;
+ }
+ _entries.clear();
+ _lru.clear();
+ };
+ try {
+ std::optional<SwitchThreadMemTrackerLimiter> switch_query_tracker;
+ if (owner->_query_mem_tracker != nullptr) {
+ switch_query_tracker.emplace(owner->_query_mem_tracker);
+ }
+ clear_entries();
+ } catch (...) {
+ // Destructors cannot propagate memory-tracker setup failures during
query cancellation.
+ clear_entries();
+ }
+ if (memory_bytes > 0) {
+ owner->_release(memory_bytes);
+ }
+ if (reserved_bytes > 0) {
+ owner->_cancel_reservation(reserved_bytes);
+ }
+}
+
+void FileScannerV2ReaderLocalFileCache::_touch_locked(const
std::shared_ptr<Entry>& entry) {
+ if (entry->in_lru) {
+ _lru.splice(_lru.begin(), _lru, entry->lru_position);
+ }
+}
+
+bool FileScannerV2ReaderLocalFileCache::_evict_one() {
+ const auto owner = _owner.lock();
+ if (owner == nullptr) {
+ return false;
+ }
+ std::shared_ptr<std::vector<char>> data;
+ {
+ std::lock_guard lock(_mutex);
+ size_t candidates = _lru.size();
+ while (candidates-- > 0 && !_lru.empty()) {
+ const size_t victim_offset = _lru.back();
+ const auto victim = _entries.find(victim_offset);
+ DORIS_CHECK(victim != _entries.end());
+ DORIS_CHECK(!victim->second->loading);
+ if (victim->second->data.use_count() > 1) {
+ // Keep pinned blocks discoverable so another reader cannot
start a duplicate fill.
+ _lru.splice(_lru.begin(), _lru, victim->second->lru_position);
+ continue;
+ }
+ data = std::move(victim->second->data);
+ _lru.pop_back();
+ _entries.erase(victim);
+ break;
+ }
+ }
+ if (data == nullptr) {
+ return false;
+ }
+ const size_t bytes = data->size();
+ // Eviction and destruction are noexcept cleanup paths. A stack guard
avoids a second heap
+ // allocation while releasing memory under pressure; if tracker switching
itself fails, the
+ // block is still released and the explicit cache budget remains
consistent.
+ try {
+ std::optional<SwitchThreadMemTrackerLimiter> switch_query_tracker;
+ if (owner->_query_mem_tracker != nullptr) {
+ switch_query_tracker.emplace(owner->_query_mem_tracker);
+ }
+ data.reset();
+ } catch (...) {
+ data.reset();
+ }
+ owner->_release(bytes);
+ return true;
+}
+
+void FileScannerV2ReaderLocalFileCache::_abort_load(size_t block_offset,
+ const
std::shared_ptr<Entry>& entry) {
+ size_t reserved_bytes = 0;
+ {
+ std::unique_lock lock(_mutex);
+ reserved_bytes = entry->reserved_bytes;
+ entry->reserved_bytes = 0;
+ entry->loading = false;
+ const auto it = _entries.find(block_offset);
+ if (it != _entries.end() && it->second == entry) {
+ _entries.erase(it);
+ }
+ }
+ if (reserved_bytes != 0) {
+ if (const auto owner = _owner.lock(); owner != nullptr) {
+ owner->_cancel_reservation(reserved_bytes);
+ }
+ }
+ // A loader must publish every exit, including allocation and tracker
exceptions, otherwise a
+ // same-block waiter can remain asleep after the scan has already fallen
back to FileCache.
+ entry->ready.notify_all();
+}
+
+bool FileScannerV2ReaderLocalFileCache::pin_if_present(size_t block_offset,
size_t read_offset,
+ size_t read_size,
LookupResult* lookup) {
+ DORIS_CHECK(lookup != nullptr);
+ *lookup = {};
+ std::shared_ptr<Entry> entry;
+ bool touch_lru = false;
+ {
+ std::shared_lock lock(_mutex);
+ const auto it = _entries.find(block_offset);
+ if (it == _entries.end() || it->second->loading ||
!it->second->load_status.ok() ||
+ it->second->data == nullptr || read_offset < block_offset ||
+ read_offset - block_offset > it->second->data->size() ||
+ read_size > it->second->data->size() - (read_offset -
block_offset)) {
+ return false;
+ }
+ entry = it->second;
+ lookup->data = entry->data;
+ lookup->admitted = true;
+ lookup->hit = true;
+ touch_lru =
+ entry->hit_count.fetch_add(1, std::memory_order_relaxed) %
LRU_TOUCH_INTERVAL == 0;
+ if (touch_lru) {
+ lookup->file_block_to_touch = entry->source_file_block.lock();
+ }
+ }
+ if (touch_lru) {
+ std::unique_lock lock(_mutex);
+ const auto it = _entries.find(block_offset);
+ if (it != _entries.end() && it->second == entry) {
+ _touch_locked(entry);
+ }
+ }
+ return true;
+}
+
+bool FileScannerV2ReaderLocalFileCache::read_if_present(size_t block_offset,
size_t read_offset,
+ Slice result,
LookupResult* lookup) {
+ if (!pin_if_present(block_offset, read_offset, result.size, lookup)) {
+ return false;
+ }
+ memcpy(result.data, lookup->data->data() + read_offset - block_offset,
result.size);
+ return true;
+}
+
+Status FileScannerV2ReaderLocalFileCache::get_or_load(size_t block_offset,
size_t block_size,
+ const FileBlockSPtr&
file_block,
+ size_t file_block_offset,
+ LookupResult* lookup) {
+ DORIS_CHECK(lookup != nullptr);
+ *lookup = {};
+ const auto owner = _owner.lock();
+ if (owner == nullptr) {
+ return Status::OK();
+ }
+ std::shared_ptr<Entry> entry;
+ bool load = false;
+ {
+ std::unique_lock lock(_mutex);
+ const auto it = _entries.find(block_offset);
+ if (it == _entries.end()) {
+ try {
+ entry = std::make_shared<Entry>();
+ } catch (const doris::Exception&) {
+ return Status::OK();
+ } catch (const std::bad_alloc&) {
+ return Status::OK();
+ }
+ try {
+ _entries.emplace(block_offset, entry);
+ } catch (const doris::Exception&) {
+ return Status::OK();
+ } catch (const std::bad_alloc&) {
+ return Status::OK();
+ }
+ load = true;
+ } else {
+ entry = it->second;
+ lookup->admitted = true;
+ if (entry->loading) {
+ lookup->waited = true;
+
TEST_SYNC_POINT("CachedRemoteFileReader::reader_local_cache_before_wait");
+ MonotonicStopWatch wait_watch;
+ wait_watch.start();
+ entry->ready.wait(lock, [&entry]() { return !entry->loading;
});
+ lookup->wait_time = wait_watch.elapsed_time();
+ }
+ RETURN_IF_ERROR(entry->load_status);
+ if (entry->data == nullptr || entry->data->size() < block_size) {
+ // FileCache ranges can end at different boundaries for the
same aligned offset.
+ // Never reuse a shorter promotion for a later, wider range.
+ lookup->admitted = false;
+ return Status::OK();
+ }
+ lookup->hit = true;
+ lookup->data = entry->data;
+ _touch_locked(entry);
+ }
+ }
+
+ if (!load) {
+ return Status::OK();
+ }
+
+ std::shared_ptr<std::vector<char>> data;
+ std::optional<SwitchThreadMemTrackerLimiter> switch_query_tracker;
+ try {
+ if (!owner->_reserve(block_size, this, &lookup->evicted)) {
Review Comment:
[P2] Publish reservation failures as admission rejects
`LookupResult::admission_rejected` defaults to false and is never set
anywhere, including this `_reserve()` failure path. `_read_local_block()`
consequently adds zero to `ReaderLocalCacheAdmissionRejects` for every
capacity, query-limit, and global-soft-limit rejection, making the new counter
permanently dead. Please set the flag for policy rejection (and define whether
allocation failure is included), then assert it in the existing pressure tests.
##########
be/src/exec/scan/file_scanner_v2.cpp:
##########
@@ -1095,7 +1110,10 @@ void FileScannerV2::update_realtime_counters() {
_state->get_query_ctx()->resource_ctx()->io_context()->update_scan_bytes_from_remote_storage(
deltas.scan_bytes_from_remote_storage);
- COUNTER_SET(_file_read_bytes_counter, bytes_read);
+ // Scanner instances share the profile counter, so publishing an absolute
value would erase
+ // bytes already reported by sibling scanners.
+ COUNTER_UPDATE(_file_read_bytes_counter,
Review Comment:
[P2] Aggregate calls and time across sibling scanners too
The sharing rule in this new comment applies to all three adjacent counters,
but only bytes was converted to a per-scanner additive delta. `FileReadCalls`
and `FileReadTime` still use scanner-local cumulative `COUNTER_SET` values here
and in `_collect_profile_before_close()`, so each realtime/final publication
overwrites work already reported by sibling scanners and the result depends on
scheduling/close order. Please track reported calls/time and `COUNTER_UPDATE`
their deltas as well, then add an interleaved two-scanner profile test that
converges to the sum.
##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -2310,6 +2373,9 @@ Status ParquetScanScheduler::read_filter_columns(int64_t
batch_rows,
const size_t idx =
_predicate_indices_by_position_scratch.at(position);
const auto& col = request.predicate_columns[idx];
const auto fid = col.column_id();
+ if (_current_merge_range_reader != nullptr) {
+ RETURN_IF_ERROR(activate_merge_ranges_for_columns({fid}));
Review Comment:
[P1] Activate deferred ranges before skipping later predicates
When an early staged predicate reduces `selected_rows` to zero, this
activation path is never reached for the remaining predicate columns.
`skip_unmaterialized_predicate_columns()` still calls
`NativeColumnReader::skip(batch_rows)` on each of them; inside the current
selected range that executes `read_with_filter()`, but the deferred
`MergeRangeFileReader` still has no registered range and therefore falls
through to direct underlying reads. Highly selective cold remote scans can
regress to one set of unmerged page/header reads for every later predicate
column. Please activate each unmaterialized column before its physical skip (or
retain a logical pending skip until activation), and cover the zero-survivor
staged-predicate path with an underlying-request-count test.
##########
be/src/io/cache/cached_remote_file_reader.cpp:
##########
@@ -1246,6 +1933,21 @@ Status CachedRemoteFileReader::read_at_impl(size_t
offset, Slice result, size_t*
}
}};
+ const bool bypass_reader_local_cache = io_ctx->bypass_reader_local_cache;
+ MonotonicStopWatch reader_local_probe_watch;
+ reader_local_probe_watch.start();
+ const bool reader_local_hit =
Review Comment:
[P2] Promote full hits in the remote-only policy
The hot probe above cannot help until some path has populated the
reader-local map, but `_read_remote_only_on_cache_miss()` still reads fully
covered blocks via `FileBlock::read()` directly. After the scanner's write
limiter switches to this policy, an unwrapped/large-chunk Parquet reader can
repeatedly hit FileCache on disk without ever creating the 256 KiB promotion,
so the default-enabled cache is ineffective for exactly those revisits. Please
route full covered hits through `_read_local_block()` while retaining
remote-only miss behavior, and test this with an external reader (the current
Doris-table tests disable the cache).
##########
be/src/io/fs/buffered_reader.cpp:
##########
@@ -397,6 +494,39 @@ Status MergeRangeFileReader::_fill_box(int range_index,
size_t start_offset, siz
return Status::OK();
}
+void MergeRangeFileReader::_record_merged_read(int range_index, size_t
start_offset,
+ size_t bytes_read) {
+ if (bytes_read == 0) {
+ return;
+ }
+ if (range_index < 0) {
+ _statistics.merged_useful_bytes += bytes_read;
+ return;
+ }
+ const size_t read_end = start_offset + bytes_read;
+ size_t useful_bytes = 0;
+ size_t future_predicate_bytes = 0;
+ for (size_t index = static_cast<size_t>(range_index);
+ index < _random_access_ranges.size() &&
+ _random_access_ranges[index].start_offset < read_end;
+ ++index) {
+ const auto& range = _random_access_ranges[index];
+ const size_t overlap_start = std::max(start_offset,
range.start_offset);
+ const size_t overlap_end = std::min(read_end, range.end_offset);
+ if (overlap_start >= overlap_end) {
+ continue;
+ }
+ const size_t overlap = overlap_end - overlap_start;
+ useful_bytes += overlap;
+ if (_range_stages[index] > _range_stages[range_index]) {
Review Comment:
[P2] Make future-stage accounting reachable from production
This comparison has no production path with a greater-stage range already
present. Eager readers construct the whole plan with every stage set to 0 and
never call `add_random_access_ranges()`; deferred readers insert only the
current predicate immediately before reading it, and lazy outputs are inserted
together at one stage. `FuturePredicatePrefetchBytes` therefore stays zero,
while the changed nonzero test manually pre-registers stage 1 in a sequence the
scheduler never performs. Please instrument stage metadata/visibility from the
real scheduler (or remove/rename this counter if zero is the intended
invariant) and cover it with a scheduler-level profile test.
--
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]