perf(telemetry): stop dictionary-encoding near-unique JSON columns - #255
Merged
Merged
Conversation
Parquet dictionary encoding keeps every distinct value of a column chunk for the whole row group, and ingest flushes a row group only at parquetRowGroupRows (50,000). On a column whose values repeat -- service names, HTTP methods, scope identifiers, the resource block shared by every span in a ResourceSpans -- that is a saving. On attributes_json, events_json, links_json, exemplars_json and exception_message it is not: those are near-unique per row by construction, so the dictionary is a second full copy of the payload, held until the row group closes. Measured against a 23.7 MiB synthetic batch of 50k spans, peak heap for one CommitBatch: 4.09x-4.92x payload with dictionary encoding, 2.79x-3.49x without. Those columns are now written plain. This does not make one commit cheap, it makes it cheaper. What remains above 1x is the conversion copy in makeSpanParquetRow, roughly 24 MiB of row-struct headers per 50k spans, and the writer's own buffers -- each tracked separately in the ingest-memory issue. Adds TestCommitBatchPeakHeapStaysNearPayloadSize, which samples HeapInuse during a commit and fails above a ratio placed in the gap between the two measured distributions, plus BenchmarkCommitBatch reporting peak heap so a regression appears as a number rather than as a production OOM kill. There was no in-tree memory benchmark before this. No migration: DuckDB reads either encoding, and parquet-go compares schema by node rather than by encoding, so existing files and merges are unaffected. Ingest files grow modestly; compaction re-encodes them anyway.
This was referenced Sep 20, 2026
vishr
added a commit
that referenced
this pull request
Sep 20, 2026
* test(telemetry): assert commit cost on allocation, not peak heap The commit-memory gate added in #255 is flaky and its threshold does not separate the two encodings. It failed CI at 3.83x against a 3.75 limit for code that measured 2.79x-3.49x locally, turning main red. Peak live heap was the wrong signal. It carries whatever slack the collector happened to be holding when the sampler looked, which varies with GOMAXPROCS and machine speed, and the two encodings' peak-heap distributions overlap: dictionary encoding measured 3.21x-3.56x and plain 1.45x-2.52x once the sampling was averaged over several commits. A threshold between those is a coin flip, and the 4.0 I first reached for would have passed the very regression the test exists to catch. Total bytes allocated does separate them, and barely varies: 10.68x-10.90x with dictionary encoding against 4.40x-5.69x without. It also answers the question the test is actually asking -- how many copies does one commit make -- rather than how much the collector was holding at one instant. The limit moves to 7.0, in the middle of that gap, and is verified in both directions: restoring the dictionary tags fails it at 10.6x. Peak heap is still measured and logged, because it is what the kernel kills on, and the figure is now the minimum across three commits so the log is comparable between runs. * test(telemetry): exclude the commit-memory gate from race builds The race detector instruments every allocation, which inflated total allocation from roughly 5x payload to 33x on unchanged code and failed the race target in CI. Peak heap is distorted the same way. These figures are only meaningful in an uninstrumented build, so the file is excluded from -race rather than given a second, meaningless threshold.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First item of #254.
Problem
Parquet dictionary encoding holds every distinct value of a column chunk for the whole row group, and ingest flushes a row group only at
parquetRowGroupRows(50,000,parquet.go:29). That is a saving on a column whose values repeat. It is a second full copy of the payload on a column whose values do not.Five columns are near-unique per row by construction and were dictionary-encoded anyway:
attributes_json(spans, logs, metrics)events_json,links_json(spans)exemplars_json(metrics)exception_message(spans)Columns that genuinely repeat —
resource_json,scope_*,http_*,hist_*, severity, service — keep their dictionaries.Measurement
Peak
HeapInuseduring oneCommitBatch, 50,000 synthetic spans, 23.7 MiB payload, sampled every 2ms with GOGC pinned low, repeated runs under-race:Roughly 40 MiB less live heap per in-flight commit. Four commit workers run concurrently with more batches queued behind them, so this multiplies across the process.
What this does not fix
It makes one commit cheaper, not cheap. What remains above 1x:
makeSpanParquetRow—string(r.ResourceJSON)and friends, while the originals stay live until durable ackEach is tracked separately in #254. Compaction's k-way merge, which is likely the larger allocator, is untouched here.
Test
TestCommitBatchPeakHeapStaysNearPayloadSizesamples heap during a commit and fails above a ratio placed in the gap between the two measured distributions above, rather than at a guessed number. It was written first and verified to fail against the dictionary encoding — restoring the old tags reproducesratio 4.92xand a red test.The constant is documented as a ratchet: lower it as each remaining item in #254 lands.
BenchmarkCommitBatchreportspeak-heap-MiBandpeak/payloadso a regression shows up as a number. There was no in-tree memory benchmark before this — the note inconfig.go:44-48points at an externalfanout-benchharness that is not in this checkout.Compatibility
None needed. DuckDB reads either encoding, and parquet-go compares schema by node rather than by encoding, so existing files and in-flight merges are unaffected. Ingest files grow modestly; compaction re-encodes them anyway.
just checkpasses.