ui: Add bulk columnar decodeColumns() funciton to QueryResult and use it in track hot paths - #7277
Draft
stevegolton wants to merge 8 commits into
Draft
ui: Add bulk columnar decodeColumns() funciton to QueryResult and use it in track hot paths#7277stevegolton wants to merge 8 commits into
stevegolton wants to merge 8 commits into
Conversation
stevegolton
marked this pull request as draft
August 28, 2026 14:37
stevegolton
force-pushed
the
dev/sg/query-result-columnar-decode
branch
from
August 28, 2026 14:38
05e3223 to
ed62131
Compare
🎨 Perfetto UI Builds
|
…aths Add a decodeColumns() API to QueryResult that decodes entire columns at once into TypedArrays (Float64Array, BigInt64Array, etc.) instead of row-by-row iteration. The return type is fully mapped from the spec so callers get correctly-typed columns without casts. Switch counter_track, slice_track, and cpu_freq_track rendering hot paths to use the new columnar decode. Includes benchmarks and unit tests.
stevegolton
force-pushed
the
dev/sg/query-result-columnar-decode
branch
from
August 29, 2026 13:07
ed62131 to
7a36976
Compare
stevegolton
force-pushed
the
dev/sg/query-result-columnar-decode
branch
from
August 29, 2026 13:13
7a36976 to
d244907
Compare
decodeColumns() for LONG specs now overlays an Int32Array on the destination BigInt64Array's backing buffer and stores the varint's lo/hi 32-bit halves straight into it. The two int32 stores are bit identical to BigInt64Array[i] = asIntN(64, hi<<32 | lo), but skip the per-cell bigint allocation and shift/OR work. Guarded by a runtime IS_LITTLE_ENDIAN check that falls back to the old bigint path. Also fixes a bug where decodeColumns() with a partial spec crashed on VARINT/FLOAT64 cells of columns not in the spec (dest was undefined for ignored columns). Benchmarks (1M rows, median of 25): long-only (4 LONG cols) decodeColumns 38.7ms -> 25.0ms (-35%). num paths unchanged; an Int32 word-pair bit-copy for CELL_FLOAT64 was tried and measured 26% slower than the plain Float64Array store, so it was not kept. The benchmark harness gains num-varint-only, num-float64-only and long-only workloads alongside the existing mixed instant-track one.
benchSink is a JIT sink that is intentionally write-only, but tsc's noUnusedLocals flags 'declared but never read'. Wrap it in an object and assign to a property instead, which tsc does not flag.
decodeColumns() now rejects spec/wire mismatches with the same checks and errors as iter(). The validation is factored into three helpers shared with iter(): scanCellTypeMasks() (one cheap byte scan per batch building per-column cell-type bitmasks), isMaskCompatible() (the per-column bitmask check) and throwOnIncompatibleCell() (slow-path walk that throws naming the offending row and column; decodeColumns passes a row offset so multi-batch errors report global row numbers). Behavior change: NULL cells for non-nullable spec types (NUM/LONG/STR) now throw instead of silently storing NaN/0n, matching iter(). This also fixes latent corruption, e.g. a CELL_VARINT cell with a STR spec used to store a bigint into the string array. Benchmark effect (1M rows, medians): decodeColumns() +15..40% (4-5.5ns/row, the extra cell-types scan), still 2.4-3.1x faster than iter(). iter() itself got 18% faster on mixed-cell batches: the old trustMasks heuristic ORed the cell type values, so any batch mixing cell types (e.g. VARINT+STRING) skipped the fast bitmask path and did a full per-cell walk per column; the shared scan now tracks the max type so valid mixed batches take the fast path.
The slice and counter tracks now bulk-decode their columns with QueryResult.decodeColumns() and build the renderer buffers with native Float64 -> Float32 typed-array conversion, instead of copying values per-row inside a chunked task loop. In the counter track this lets us drop the deferChunkedTask() machinery entirely, since the remaining min/max pass is a tight typed-array loop that no longer needs to yield to the event loop. The slice track keeps its chunked task because it still builds a row object per row for the getTitle/getSubtitle/getColor callbacks. Also trims a stale note from the decodeColumns benchmark output.
Convert GroupSummaryTrack.fetchData() from row-by-row iter() to the bulk decodeColumns() API, copying the count/ts/lane/utid columns directly into the typed arrays and filling the derived columns in a single pass. This matches the slice and counter tracks and avoids the per-row iteration overhead.
Replace the row-by-row copies of the plain data columns (timestamps, minFreqKHz, maxFreqKHz, lastFreqKHz, lastIdleValues) with direct typed-array copies from the decodeColumns() buffers. The timestamps column reuses the decoded BigInt64Array directly and the integer columns use native typed-array conversion, so the per-row loop now only computes the step-area buffers. Also drop the now-redundant typecasts on the decoded columns.
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.
Currently all queries must be iterated out row by row which involves mutating the row object for every iteration. On heavy queries with many rows (such as tracks) this overhead can add up. In addition, the row object is wasted as most tracks simply load the values from each row into TypedArrays.
This patch introduces a new QueryResult API -
decodeColumns(spec)- which takes the same spec object as.iter(spec)but returns all rows in one go in columnar basedTypedArrays. Building arrays directly allows certain shortcuts to be taken such as avoiding creating bigints and just copying bytes.Compared to
iter(),decodeColumns()runs around 2-4x faster depending on the row spec.This patch also migrates SliceTrack and CounterTrack over to use decodeColumns() instead of iter(). Some per-row work is still done in each of these tracks which leaves some performance on the table but changing this would involve changing contracts and this is out of scope of this PR.
Added a benchmark utility to compare the relative performance of
iter()anddecodeColumns().RUN_BENCH=1 ui/run-unittests -f 'QueryResultBenchmark' -n