Optimize get_timeseries_metadata() to eliminate N+1 I/O (~94x speedup)#815
Optimize get_timeseries_metadata() to eliminate N+1 I/O (~94x speedup)#815JackieTien97 wants to merge 10 commits into
Conversation
- Add get_device_timeseries_meta_by_offset() that accepts pre-resolved offsets, skipping redundant load_device_index_entry() tree search and reusing the deserialized measurement index node via get_all_leaf() instead of re-reading the same bytes through load_all_measurement_index_entry() - Add get_all_device_entries() to collect device IDs with their (start_offset, end_offset) in a single index tree traversal - Rewrite get_timeseries_metadata() to use the above two methods - Remove redundant PageArena::init() call in get_timeseries_metadata_impl
379c30a to
a7413de
Compare
There was a problem hiding this comment.
Pull request overview
This PR optimizes the “collect metadata for all devices” path by avoiding repeated device-index traversals and redundant disk reads when building the full device→timeseries-metadata map.
Changes:
- Adds an offset-based metadata loader (
TsFileIOReader::get_device_timeseries_meta_by_offset) to skip per-device index-tree searches. - Adds a single-pass device index traversal that collects
(device_id, start_offset, end_offset)entries for all devices. - Rewrites
TsFileReader::get_timeseries_metadata()to use the above and removes a redundantPageArena::init()call.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| cpp/src/reader/tsfile_reader.cc | Adds device-entry collection by offset and rewrites the “all devices metadata” path to use it. |
| cpp/src/file/tsfile_io_reader.h | Declares new offset-based device metadata API. |
| cpp/src/file/tsfile_io_reader.cc | Implements new offset-based device metadata loader to reduce redundant reads/traversals. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
JackieTien97
left a comment
There was a problem hiding this comment.
Code Review
Great optimization — the 94x speedup from eliminating the N+1 I/O pattern is impressive. The approach of collecting device entries with offsets in a single traversal and then reusing the deserialized top node is well-designed.
One critical bug found: integer truncation of 64-bit file offsets in get_all_device_entries. See inline comments.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #815 +/- ##
===========================================
- Coverage 63.18% 63.17% -0.01%
===========================================
Files 717 717
Lines 43783 43875 +92
Branches 6526 6562 +36
===========================================
+ Hits 27664 27720 +56
- Misses 15127 15145 +18
- Partials 992 1010 +18 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
When DeviceMetaIterator receives a single TagEq filter (exact match on one tag column), construct the target device ID and use load_device_index_entry() for O(log N) B-tree binary search instead of traversing all internal nodes and scanning all leaf devices. For ecg_dataset (53040 devices), this reduces queryByRow's DeviceTaskIterator::next() from ~117ms to ~0.5ms per query (~230x). Also adds bench_read.cpp for standalone C++ read path benchmarking and makes load_device_index_entry() public for reuse. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TagEq direct B-tree lookup constructed a device ID with only col_idx_+1 segments, but devices with multiple tag columns have more. The segment count mismatch caused operator== to always return false, so queries returned 0 rows. Guard the optimization to only activate when the filter fully specifies the device ID (single tag column).
- GetTimeseriesMetadataTableModel: test get_timeseries_metadata() with table model, covering get_all_device_entries (LEAF_DEVICE path) and get_device_timeseries_meta_by_offset - GetTimeseriesMetadataMultiTable: test with two tables, covering the multi-table iteration in get_timeseries_metadata - DirectLookupSingleTagColumn: test the TagEq direct B-tree lookup optimization with a single-tag-column table, covering try_setup_direct_lookup and load_results_direct - DirectLookupNonExistDevice: test direct lookup returns 0 rows when the device does not exist
The previous guard (actual_segment_count != eq->col_idx_ + 1) still allowed direct lookup when filtering on the last tag column in a multi-tag table (e.g., 2 tags: segment_count=3, col_idx=2, 2+1=3). The constructed device ID had empty segments for unfiltered tags, causing B-tree lookup to find nothing. Restrict direct lookup to single-tag tables only (segment_count == 2).
Summary
Optimize TsFile C++ read path in two areas:
1. Optimize
get_timeseries_metadata()— eliminate N+1 I/O (~94x speedup for init)Root Causes:
get_all_device_ids()traverses the entire device index tree but discards offset information. Then for each device,load_device_index_entry()re-searches the tree from the root — O(N×D) redundant disk reads for N devices and tree depth D.get_device_timeseries_meta_without_chunk_meta()reads the measurement index node once to check alignment, thenload_all_measurement_index_entry()reads the exact same byte range again.Changes:
TsFileIOReader::get_device_timeseries_meta_by_offset()— accepts pre-resolved offsets (skipsload_device_index_entry), reuses deserialized top node forget_all_leaf()(eliminates duplicate read)TsFileReader::get_all_device_entries()— single tree traversal collecting device IDs with their(start_offset, end_offset)get_timeseries_metadata()to combine the abovePageArena::init()call fromget_timeseries_metadata_impl()Benchmark (
ecg_dataset/part_0.tsfile, 634 MB, 53040 devices):get_all_device_ids+get_timeseries_metadata(ids))get_timeseries_metadata()optimized)2. Optimize
queryByRowwith exact tag filter — B-tree direct lookup (~230x speedup per query)Root Cause:
When
queryByRowis called with a tag filter (e.g. exact match on one device),DeviceMetaIteratorstill traverses the entire MetaIndexNode tree: reading all 208 internal nodes from disk and scanning all 53040 leaf device entries to check the filter. This takes ~117ms per query even though only 1 device matches.Changes:
DeviceMetaIterator::try_setup_direct_lookup()— detects singleTagEqfilter at construction time and constructs the target device IDDeviceMetaIterator::load_results_direct()— usesload_device_index_entry()for O(log N) B-tree binary search instead of full tree traversalload_device_index_entry()from private to public inTsFileIOReaderfor reusebench_read.cppfor standalone C++ read path benchmarkingBenchmark (
ecg_dataset/part_0.tsfile, 53040 devices, offset=1000, limit=3584):DeviceTaskIterator::next()first next()(full lazy init)queryByRowEnd-to-end Python benchmark (training sample read, ecg_dataset):
Test plan