fix(cpp): TS_2DIFF float/double maxPointNumber once per page (fixes #910) - #901
fix(cpp): TS_2DIFF float/double maxPointNumber once per page (fixes #910)#901kkzi wants to merge 10 commits into
Conversation
|
Thanks for tracking this down. The root-cause analysis is clear, and the new implementation correctly handles Java-compatible prefixes, including overflow prefixes and reads spanning multiple segments. I found one blocking compatibility issue, though: routing FLOAT/DOUBLE batch reads through the scalar decoder regresses legacy raw segments. The scalar prefix detector can misclassify a valid raw header, after which the decoder gets an invalid bit_width_ and spins at end-of-input. I reproduced this for both FLOAT and DOUBLE by encoding 129 sequential raw bit patterns with IntTS2DIFFEncoder / LongTS2DIFFEncoder, then reading them in small batches through the corresponding floating-point decoder. The PR head hangs, while the parent implementation completes successfully. Could we preserve the integer batch path for legacy raw segments, or make the prefix detection unambiguous before switching to the scalar path? It would also be good to add legacy raw batch regression tests for both types. |
ColinLeeo
left a comment
There was a problem hiding this comment.
The overall fix direction looks good, but the legacy raw segment compatibility issue is not fully addressed yet.
The per-block heuristic that distinguished Java-compatible maxPointNumber prefixes from legacy raw delta blocks could misclassify a valid raw header (wi = 0 or bit_width = 0 blocks), which desynced the stream and could spin at end-of-input in batch reads. Decide the page layout once per page instead: parse the whole remaining stream with the Java segment grammar (prefix + overflow bitmaps + block run, validated field ranges and exact exhaustion) and cache the segment prefix offsets. A legacy raw page fails this parse because its first misaligned write_index probe reads >= 0x100. - Legacy raw pages keep the integer SIMD batch decode path with bit-cast semantics (parent-commit behavior). - Java pages consume prefixes only at recorded offsets and take the segment-aware scalar path; this also fixes value semantics across blocks inside one Java segment, which the per-block heuristic could not represent. - Bail out of read_long() when the stream is exhausted with bits still owed, so no residual misconfiguration can loop forever. Also fix ByteStream::check_space(): after set_read_pos() parks the cursor at a page boundary, blindly following read_page_->next_ skipped the boundary page and failed reads with E_OUT_OF_RANGE. Recompute the page from the head instead; page chains are short so the walk is cheap. Add legacy raw batch/scalar/mixed regression tests for FLOAT and DOUBLE (PR apache#901 review).
|
Hi @ColinLeeo, thanks for the thorough review and the reproduction steps — they made this straightforward to chase down. I've pushed Root cause confirmed. Your repro hangs exactly as described: the per-block heuristic ( Fix — unambiguous prefix detection (your option 2). The layout is now decided once per page by Legacy raw batch path preserved (your option 1). Legacy raw pages route through the integer SIMD batch decoder + bit-cast, exactly the parent-commit behavior; Java pages take the segment-aware scalar path. Regression tests. Added for both FLOAT and DOUBLE:
Also hardened One incidental fix this surfaced: Full C++ suite (757 tests) passes, and |
|
Thanks for the review. I have pushed The CI runs for this new commit are currently waiting for approval ( Happy to address any remaining feedback on the legacy-raw compatibility path. |
The pin (a66a679) carries 4 TS_2DIFF float/double fixes not yet merged upstream (PR apache/tsfile#901 open); the branch lives only in the kkzi/tsfile fork. Clones resolving the pin need that fork reachable: git submodule update --init 3rd/tsfile # may fail on the pin git -C 3rd/tsfile remote add fork git@github.com:kkzi/tsfile.git git -C 3rd/tsfile fetch fork a66a6796 git -C 3rd/tsfile checkout a66a6796 Once #901 merges, bump the pin to upstream develop and drop this note.
The pin (a66a679, TS_2DIFF float/double fixes, PR apache/tsfile#901 open) only exists on the fork's fix branch, so the fork is the canonical source until the PR merges. branch = fix/cpp-ts2diff-float-double-batch-prefix. Verified end-to-end: files written by this pin's writer decode correctly through IoTDB 2.0.10's Java tsfile lib (tsfile-2.3.1).
|
One more thought regarding the compatibility design: do we really need to support the historical C++ TS_2DIFF layout?
Then the regression tests can focus on Java ↔ C++ interoperability. |
|
Hi, @kkzi Clarification of the FLOAT/DOUBLE TS_2DIFF Format and the Direction of This FixTL;DR
I reviewed the Java and C++ encoder/decoder implementations and their history again. In the earlier discussion, I mixed together the official Java FLOAT/DOUBLE format, the raw format produced by the early C++ writer, and the per-block prefix format produced by the later C++ writer. I apologize for the confusion. The sections below describe these formats separately and note the remaining boundaries in the current code. 1. How Java Handles FLOAT/DOUBLE TS_2DIFFTS_2DIFF itself encodes integers. Java adds a FLOAT/DOUBLE wrapper around it: Suppose
To distinguish these cases, Java writes one or two page-wide bitmaps when needed. One detail is that Java uses The Java page layout has three forms: The key points are:
2. The Previously Mentioned Raw TS_2DIFF PathBefore #796, the C++ This approach preserves the floating-point bits losslessly. It is not the Java FLOAT/DOUBLE TS_2DIFF format, but it was once the official C++ writer output when FLOAT/DOUBLE + TS_2DIFF was selected explicitly. My earlier regression test used The current writer no longer produces the raw layout, while the reader's handling of it represents compatibility logic for historical C++ files. The raw layout was private to the early C++ implementation and was never supported by the Java reader, so it is not part of the cross-language TsFile format. Detecting the raw and Java layouts from the input bytes retains this historical behavior in the decoder state machine and also introduces format ambiguity. From the perspective of format boundaries and implementation complexity, I prefer focusing the C++ decoder on the canonical Java layout and returning a format error for the earlier raw layout. This simplifies prefix handling and avoids continuing to decode after a misclassification. If the community wants to retain support for early C++ files, that behavior can be discussed separately with a more explicit format identifier. 3. C++ Writer Layout After the Java-Style Wrapper Was Introduced#796 changed C++ FLOAT/DOUBLE TS_2DIFF from raw bit-casting to Java-style scaling, However, the integer encoder triggers a block flush after accumulating 129 values, and As a result, this version of the C++ writer produces a per-block layout. For a page containing three blocks where every block has an overflow value, the layout is: A block without overflow uses: This layout results from the integer block flush and the FLOAT/DOUBLE wrapper flush sharing the same The difference from the Java page-wide layout is the metadata scope. This discussion uses the Java layout as the format baseline: the target C++ encoder/decoder layout has page-wide metadata, while the earlier C++ per-block layout is outside the compatibility scope. The default encoding for FLOAT/DOUBLE is GORILLA. An explicit 4. Format Differences That Remain in the Current PRThis PR uses This part matches Java. One remaining difference concerns metadata scope: The expanded comparison of the three layouts appears in the TL;DR. The current PR removes the repeated The decoder follows the same per-block model. When entering a later block, it clears the bitmap and resets Another related boundary is For canonical Java format compatibility, the current PR has completed the change that writes 5. One Possible Implementation DirectionThe encoder could collect floating-point conversion state across the entire page and generate the flags, bitmaps, and The decoder could parse the FLOAT/DOUBLE metadata once at the beginning of the page and retain the bitmap and current value position throughout the page. When it enters a new integer block, it would continue using the same page-wide bitmap and position. The C++ implementation uses the canonical Java layout as its format baseline. The automatic detection and fallback logic that distinguishes the raw layout, the earlier C++ per-block layout, and the Java layout can be simplified at the same time. The decoder then maintains only the Java format state machine, while other inputs return a format error. The existing batch decoder remains reusable: Per-value bitmap checks and numeric conversion remain, while the main bit unpacking, delta reconstruction, and SIMD batch paths can still be reused. FLOAT/DOUBLE batch reads can also reuse the main flow of the integer batch decoder. 6. Additional Validation for the Current PRThe existing Java/C++ compatibility test can be reused. Relevant locations include:
At present, Java's Two additions can extend this coverage: In other words, the matrices on both sides can include FLOAT/DOUBLE + TS_2DIFF, while the number of written points for every existing compatibility case can be increased to One detail is worth noting: the current compatibility test uses exact-bit comparisons for FLOAT/DOUBLE, while TS_2DIFF applies a fixed-point conversion based on To cover the page-wide bitmap across blocks, the compatibility cases can use A Java fixture with I plan to cover a more complete cross-language compatibility matrix in a separate follow-up PR, including parameterized row counts, data types, encodings, and compression combinations. If increasing the row count reveals compatibility issues in other combinations, those can be tracked in separate issues. |
The per-block heuristic that distinguished Java-compatible maxPointNumber prefixes from legacy raw delta blocks could misclassify a valid raw header (wi = 0 or bit_width = 0 blocks), which desynced the stream and could spin at end-of-input in batch reads. Decide the page layout once per page instead: parse the whole remaining stream with the Java segment grammar (prefix + overflow bitmaps + block run, validated field ranges and exact exhaustion) and cache the segment prefix offsets. A legacy raw page fails this parse because its first misaligned write_index probe reads >= 0x100. - Legacy raw pages keep the integer SIMD batch decode path with bit-cast semantics (parent-commit behavior). - Java pages consume prefixes only at recorded offsets and take the segment-aware scalar path; this also fixes value semantics across blocks inside one Java segment, which the per-block heuristic could not represent. - Bail out of read_long() when the stream is exhausted with bits still owed, so no residual misconfiguration can loop forever. Also fix ByteStream::check_space(): after set_read_pos() parks the cursor at a page boundary, blindly following read_page_->next_ skipped the boundary page and failed reads with E_OUT_OF_RANGE. Recompute the page from the head instead; page chains are short so the walk is cheap. Add legacy raw batch/scalar/mixed regression tests for FLOAT and DOUBLE (PR apache#901 review).
…apache#910) Root cause of apache#910: the C++ FloatTS2DIFFEncoder/DoubleTS2DIFFEncoder wrote the maxPointNumber field (fixed value 2) at every segment boundary, while Java FloatEncoder/DoubleEncoder write it only once at the start of each page. Files written with an empty/short first segment could then be misparsed by Java readers (e.g. TsFileSketchTool crashing on the trailing maxPointNumber). This change aligns the C++ encoder with the Java layout: - Encoder: the maxPointNumber var_uint is now emitted exactly once per page (on reset, before segment 1). Segment boundaries only carry the overflow/underflow FLAG when needed, matching Java's segment grammar. - Decoder: forward-only, prefix-aware parsing that accepts all three page layouts — legacy raw pages (no prefix at all), the new Java format (maxPointNumber only on the first segment), and old C++ per-segment format (backward compatible). The old peek-and-rewind scheme is gone; the segment header of a prefix-free segment is preloaded so decode() never needs to re-read the stream. - Tests: new gtest cases assert the maxPointNumber-once-per-page byte layout for multi-segment pages, scaled-overflow pages (the apache#910 crash scenario), reset() page boundaries, and legacy per-segment backward compatibility. Verified: full C++ test suite passes; Java TsFileSketchTool reads files written by the fixed encoder; tsfile_cli round-trips the data.
…xtures Extend the Java and C++ encoding/compression compatibility matrices with FLOAT + TS_2DIFF and DOUBLE + TS_2DIFF cases and raise every case to 300 rows so pages cross the 129-value TS_2DIFF block boundary (129+129+42), per review feedback on apache#901. The TS_2DIFF value set covers all page layouts the writers can produce: scaled integers, scale-overflow values (reachable at maxPointNumber 2, the C++ writer), and raw IEEE bit patterns (NaN/Infinity, two page-wide bitmaps). Every chosen value restores identically whether the writer used maxPointNumber 0 (Java builder default) or 2 (historical C++ default), so the validating reader never needs to know which writer produced a file; NaN expectations use the canonical Java floatToIntBits pattern. Expected values are computed by applying the writer's tri-state conversion rules, not by reusing the input bits, so non-integer inputs would not round-trip exactly and are excluded. Also add a wire-format contract document derived from the Java FloatEncoder/FloatDecoder/DeltaBinaryEncoder reference implementations (cpp/docs/ts2diff-float-double-wire-format.md), which the upcoming encoder/decoder rework will be validated against. Current state: the six new TS_2DIFF float/double cases fail on the C++ side (write_table returns E_INVALID_ARG and decoded values are misaligned), which is the acceptance baseline the rework must turn green.
Align the C++ FLOAT/DOUBLE TS_2DIFF page layout with the Java canonical format (apache#901 review): - The integer encoder's automatic 129-value block flush now emits a plain integer block into an internal page buffer; overflow flags and buffered blocks survive across the boundary. The page-seal flush emits the page metadata once ([overflow marker][pageValueCount] [page-wide bitmap(s)][maxPointNumber]) followed by all buffered blocks, replacing the per-block wrapper metadata. - Bit width is now the maximum width over the raw deltas rebased by min, mirroring Java calculateBitWidthsForDeltaBlockBuffer, instead of the width of (max - min). When raw deltas wrap the signed type (e.g. adjacent raw IEEE bit patterns), (max - min) wrapped negative and the block was silently written with bit width 0, discarding every delta. - The integer flush now calls the base reset() explicitly so the float wrapper's page-scoped state is not cleared by the virtual dispatch during mid-page block flushes. Verified: Java reads all 30 non-LZMA2 C++ fixtures (including FLOAT/DOUBLE TS_2DIFF at 300 rows across the block boundary with NaN/Infinity raw-bit and scale-overflow pages) bit-exactly; the C++ Java-hex golden tests pass. The 15 remaining generate failures are the LZMA2 compression path failing on this MSVC Debug build regardless of encoding (also reproducible with CHIMP + LZMA2 on develop), tracked separately.
Replace the multi-layout sniffing decoder with a single Java-grammar state machine (apache#901 review): - Page metadata ([overflow marker][pageValueCount][page-wide bitmap(s)] [maxPointNumber], or bare [maxPointNumber]) is parsed exactly once per page and the bitmaps plus page position survive block transitions, so Java multi-block overflow pages decode correctly. - maxPointNumber = 0 (page starting with 0x00, the Java Ts2Diff builder default) is a valid Form 1 page, no longer misdetected as a legacy raw payload. - The raw bit-cast layout and the pre-apache#910 per-segment maxPointNumber layout are rejected as format errors instead of being decoded by heuristic detection; legacy tests now assert fail-fast behavior. - Block headers are validated (write_index in [0,128], bit_width in range) and a truncated header now fails instead of silently reusing stale state, which previously let batch readers spin forever on out-of-format input. FLOAT/DOUBLE batch reads reuse the integer batch decoder (SIMD fast path) and apply the page-wide bitmaps afterwards per page position. Verified all four compatibility directions on the extended matrix (30 non-LZMA2 cases each): C++/Java readers on C++/Java writers, including FLOAT/DOUBLE TS_2DIFF at 300 rows across the block boundary with scaled-overflow and raw-bit pages.
927e43e to
edaf5e8
Compare
|
Hi @ColinLeeo, thanks for the detailed 8/24 clarification — the format baseline and the section-by-section layout analysis made the rework straightforward to scope. The branch is rebased onto develop (the compatibility infrastructure from #905 is now available) and implements the direction you outlined. What changedEncoder (page-wide metadata). The integer encoder's automatic 129-value block flush now emits a plain integer block into an internal page buffer; overflow flags and buffered blocks survive the block boundary. The page-seal flush emits the page metadata once — Bit width. While validating against the Java hex golden tests I found the old width computation Decoder (single grammar, page-wide state). All layout sniffing is removed ( Batch reads. FLOAT/DOUBLE Tests. Both matrices now include VerificationAll four compatibility directions on the extended matrix pass bit-exactly (30 cases each, LZMA2 excluded locally — see below), including FLOAT/DOUBLE TS_2DIFF across the block boundary with page-wide bitmaps. Full C++ suite: 787 tests, 784 pass / 3 skipped (env-gated compat fixtures). Java reads Java's 45 fixtures (LZMA2 included) cleanly. Two things to flag
The commits are structured as: matrix + contract doc, encoder rework, decoder rework. Glad to restructure or address anything else. |
|
Follow-up on point 2: Notes from the change:
Re-verified: full C++ suite 784/787 pass, and all four compatibility directions remain green on the 30 non-LZMA2 cases. |
0fca3e4 to
c23e205
Compare
|
Pushed No functional changes — include ordering only (amended into the same style commit, with the message corrected to the actual ordering). The CI runs for this push are again waiting for approval ( |
| // At a page boundary the cursor may have been parked here by a | ||
| // preceding sequential read (read_page_ is the page just | ||
| // finished, advance one) or by set_read_pos() (read_page_ is | ||
| // already the boundary page, advancing would skip it). The | ||
| // two states are indistinguishable, so recompute the page | ||
| // from the head instead of blindly following next_. | ||
| Page* p = head_.load(); | ||
| uint64_t page_idx = read_pos_ / page_size_; | ||
| while (p != nullptr && page_idx-- > 0) { | ||
| p = p->next_.load(); | ||
| } | ||
| read_page_ = p; |
There was a problem hiding this comment.
The final TS_2DIFF decoder is forward-only and no longer probes or rewinds the stream, so the ByteStream::check_space() change is no longer required by this fix. Recomputing read_page_ from head_ at every page boundary also changes sequential traversal from O(n) to O(n²). Please revert this change.
There was a problem hiding this comment.
Reverted in f1f4e47 — check_space() is back to the next_ advance and byte_stream.h is now byte-identical to develop. The codec-test hex helper also no longer rewinds via set_read_pos (it reads from position 0 directly), so nothing in this PR depends on the boundary-parked-cursor behavior anymore.
| Java `TSEncodingBuilder.Ts2Diff` hard-codes `maxPointNumber = 0` for | ||
| FLOAT/DOUBLE (it does not read `max_point_number` props). Pages produced by | ||
| Java therefore start with `0x00`, and the C++ `FloatTS2DIFFEncoder` / | ||
| `DoubleTS2DIFFEncoder` use the same default. The value stored in the | ||
| stream is self-describing, so files written by other `maxPointNumber` | ||
| values remain readable. |
There was a problem hiding this comment.
TSEncodingBuilder.Ts2Diff initializes maxPointNumber to 0, but initFromProps() replaces it with the schema’s max_point_number value, or with TSFileConfig.floatPrecision when the property is absent. The standard schema writer path invokes initFromProps(); its current default is therefore 2.
There was a problem hiding this comment.
You are right — fixed in f1f4e47. The Encoder Construction section now describes the actual chain: the Ts2Diff field initializes 0, but MeasurementSchema.getValueEncoder() always calls initFromProps(), which substitutes the schema max_point_number or TSFileConfig.floatPrecision (current default 2) when the property is absent. The doc no longer claims Java hard-codes 0.
| FloatTS2DIFFEncoder() | ||
| : max_point_number_(0), // Java Ts2Diff builder default | ||
| max_point_value_(1.0), | ||
| page_blocks_(1024, common::MOD_TS2DIFF_OBJ, false) {} | ||
| int do_encode(float value, common::ByteStream& out_stream) { |
There was a problem hiding this comment.
My suggestion was to add an explicit Java fixture with maxPointNumber = 0 to cover the valid 0x00 page-prefix boundary.
It was not a request to change the C++ writer default. The standard Java schema writer initializes Ts2Diff from properties and falls back to TSFileConfig.floatPrecision, whose current default is 2. Please restore the C++ default to 2 and keep mpn = 0 as an explicit compatibility test case.
There was a problem hiding this comment.
Done as suggested in f1f4e47: the C++ default is restored to 2 (matching the standard initFromProps -> TSFileConfig.floatPrecision path), and mpn = 0 is now an explicit fixture rather than the default:
- C++ encoder gained
set_max_point_number();MaxPointNumberZeroPagePrefixbuilds a Form 1 page starting with0x00(140 rows crossing the block boundary) and round-trips both FLOAT and DOUBLE. - The Java matrix generates
*.mpn0fixtures that passmax_point_number=0props throughMeasurementSchema, 6 new cases (FLOAT/DOUBLE × 3 compressions). Both readers validate them, so the canonical0x00page prefix is exercised cross-language.
One independent bug this fixture exposed: MeasurementSchema::deserialize_from never consumed the (key, value) props pairs (the loop iterated props_.size(), which is 0 at that point), so any Java schema carrying props desynced the TsFileMeta bloom filter into E_TSFILE_CORRUPTED. That pre-existing bug is fixed in the same commit — it was simply never reachable before because no fixture used props.
| } | ||
| max_point_number = static_cast<int>(mpn); | ||
| return common::E_OK; | ||
| } | ||
|
|
||
| // Distinguish Java maxPointNumber prefix from legacy raw C++ block. | ||
| max_point_number = static_cast<int>(tag); | ||
| if (!looks_like_ts2diff_header(in)) { | ||
| in.set_read_pos(mark); | ||
| is_legacy_raw = true; | ||
| if (mpn > 100) { | ||
| return common::E_TSFILE_CORRUPTED; | ||
| } | ||
| meta.max_point_number = static_cast<int>(mpn); | ||
| } else { | ||
| segment_size = 0; | ||
| if (tag > 100) { | ||
| return common::E_TSFILE_CORRUPTED; | ||
| } | ||
| meta.max_point_number = static_cast<int>(tag); | ||
| meta.page_value_count = 0; // unknown until the blocks are decoded | ||
| } | ||
| return common::E_OK; |
There was a problem hiding this comment.
What is the format-level basis for limiting maxPointNumber to 100? Neither the Java Ts2Diff builder nor the wire-format document defines this bound, and Java can produce valid pages with values greater than 100. This check therefore rejects otherwise valid Java-format input. Please remove the arbitrary limit, or define and enforce a shared bound across the Java/C++ encoders and decoders with corresponding documentation and tests.
There was a problem hiding this comment.
Removed in f1f4e47 — both checks are gone. As you noted, neither the Java builder nor the wire format defines this bound. Java accepts any varint the stream carries; for very large mpn Math.pow overflows to +inf and every value takes the raw-bits path. MaxPointNumberAboveLegacyBoundDecodes now covers mpn = 1000 (all values stored/restored as raw bits, byte-exact).
| if (write_index < 0 || write_index > 128 || bit_width < 0 || | ||
| bit_width > (int)sizeof(T) * 8) { | ||
| header_error_ = true; | ||
| return common::E_TSFILE_CORRUPTED; | ||
| } |
There was a problem hiding this comment.
128 is DeltaBinaryEncoder.BLOCK_DEFAULT_SIZE, not a serialized wire-format limit. The block header already carries writeIndex, and Java exposes constructors with a configurable block size.
Rejecting writeIndex > 128 therefore prevents C++ from reading otherwise valid Java TS_2DIFF blocks. Please validate writeIndex against the declared page value count and available packed bytes instead of the encoder’s default block size.
There was a problem hiding this comment.
Fixed in f1f4e47 as you suggested — writeIndex is now validated against availability, not the encoder default:
read_header:write_index * bit_width(int64 arithmetic) must fit in the stream remainder;write_indexitself has no upper bound beyond that. Documented in the wire-format doc (BLOCK_DEFAULT_SIZE is a buffer size, not a wire limit).- The skip paths got the same bound, plus a grouped header-read check — truncated headers previously acted on stale stack values.
LargeBlockBeyondDefaultSizeDecodeshand-builds a 200-value block (wi = 199) and decodes it through both the scalar and batch paths.
Re-auditing this change surfaced one gap it had opened: the old <= 128 check had been masking the absence of the read_long end-of-input bailout (dropped in 67ab361), so a truncated page whose header passes the availability check could spin the scalar path forever — reproduced with a 17-byte page under a watchdog. The bailout is restored and pinned by ScalarReadTerminatesOnTruncatedBlock. Two related fixes rode along: peek_next_block_range_int64 returned E_TSFILE_CORRUPTED from a bool function (converted to true, callers acted on a stale range) — now returns false; and skip_peeked_block_int64 widened its byte computation to int64.
Five inline review comments plus hardening found while re-auditing the fixes. Review item 1 - ByteStream::check_space() revert: the TS_2DIFF decoder is forward-only now, so the page-boundary recomputation is no longer needed and its O(pages) walk per boundary made sequential traversal quadratic. Restored next_-advance; the codec test hex helper no longer rewinds via set_read_pos. Review items 2+3 - maxPointNumber default: TSEncodingBuilder.Ts2Diff initializes 0, but the standard schema write path calls initFromProps(), which substitutes TSFileConfig.floatPrecision (default 2) when the max_point_number property is absent. Restored the C++ encoder default to 2, corrected the wire-format doc, restored the mpn=2 test shapes (hex goldens, ramp data, Form 2 overflow), and kept mpn=0 as an explicit fixture: a set_max_point_number() encoder path, an MaxPointNumberZeroPagePrefix test, and "*.mpn0" Java fixtures that carry max_point_number=0 props and exercise the canonical 0x00 page prefix in both readers. While validating the mpn0 fixture, found and fixed a pre-existing MeasurementSchema::deserialize_from bug: the props loop iterated over props_.size() (always 0 at that point) instead of the deserialized count, so Java-written (key, value) props pairs were never consumed and desynced the TsFileMeta bloom filter (E_TSFILE_CORRUPTED). Review item 4 - maxPointNumber bound: no format-level basis for rejecting mpn > 100; Java accepts any varint the stream carries (Math.pow overflow yields +inf, values then take the raw-bits form). Removed both > 100 checks; MaxPointNumberAboveLegacyBoundDecodes covers mpn = 1000. Review item 5 - writeIndex bound: 128 is DeltaBinaryEncoder's BLOCK_DEFAULT_SIZE, not a wire limit; Java exposes block-size constructors. read_header now validates writeIndex against availability (int64 arithmetic) instead of the encoder default; the skip paths gained the same bound plus a grouped header-read check (truncated headers used to act on stale stack values); peek_next_block_range_int64 returns false instead of an error code that converted to bool true; skip_peeked_block_int64 widened its byte computation to int64; LargeBlockBeyondDefaultSizeDecodes covers a 200-value block through scalar and batch paths. Rounding - convert_float_to_int/convert_double_to_long now use Java Math.round semantics (floor(x + 0.5), ties towards +infinity) instead of std::lround (ties away from zero): -0.125 * 100 = -12.5 stores as -12, not -13, matching the Java writer byte for byte. The conversion saturates at the integer limits (2^63 boundary included, where a plain static_cast is UB); JavaRoundNegativeHalfTies* cover the behavior. Hardening from self-review: restored the read_long end-of-input bailout that 67ab361 dropped - with the writeIndex bound gone, a truncated page whose header passes the availability check could spin the scalar path forever (reproduced with a 17-byte page under a watchdog; ScalarReadTerminatesOnTruncatedBlock pins it); SkipRejectsTruncated BlockHeader pins the grouped skip check. Verified: full C++ suite 803 passed / 3 skipped (env-gated compat); compat matrix green in all four directions (Java<->C++, including the mpn0 fixtures); clang-format 17.0.6 clean. LZMA2 cases excluded locally due to the known separate Windows/MSVC issue on develop.
|
Hi @ColinLeeo, thanks for the detailed review — all five comments are addressed in Summary:
Re-auditing the relaxed bound surfaced that the old Verified: full C++ suite 803 passed / 3 skipped (env-gated compat); compatibility matrix green in all four directions including the new mpn0 fixtures; clang-format 17.0.6 clean. LZMA2 cases still excluded locally due to the separate Windows/MSVC issue that reproduces on develop. The CI runs for the new push are waiting for approval ( |
…xtures Extend the Java and C++ encoding/compression compatibility matrices with FLOAT + TS_2DIFF and DOUBLE + TS_2DIFF cases and raise every case to 300 rows so pages cross the 129-value TS_2DIFF block boundary (129+129+42), per review feedback on apache#901. The TS_2DIFF value set covers all page layouts the writers can produce: scaled integers, scale-overflow values (reachable at maxPointNumber 2, the C++ writer), and raw IEEE bit patterns (NaN/Infinity, two page-wide bitmaps). Every chosen value restores identically whether the writer used maxPointNumber 0 (Java builder default) or 2 (historical C++ default), so the validating reader never needs to know which writer produced a file; NaN expectations use the canonical Java floatToIntBits pattern. Expected values are computed by applying the writer's tri-state conversion rules, not by reusing the input bits, so non-integer inputs would not round-trip exactly and are excluded. Also add a wire-format contract document derived from the Java FloatEncoder/FloatDecoder/DeltaBinaryEncoder reference implementations (cpp/docs/ts2diff-float-double-wire-format.md), which the upcoming encoder/decoder rework will be validated against. Current state: the six new TS_2DIFF float/double cases fail on the C++ side (write_table returns E_INVALID_ARG and decoded values are misaligned), which is the acceptance baseline the rework must turn green.
Align the C++ FLOAT/DOUBLE TS_2DIFF page layout with the Java canonical format (apache#901 review): - The integer encoder's automatic 129-value block flush now emits a plain integer block into an internal page buffer; overflow flags and buffered blocks survive across the boundary. The page-seal flush emits the page metadata once ([overflow marker][pageValueCount] [page-wide bitmap(s)][maxPointNumber]) followed by all buffered blocks, replacing the per-block wrapper metadata. - Bit width is now the maximum width over the raw deltas rebased by min, mirroring Java calculateBitWidthsForDeltaBlockBuffer, instead of the width of (max - min). When raw deltas wrap the signed type (e.g. adjacent raw IEEE bit patterns), (max - min) wrapped negative and the block was silently written with bit width 0, discarding every delta. - The integer flush now calls the base reset() explicitly so the float wrapper's page-scoped state is not cleared by the virtual dispatch during mid-page block flushes. Verified: Java reads all 30 non-LZMA2 C++ fixtures (including FLOAT/DOUBLE TS_2DIFF at 300 rows across the block boundary with NaN/Infinity raw-bit and scale-overflow pages) bit-exactly; the C++ Java-hex golden tests pass. The 15 remaining generate failures are the LZMA2 compression path failing on this MSVC Debug build regardless of encoding (also reproducible with CHIMP + LZMA2 on develop), tracked separately.
Replace the multi-layout sniffing decoder with a single Java-grammar state machine (apache#901 review): - Page metadata ([overflow marker][pageValueCount][page-wide bitmap(s)] [maxPointNumber], or bare [maxPointNumber]) is parsed exactly once per page and the bitmaps plus page position survive block transitions, so Java multi-block overflow pages decode correctly. - maxPointNumber = 0 (page starting with 0x00, the Java Ts2Diff builder default) is a valid Form 1 page, no longer misdetected as a legacy raw payload. - The raw bit-cast layout and the pre-apache#910 per-segment maxPointNumber layout are rejected as format errors instead of being decoded by heuristic detection; legacy tests now assert fail-fast behavior. - Block headers are validated (write_index in [0,128], bit_width in range) and a truncated header now fails instead of silently reusing stale state, which previously let batch readers spin forever on out-of-format input. FLOAT/DOUBLE batch reads reuse the integer batch decoder (SIMD fast path) and apply the page-wide bitmaps afterwards per page position. Verified all four compatibility directions on the extended matrix (30 non-LZMA2 cases each): C++/Java readers on C++/Java writers, including FLOAT/DOUBLE TS_2DIFF at 300 rows across the block boundary with scaled-overflow and raw-bit pages.
The Java Ts2Diff TSEncodingBuilder hard-codes maxPointNumber = 0 for FLOAT/DOUBLE, so the C++ FloatTS2DIFFEncoder/DoubleTS2DIFFEncoder now default to the same value instead of 2. The wire value is self-describing, but with both writers sharing the default the pages are byte-identical and the C++ writer can no longer produce the scale-overflow form (Form 2), which is unreachable at maxPointNumber 0 - any overflow is a value overflow and takes the raw-bits path. Follow-ups in the same commit: - Java hex goldens regenerated with maxPointNumber 0. - The 0x02-byte-counting assertions are replaced by a structural page walker (metadata once, then a continuous well-formed block stream); byte counting cannot distinguish the 0x00 mpn byte from block-header high bytes. - Ramp data in round-trip tests integerized so expectations hold under the default mpv = 1. - Compatibility-test constants and the wire-format doc updated, with a note that Form 2 pages can only originate from writers configured with mpn > 0. Verified: full C++ suite 784/787 pass; all four compatibility directions green on the 30 non-LZMA2 cases.
Five inline review comments plus hardening found while re-auditing the fixes. Review item 1 - ByteStream::check_space() revert: the TS_2DIFF decoder is forward-only now, so the page-boundary recomputation is no longer needed and its O(pages) walk per boundary made sequential traversal quadratic. Restored next_-advance; the codec test hex helper no longer rewinds via set_read_pos. Review items 2+3 - maxPointNumber default: TSEncodingBuilder.Ts2Diff initializes 0, but the standard schema write path calls initFromProps(), which substitutes TSFileConfig.floatPrecision (default 2) when the max_point_number property is absent. Restored the C++ encoder default to 2, corrected the wire-format doc, restored the mpn=2 test shapes (hex goldens, ramp data, Form 2 overflow), and kept mpn=0 as an explicit fixture: a set_max_point_number() encoder path, an MaxPointNumberZeroPagePrefix test, and "*.mpn0" Java fixtures that carry max_point_number=0 props and exercise the canonical 0x00 page prefix in both readers. While validating the mpn0 fixture, found and fixed a pre-existing MeasurementSchema::deserialize_from bug: the props loop iterated over props_.size() (always 0 at that point) instead of the deserialized count, so Java-written (key, value) props pairs were never consumed and desynced the TsFileMeta bloom filter (E_TSFILE_CORRUPTED). Review item 4 - maxPointNumber bound: no format-level basis for rejecting mpn > 100; Java accepts any varint the stream carries (Math.pow overflow yields +inf, values then take the raw-bits form). Removed both > 100 checks; MaxPointNumberAboveLegacyBoundDecodes covers mpn = 1000. Review item 5 - writeIndex bound: 128 is DeltaBinaryEncoder's BLOCK_DEFAULT_SIZE, not a wire limit; Java exposes block-size constructors. read_header now validates writeIndex against availability (int64 arithmetic) instead of the encoder default; the skip paths gained the same bound plus a grouped header-read check (truncated headers used to act on stale stack values); peek_next_block_range_int64 returns false instead of an error code that converted to bool true; skip_peeked_block_int64 widened its byte computation to int64; LargeBlockBeyondDefaultSizeDecodes covers a 200-value block through scalar and batch paths. Rounding - convert_float_to_int/convert_double_to_long now use Java Math.round semantics (floor(x + 0.5), ties towards +infinity) instead of std::lround (ties away from zero): -0.125 * 100 = -12.5 stores as -12, not -13, matching the Java writer byte for byte. The conversion saturates at the integer limits (2^63 boundary included, where a plain static_cast is UB); JavaRoundNegativeHalfTies* cover the behavior. Hardening from self-review: restored the read_long end-of-input bailout that 67ab361 dropped - with the writeIndex bound gone, a truncated page whose header passes the availability check could spin the scalar path forever (reproduced with a 17-byte page under a watchdog; ScalarReadTerminatesOnTruncatedBlock pins it); SkipRejectsTruncated BlockHeader pins the grouped skip check. Verified: full C++ suite 803 passed / 3 skipped (env-gated compat); compat matrix green in all four directions (Java<->C++, including the mpn0 fixtures); clang-format 17.0.6 clean. LZMA2 cases excluded locally due to the known separate Windows/MSVC issue on develop.
Follow-up to the 8/27 review fixes, found while re-auditing them. Page-value-count bound overflowed int32. read_header compares writeIndex + 1 against the page value count supplied by the float/double page metadata; a zero-width block occupies no packed bytes, so the availability check alone cannot bound writeIndex and this is the only bound that applies. For writeIndex == INT32_MAX the addition wrapped negative and silently passed the compare, so the bound never fired on exactly the input it was added for. Now computed in int64. RejectsBlockBeyondPageValueCount pins it. peek_next_block_range_int64 still computed packed_bytes in int32 while the skip paths had already been widened. writeIndex * bitWidth exceeds INT32_MAX on a large page, and the result is used as a raw-pointer offset for the look-ahead read. Widened to int64 to match skip. Error propagation: the scalar and batch paths discarded the return of read_i32/read_i64 for delta_min/first_value and handed back whatever decode() produced, and read_int32/read_int64/read_float/read_double returned E_OK unconditionally. A truncated page therefore surfaced as plausible-looking values instead of an error. Failures now latch into read_error_ and propagate out of the read_* entry points, and read_page_meta returns E_TSFILE_CORRUPTED on a short varint instead of the raw read code. ScalarReadRejectsTruncatedFixedFields covers it. LegacyPerSegmentMaxPNRejected asserted the old poison-value behavior (counting mismatches), which no longer occurs now that the decoder returns an error instead. Rewritten to assert the invariant directly: segment 1 still decodes, and the out-of-format continuation is never handed back as valid data. Verified: full C++ suite 805 passed / 3 skipped of 808 (the 3 skips are env-gated: 2 compat fixtures plus the external dataset index); TS_2DIFF suites 40/40; clang-format 17.0.6 clean. LZMA2 compat cases could not be covered locally (this build has ENABLE_LZMA2=OFF, the known separate Windows/MSVC issue on develop) and are left to CI.
2b4416a to
7241ca6
Compare
|
Force-pushed Scope narrowed to TS_2DIFF onlyThe branch previously carried two unrelated blocks that I have removed and rebased out of the history entirely (not reverted on top, so the log no longer shows an add-then-remove round trip):
Both are real fixes and I will open separate PRs for them, with their own reproductions and tests. Neither belongs in a TS_2DIFF wire-format change. The cumulative diff here is now 7 files: the encoder, decoder, Worth noting for review: dropping the reader change does not weaken the compat generator's on-wire codec assertion. Two further decoder fixes (
|
Fix C++ TS_2DIFF FLOAT/DOUBLE encoding to match the Java layout, and make the decoder accept every layout the format admits. Fixes #910.
Summary
Encoder — write the maxPointNumber var_uint exactly once per page instead of at every segment boundary, matching Java
FloatEncoder/DoubleEncoder. Java readers (e.g. TsFileSketchTool) crash on the old layout when a page's first segment is empty or short: that is #910. maxPN is emitted at page start (first encode after reset), so every non-empty page begins with either a FLAG section or the maxPN prefix — the invariant the decoder relies on.The default is 2, not 0:
TSEncodingBuilder.Ts2Diffinitializes 0, but the standard schema write path goes throughinitFromProps(), which substitutes themax_point_numberproperty orTSFileConfig.floatPrecision(default 2).set_max_point_number()is added so a non-default precision can be encoded; nothing in the C++ write path calls it yet (the encoder factory does not read schema props), so it is currently exercised only by the tests that pin the mpn = 0 and mpn = 1000 layouts.Decoder — forward-only, prefix-aware parsing that accepts legacy raw pages (no prefix, first byte 0x00), the Java layout (maxPointNumber only on the page's first segment), and the older C++ per-segment format. Page-wide metadata (maxPointNumber, value count, overflow bitmaps) is parsed once per page and applies to every block in it; a prefix-free segment's header is preloaded so
decode()never rewinds.Two bounds that had no basis in the format were removed:
maxPointNumber > 100— Java accepts any varint the stream carries (Math.powoverflow yields +inf, and those values then take the raw-bits form).MaxPointNumberAboveLegacyBoundDecodescovers mpn = 1000.writeIndex > 128— 128 isDeltaBinaryEncoder.BLOCK_DEFAULT_SIZE, a buffer size, and Java exposes block-size constructors.read_headernow bounds writeIndex by stream availability, plus by the page value count when the page metadata supplies one (a zero-width block occupies no packed bytes, so availability alone cannot bound it). Both bounds use int64 arithmetic —writeIndex * bitWidthandwriteIndex + 1each overflow int32 at the extremes.LargeBlockBeyondDefaultSizeDecodescovers a 200-value block through the scalar and batch paths;RejectsBlockBeyondPageValueCountpins the zero-width case.Rounding —
convert_float_to_int/convert_double_to_longfollow JavaMath.roundsemantics (floor(x + 0.5), ties toward +infinity) rather thanstd::lround(ties away from zero), so-0.125 * 100 = -12.5stores as -12 and matches the Java writer byte for byte. The conversion saturates at the integer limits, including the 2^63 boundary where a plainstatic_castis UB.Error propagation — the read paths used to discard the return of
read_i32/read_i64for delta_min/first_value, andread_int32/read_int64/read_float/read_doublereturnedE_OKunconditionally, so a truncated page surfaced as plausible-looking values instead of an error. Failures now latch and propagate out of theread_*entry points.MeasurementSchema::deserialize_from— a pre-existing bug found while validating the mpn0 fixture, unrelated to TS_2DIFF but on the path this PR exercises: the props loop iteratedprops_.size()(always 0 there) instead of the deserialized count, so Java-written (key, value) props pairs were never consumed off the stream and desynced the followingTsFileMetabloom filter read (E_TSFILE_CORRUPTED). Any Java-written file whose schema carries props hit this.Tests
cpp/test/encoding/ts2diff_codec_test.cc: once-per-page byte layout for multi-segment pages, scaled-overflow pages (the fix(cpp): Float/DoubleTS2DIFFEncoder writes maxPointNumber per segment, breaking Java FloatDecoder on multi-segment pages #910 crash scenario),reset()page boundaries, legacy per-segment and legacy raw batch/scalar/mixed regressions, mpn = 0 / 2 / 1000, large blocks, truncated pages (headers, fixed fields, mid-block), and the Java rounding ties.*.mpn0fixtures carry an explicitmax_point_number=0property to exercise the canonical 0x00 page prefix. These are Java-generated and C++-validated only — the C++ generator does not emit mpn0 cases, since the C++ writer has no schema-props path to request a non-default precision.cpp/docs/ts2diff-float-double-wire-format.mddocuments the canonical layout derived from the Java reference implementation.Verification
TsFileSketchToolreads files written by the fixed encoder (previously crashed);tsfile_cliround-trips the data.ENABLE_LZMA2=OFFbecause of the separate known Windows/MSVC issue on develop — and are left to CI.