Repository navigation
perf(prefetch): prefetch the late-materialization payload ranges mid-read - #342
Merged
Merged
Conversation
lxy-9602
reviewed
Sep 16, 2026
This reverts commit 443a734.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Linked issue: close #341
The shared read-ahead cache is fed once per read-range generation through
PrefetchFileBatchReader::PreBufferRange(), before any read starts. For late materialization the payload pass cannot know which pages hold the matched rows until the probe pass has run, soPreBufferRange()only reported the probe ranges (an explicit TODO covered this) and the payload pass - the one touching the wide columns - was never prefetched: every payload read missed the cache and waited for its own underlying IO. This change lets a reader report byte ranges that only become known after reading has started, and lets the cache register them mid-read.PrefetchFileBatchReader::PreBufferRangeCallback+SetPreBufferRangeCallback()(default no-op).PrefetchFileBatchReaderImplinstalls a callback on each sub-reader;LateMaterializingFileBatchReaderreports the payload ranges once the probe pass has refined the inner reader's target pages, and propagates a failure to compute them (they come from the file metadata) rather than swallowing it.ReadAheadCache::AddRanges(std::vector<ByteRange>&&)registers ranges into a live cache and is safe to call repeatedly and concurrently withRead(). It merges into the single offset-ordered, disjoint range list, registering only the parts no registered range covers and dropping the overlap (those bytes are already being fetched), and returns the first newly-registered offset (nulloptwhen everything was already covered).Warmup(from_offset)then fetches from there instead of from the head.cache_warmed_flag rather than a round counter:CleanUp()/Close()and the reader-reuse entry points (SetReadSchema/RefreshReadRanges) clear it, so a report that outlives its read-range generation registers nothing instead of prefetching bytes nobody reads. The callbacks are detached inClose()and in the destructor after the background thread has been joined.ReadAheadCachedropped its pimpl and collapsed the three parallel containers (pending_ranges_/is_cached_/entries_) into oneranges_vector, where an entry is published exactly when its buffer is set;PreBuffer()takes the write lock only when the prefetch window still holds an unpublished range and otherwise returns under a read-lock fast path; both release paths swap the entries/blocks out under the lock and wait on the in-flight futures outside it;FileBlockCachemoved to astd::shared_mutexwith a read-lock hit path and a double-checked write-lock miss;AtomicCounterPair::Addwas renamed toRecord.CacheConfigdefault tuning:range_size_limitlowered 32 MiB -> 8 MiB (now the single cap for both the up-front and the mid-read ranges; one range is one prefetch IO, so a smaller cap spreads a pass over several concurrent requests instead of one long serial one), andhole_size_limitraised 8 KiB -> 512 KiB (coalesce across the page-sized gaps a filtered read leaves between the pages it keeps, trading bytes for fewer round trips on remote storage).read-ahead-cache.read.count/.bytes/.hits/.hit-bytes/.misses/.miss-bytes,.io.count/.bytesand.block.fetches/.fetch-bytes/.hits/.hit-bytesthroughBatchReader::GetReaderMetrics().Risk / known limits: the callback captures the impl raw, which is safe because it owns the sub-readers and
ClearPreBufferRangeCallbacks()runs after the background thread is joined.AddRanges()concurrent withRead()is memory-safe (publish happens under the write lock, lookups under the read lock). Prefetch and block fetches issue concurrentReadAsyncon the same underlying stream, so that stream must be thread-safe, as before.Bundled unrelated to the feature:
page_filtered_row_group_reader.cppchanges the up-front value-byte reservation cap from an absolute 16 MiB to 8x the file-bounded compressed chunk size (forged-footer hardening), andretry_waiter_test.cppis removed (timing-based).Tests
read_ahead_cache_test.cpp(TestReadAheadCache): newAddRangescasesTestAddRangesRegistersNewRanges,TestAddRangesClipsIntersectingRange(only the uncovered part is registered, the overlap dropped),TestAddRangesDropsCoveredRange,TestAddRangesInterleavesWithRegisteredRanges(result stays offset-ordered and disjoint),TestAddRangesSplitsLargeRange(cut atrange_size_limitinto concurrent IOs),TestAddRangesKeepsCachedRanges(published entries are not fetched twice),TestWarmupFromOffsetandTestCoalescesSmallHoles(hole_size_limit), alongside the existing no-eviction, metrics, IO-error-propagation, in-flight-racing-reader, pre-buffer-window and block-cache cases.late_materializing_file_batch_reader_test.cpp(5 new):PayloadPreBufferRangesReportedToCallback,NoCallbackReportWithoutLateMaterialization,EmptyMatchDoesNotReportPayloadRanges,PayloadPreBufferRangeErrorFailsRead, andPrefetchInnerRegistersPayloadPreBufferRanges(end to end under the prefetch layer: probe ranges registered up front plus payload ranges registered mid-read reach the shared cache and are fetched).read_inte_test.cpp::TestReadAheadCacheMetrics: asserts the read/hit/miss/io counters are exposed andhits > 0exactly when a cache is created, absent otherwise.MockFileBatchReadergained per-schema pre-buffer ranges and an injectablePreBufferRange()status;MockInputStream::ReadAsynccompletes inline so a cache-attached promise is always resolved instead of hanging reads/ReleaseBuffers(). The adaptive-sizer cases inbyte_range_combiner_test.cppwere removed together with the adaptive sizer.Validation run (release build):
paimon-common-test1637 passed / 2 skipped;TestReadAheadCache.*+*LateMaterializing*48/48;paimon-read-inte-test*ReadAheadCache*8/8;paimon-parquet-format-test*Cache*:*PreBuffer*:*ReadHints*10/10;git diff --checkclean.API and Format
New public API surface; no storage format or protocol changes:
include/paimon/reader/prefetch_file_batch_reader.h: thePreBufferRangeCallbacktype alias andvirtual void SetPreBufferRangeCallback(PreBufferRangeCallback)(default no-op).include/paimon/utils/prefetch_cache_config.h: no new knobs; two defaults change -range_size_limit32 MiB -> 8 MiB andhole_size_limit8 KiB -> 512 KiB (behavior, not signature).ReadAheadCache::AddRanges/Warmup(uint64_t)and the counter names live in the internal headersrc/paimon/common/utils/read_ahead_cache.h(ReadAheadCacheis internal, not underinclude/).Documentation
No new user-facing configuration option; the read-ahead cache stays enabled by default with no option to set, and the two
CacheConfigdefaults are internal tuning. Prefetch behavior is covered by the existingdocs/source/user_guide/prefetch.rst. No documentation changes required.Generative AI tooling
Generated-by: Qoder