Skip to content

ui: Add bulk columnar decodeColumns() funciton to QueryResult and use it in track hot paths - #7277

Draft
stevegolton wants to merge 8 commits into
mainfrom
dev/sg/query-result-columnar-decode
Draft

ui: Add bulk columnar decodeColumns() funciton to QueryResult and use it in track hot paths#7277
stevegolton wants to merge 8 commits into
mainfrom
dev/sg/query-result-columnar-decode

Conversation

@stevegolton

@stevegolton stevegolton commented Aug 28, 2026

Copy link
Copy Markdown
Member

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 based TypedArrays. 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() and decodeColumns().

RUN_BENCH=1 ui/run-unittests -f 'QueryResultBenchmark' -n

@stevegolton
stevegolton requested a review from a team as a code owner August 28, 2026 14:37
@stevegolton
stevegolton marked this pull request as draft August 28, 2026 14:37
@stevegolton
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch from 05e3223 to ed62131 Compare August 28, 2026 14:38
…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
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch from ed62131 to 7a36976 Compare August 29, 2026 13:07
@stevegolton
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch from 7a36976 to d244907 Compare August 29, 2026 13:13
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.
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.

1 participant