Skip to content

perf(cpp): cut allocations when deserializing device ids - #1090

Merged
ColinLeeo merged 4 commits into
developfrom
perf/read-metadata-deserialize
Nov 13, 2026
Merged

ColinLeeo merged 4 commits into
developfrom
perf/read-metadata-deserialize

Conversation

@ColinLeeo

@ColinLeeo ColinLeeo commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Two avoidable costs on the metadata deserialization path. It runs once per device index entry, and each device lookup re-deserializes a whole leaf node (up to max_degree_of_index_node_ = 256 entries), so these costs were paid hundreds of times per device on a scan:

  1. DeviceMetaIndexEntry::deserialize_from() built its device id with StringArrayDeviceID("init") and then immediately overwrote it. Constructing that string runs the path splitter (split_string in the current build — the ANTLR4 code path is selected by a macro that is not defined for the library targets; ENABLE_ANTLR4 only pulls in the runtime), plus a std::string allocation, for a value that was never used. ~0.16 µs per entry.
  2. SerializationUtil::read_var_char_ptr() copied each segment through a temporary mem_alloc'ed buffer: mem_alloc, memcpy, new std::string, mem_free.

Change

  • Default-construct StringArrayDeviceID instead of passing "init".
  • Read straight into the string's own buffer, one allocation per segment instead of three (two for segments longer than the string's SSO buffer).
  • Reject a segment length larger than the remaining bytes before resizing, so a corrupt 4-byte length cannot trigger a 2 GiB zero-fill.

Benchmark

3.2M rows / 1000 devices, table-model full scan, single thread (alternating dylib A/B). Each device lookup deserializes a leaf of up to 256 entries, which is why a per-entry cost of ~0.16 µs shows up in the scan:

version read
baseline 0.376–0.394 s
only read_var_char_ptr 0.349–0.352 s (−9%)
only the "init" fix 0.319–0.324 s (−17%)
both 0.295–0.304 s (−23%)

Testing

Full C++ suite: 1460 tests, 1457 passed, 3 skipped. New unit tests cover device-id round trips, null/empty segments, negative lengths and truncated streams.

Two small fixes on the metadata deserialization path, which runs once
per device index entry and is hot during table scans:

* DeviceMetaIndexEntry::deserialize_from() built the device id with
  StringArrayDeviceID("init") and immediately discarded those segments.
  That constructor runs the path parser (ANTLR4 when enabled) on the
  placeholder string, so every device paid a parse plus an allocation.
  Default-construct instead.

* SerializationUtil::read_var_char_ptr() copied each segment through a
  temporary mem_alloc'ed buffer: mem_alloc, memcpy, new std::string,
  mem_free. Read straight into the string's own buffer instead, one
  allocation per segment.

3.2M-row scan over 1000 devices: 0.376s -> 0.290s (~23%); the two
changes measured at ~17% and ~9% respectively.
Round-trip device ids whose segments include one longer than the small
string buffer, an empty segment and a null segment, and pin the rewritten
read_var_char_ptr() contract: null sentinel yields nullptr, zero length
yields an empty string, negative length is rejected.
@codecov-commenter

codecov-commenter commented Nov 12, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
cpp/src/common/allocator/byte_stream.h 63.63% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1090      +/-   ##
===========================================
+ Coverage    66.51%   66.56%   +0.04%     
===========================================
  Files          778      782       +4     
  Lines        56577    56712     +135     
  Branches      9105     9136      +31     
===========================================
+ Hits         37633    37749     +116     
- Misses       17291    17304      +13     
- Partials      1653     1659       +6     

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

read_var_char_ptr() resized the output string before reading, so a corrupt
4-byte length field (any value up to INT32_MAX) made it allocate and
zero-fill up to 2 GiB before the short read was detected, and a failed
resize threw std::bad_alloc out of the read path.

Check the length against the remaining bytes first and return
E_BUF_NOT_ENOUGH without allocating.  Add a truncated-stream test.

Also stop leaking the segment strings in the two new device-id tests: the
StringArrayDeviceID constructor deep-copies them, so the caller keeps
ownership (LeakSanitizer would flag the Ubuntu ASan job).
Parallel CTest processes can seed the name generator in the same clock
tick; one then removes the file another is opening (E_ALREADY_EXIST at
the fixture's open, observed as SEGFAULT / test failure on CI). Append
pid and a per-process counter, as the writer tests already do.

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.

🟢 Approval recommended

No active review issues block approval of the device-ID deserialization changes.

No open issues 🎉

What changed in this PR

This PR reduces allocation overhead in C++ device-ID deserialization, improving the metadata-reading path used by device lookups and scans.

Changes:

  • Remove placeholder device-ID construction and read segments directly into string buffers.
  • Reject invalid or oversized segment lengths before allocating, with tests for round trips, null/empty segments, and truncated input.
  • Add process IDs and counters to reader-test filenames to avoid collisions during parallel execution.
File Description
cpp/​test/​reader/​tsfile_reader_test.cc Make generated filenames unique across parallel test processes.
cpp/​test/​common/​device_id_test.cc Add deserialization round-trip and invalid-input coverage.
cpp/​src/​common/​tsfile_common.h Default-construct device IDs without an unused placeholder.
cpp/​src/​common/​allocator/​byte_stream.h Eliminate temporary segment buffers and validate lengths before allocation.

Review effort: Balanced

@ColinLeeo
ColinLeeo merged commit 0f65146 into develop Nov 13, 2026
50 checks passed
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