perf: single-pass tablet serialization — +52% write throughput, −61% latency - #16
Conversation
263a4e4 to
cadb09f
Compare
|
Rebased onto develop after #15 (DATE yyyyMMdd fix) merged. Conflict resolution notes: kept #15's |
HTHou
left a comment
There was a problem hiding this comment.
The core serializer change looks sound: I verified that all 152 unit tests and the build pass, both GitHub E2E checks are green, and 2,000 randomized valid tablets matched the legacy path byte-for-byte. I left two inline comments, including one blocking scope issue.
One additional documentation issue is outside the changed diff: Config.enableFastSerialization still says that the option enables serialization “with buffer pooling” and claims a 2–3x improvement. Since this PR removes the pool from the write path and deprecates it, please update that public JSDoc to describe the new implementation and use performance wording consistent with the evidence in this PR.
cadb09f to
c5f5522
Compare
|
Also addressed the |
HTHou
left a comment
There was a problem hiding this comment.
Re-reviewed at c5f5522. The previously raised issues have been addressed: the unrelated SpecKit/Claude scaffolding was removed, non-Buffer BLOB serialization no longer performs duplicate materialization and now has golden coverage, and the public fast-serialization documentation was updated.
I also re-ran the 153 unit tests and the build successfully; both current E2E checks are green. The remaining raw ArrayBuffer compatibility edge is outside the documented/recommended BLOB input and is non-blocking for this change.
LGTM.
…64 writes CPU profiling of the write path (insertTablet-heavy workload) showed the event loop 79.5% busy with serializeTabletValues at 26% self time and GC at 24.5% — almost entirely allocation churn in the serializer. This rewrites the fast (default) write path: - serializeTabletValues: new single-pass serializeTabletValuesFast() writes all columns AND null bitmaps into ONE exactly-sized buffer. Eliminates the rows->columns transpose (one array per column), the per-column boolean[] null bitmaps (built then discarded when a column has no nulls), the per-column intermediate buffers, the per-column 1-byte Buffer.from([flag]) allocations, and the trailing Buffer.concat (which re-copied the whole payload). Fixed-width sizes come from the schema; variable-width columns get one byteLength pre-scan. - INT64/TIMESTAMP writes: writeBigInt64BE(BigInt(v)) replaced with a sign-correct hi/lo 32-bit pair (hi = floor(v / 2^32), lo = v >>> 0), avoiding one BigInt allocation per value. BigInt inputs and non-safe integers still take the exact writeBigInt64BE path. - TEXT/STRING columns: single-entry encode cache — TAG columns repeat one string per tablet, so consecutive identical values reuse the encoded Buffer instead of re-encoding UTF-8 every row. - BufferPool: removed from the write path. It was acquire-only (zero release() calls anywhere in src/), i.e. a guaranteed 0% hit rate and pure overhead over Buffer.allocUnsafe. The class stays exported (public API) but is deprecated. Wire format is unchanged — existing serialization unit tests pass untouched, and new golden tests assert the fast path output equals the legacy path byte-for-byte for mixed-type tablets with nulls, all-null columns, bitmap-width edge cases (17 rows), BLOB/DATE, and negative / MIN_SAFE_INTEGER / BigInt int64 values. The legacy path (enableFastSerialization=false) is untouched.
c5f5522 to
5f4c1e0
Compare
|
Addressed the ArrayBuffer P2 in 5f4c1e0: |
Problem
CPU profiling of an insertTablet-heavy workload (100 devices × 20 sensors × 1000-row batches, 20 clients) showed the event loop 79.5% busy, with
serializeTabletValuesat 26% self time and GC at 24.5% — the write path was dominated by allocation churn, not I/O:serializeTabletValuestransposed rows→columns (values.map(row => row[col])— a fresh array per column), built aboolean[]null bitmap per column even when the column had no nulls, serialized each column into its own intermediate buffer, and finallyBuffer.concat-ed everything (a full payload re-copy).BigIntper value forwriteBigInt64BE.BufferPoolwas acquire-only (zerorelease()calls insrc/) — 0% hit rate, pure overhead overBuffer.allocUnsafe.serializeBitMapsallocated a 1-byteBuffer.from([flag])per column.Changes
serializeTabletValuesFast): pre-compute the exact payload size (fixed widths from schema; onebyteLengthscan for variable-width columns), allocate one buffer, write values column-by-column readingvalues[row][col]directly, and pack null bitmaps inline — flag byte stays 0 and no bitmap bytes are emitted when a column has no nulls. No transpose, no intermediate buffers, noBuffer.concat.hi = Math.floor(v / 2^32),lo = v >>> 0); BigInt inputs and non-safe integers still usewriteBigInt64BE.@deprecated).The legacy path (
enableFastSerialization: false) is unchanged.Results (IoTDB 2.0.10 standalone, 16-core box, DOUBLE workload, 10^9 points/run, interleaved A/B, 2 rounds)
+52% throughput, −61% average latency, −70% P99. Server-side
count(*)verified 100,000,000 rows for every run. Serializer self time dropped from 26% to 14.2% in--cpu-prof; the profiled process now shows 16% idle where it was previously saturated.Wire compatibility
MIN_SAFE_INTEGER/BigInt int64 values.AllDataTypes+TableModelDataTypessuites pass against a live IoTDB 2.0.10.Caveats
BufferPoolis deprecated but still exported; removal would be semver-major.