Repository navigation
Conversation
get_timeseries_indexes() resolved each requested measurement on its own: binary search into the measurement index, one pread, a deserialize into a scratch arena and a clone into the caller's arena. A wide table pays that path once per column (4500 times for a 4500-column device), and do_load_all_timeseries_index() issued one pread per measurement too. For projections of 16 or more measurements, walk the measurement index once and read the device's whole TimeseriesIndex range with a single pread: the leaf entries tile one contiguous file range, and each index is deserialized straight into the caller's arena, so the clone per column disappears. Narrow projections keep the per-measurement path, where a full walk would decode every column to find a handful. do_load_all_timeseries_index() now also uses the single range read (with a per-entry fallback when the entries do not tile), which speeds up the prepared-series and device-metadata paths as well. Loading all indexes of one device: 0.53 ms instead of 42 ms for 4500 columns, and 0.006 ms instead of 0.074 ms for 40 (10 devices x 20..200 iterations, 2 rows per series, local macOS release build). An 8-column request is unchanged: it keeps the per-measurement path.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1095 +/- ##
===========================================
+ Coverage 66.51% 66.55% +0.03%
===========================================
Files 778 782 +4
Lines 56577 56743 +166
Branches 9105 9149 +44
===========================================
+ Hits 37633 37764 +131
- Misses 17291 17308 +17
- Partials 1653 1671 +18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
5 open issues
Allocating the entire read buffer (ti_buf) fromPageArenameans the raw bytes for the whole… · New This test allocatesMeasurementSchemawithnewand passes raw pointers, but it's not clear from… · New Assertingchunks->size() == 1can make this test brittle if chunking behavior changes (e.g.,… · New The comment says it deserializes \"every TimeseriesIndex\" in the range, but the implementation… · New The comment is inaccurate: GoogleTest typically runs all tests in a single process by default.… · New
What changed in this PR
Improves C++ TsFile timeseries index lookup performance for wide projections by batch-loading timeseries index blocks and reducing per-measurement syscalls/deserialization overhead.
Changes:
- Add a batch-load fast path in
TsFileIOReader::get_timeseries_indexes()for requests ≥16 measurements. - Optimize
do_load_all_timeseries_index()to pread a contiguous[first,last)range in one call when leaf entries tile the range, with fallback to per-entry reads. - Add a focused C++ test suite validating correctness for batch-load behavior (missing measurements, aligned devices, and parity with per-measurement path).
| File | Description |
|---|---|
| cpp/test/file/timeseries_index_batch_load_test.cc | Adds unit tests covering batch-load behavior, aligned wrapping, missing measurements, and consistency with the old per-measurement loader. |
| cpp/src/file/tsfile_io_reader.h | Declares the new helper API for loading a contiguous TimeseriesIndex byte range. |
| cpp/src/file/tsfile_io_reader.cc | Implements batch-load logic, range-loading helper, and single-pread optimization when leaf entries tile a contiguous region. |
Review effort: Lite
Release the raw index-range buffer right after the indexes are parsed: it comes from the heap now, so a wide device's whole serialized range no longer stays resident in the caller's arena after deserialization. Test cleanups: own the registered measurement schemas in unique_ptrs and release them only after the registration that takes ownership succeeds; accept one or more chunks instead of exactly one; and describe the per-case file name in terms of how ctest and a direct binary run actually share a working directory.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
5 open issues
Batch-loading currently hard-limits the readable TimeseriesIndex range toINT32_MAXbytes and… · New This test uses POSIX open flags (O_WRONLY | O_CREAT | O_TRUNC) and fixed-width integer types… · New The per-test file name uses onlyTestInfo::name(), which can collide across different test suites… · New This test uses POSIX open flags (O_WRONLY | O_CREAT | O_TRUNC) and fixed-width integer types… · New Since this code path targets wide devices and can insert thousands of entries intoname_to_index,… · New
5 resolved since last review
Assertingchunks->size() == 1can make this test brittle if chunking behavior changes (e.g.,… This test allocatesMeasurementSchemawithnewand passes raw pointers, but it's not clear from… Allocating the entire read buffer (ti_buf) fromPageArenameans the raw bytes for the whole… The comment is inaccurate: GoogleTest typically runs all tests in a single process by default.… The comment says it deserializes \"every TimeseriesIndex\" in the range, but the implementation…
Review effort: Lite
Fall back to per-entry reads when a device's combined index range exceeds INT32_MAX bytes instead of rejecting it, and report E_OVERFLOW (not E_TSFILE_CORRUPTED) for a single entry that large. Reserve the name map for the wide-device case, include the headers the test uses directly, and make each test's file name unique across suites.


Motivation
TsFileIOReader::get_timeseries_indexes()resolved every requested measurement on its own: a binary search into the device's measurement index, onepread, a deserialize into a scratchPageArena, and aclone_from()into the caller's arena. A wide table pays that path once per column, anddo_load_all_timeseries_index()also issued onepreadper measurement. Loading the indexes of a 4500-column device therefore dominated the fixed cost of a table query.Changes
get_timeseries_indexes()now walks the measurement index once (get_all_leaf()), then loads the device's wholeTimeseriesIndexrange and maps the results by measurement name. A narrow projection keeps the per-measurement path, where a full walk would deserialize every column to find a handful.do_load_all_timeseries_index()now reads the whole[first start, last end)range with onepreadwhen the leaf entries tile it (they do for every normal file; a per-entry fallback remains for the rest) and deserializes each index straight into the given arena. This also speeds up the prepared-series (alloc_multi_ssi) and device-metadata paths that share the helper.Nothing about the returned indexes changes: same chunk metadata, same aligned wrapper (
AlignedTimeseriesIndexwith the shared time index), samenullptrfor a measurement the device does not have.Tests
New
cpp/test/file/timeseries_index_batch_load_test.cc(40 measurements, non-aligned and aligned devices):LoadsEveryMeasurementOfWideDevice— bulk path returns every column with its chunk metadata.MissingMeasurementBecomesNull— an absent name in a bulk request staysnullptr.AlignedDeviceSharesTheTimeIndex— bulk path wraps aligned columns with the device's time index.NarrowRequestUsesPerMeasurementPath— the per-measurement path still resolves below the threshold.MatchesPerMeasurementPath— chunk offsets of the bulk path equal those of the per-measurement path for the same columns.Full
TsFile_Testsuite passes;mvn -P with-cpp spotless:checkpasses.Benchmark
get_timeseries_indexes()for every measurement of one device (2 rows per series, release build, macOS, 10 devices per iteration, freshPageArenaper query):The 4500-column case is index loading only, not a full query; for a wide-table scan it removes the per-column arena/deserialize/clone cost, which was a large part of the device's fixed read overhead.