Skip to content

perf(cpp): batch-load a device's timeseries indexes - #1095

Open
ColinLeeo wants to merge 3 commits into
developfrom
perf/batch-timeseries-indexes
Open

ColinLeeo wants to merge 3 commits into
developfrom
perf/batch-timeseries-indexes

Conversation

@ColinLeeo

@ColinLeeo ColinLeeo commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

TsFileIOReader::get_timeseries_indexes() resolved every requested measurement on its own: a binary search into the device's measurement index, one pread, a deserialize into a scratch PageArena, and a clone_from() into the caller's arena. A wide table pays that path once per column, and do_load_all_timeseries_index() also issued one pread per measurement. Loading the indexes of a 4500-column device therefore dominated the fixed cost of a table query.

Changes

  • For projections of 16 or more measurements, get_timeseries_indexes() now walks the measurement index once (get_all_leaf()), then loads the device's whole TimeseriesIndex range 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 one pread when 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 (AlignedTimeseriesIndex with the shared time index), same nullptr for 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 stays nullptr.
  • 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_Test suite passes; mvn -P with-cpp spotless:check passes.

Benchmark

get_timeseries_indexes() for every measurement of one device (2 rows per series, release build, macOS, 10 devices per iteration, fresh PageArena per query):

measurements develop this PR
4500 41.5–46.5 ms/device 0.53 ms/device
40 0.074 ms/device 0.006 ms/device
8 0.007 ms/device 0.007 ms/device (per-measurement path)

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.

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-commenter

codecov-commenter commented Nov 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.96552% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.55%. Comparing base (90c7862) to head (2afbd45).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
cpp/src/file/tsfile_io_reader.cc 68.96% 4 Missing and 14 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ColinLeeo
ColinLeeo requested a balanced review from Copilot November 12, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
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

Comment thread cpp/src/file/tsfile_io_reader.cc Resolved
Comment thread cpp/src/file/tsfile_io_reader.h Resolved
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.
@ColinLeeo
ColinLeeo requested a lite review from Copilot November 12, 2026 14:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread cpp/src/file/tsfile_io_reader.cc Resolved
Comment thread cpp/src/file/tsfile_io_reader.cc Resolved
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants