Skip to content

ui: Bulk columnar decode for QueryResult - #7277

Open
stevegolton wants to merge 3 commits into
mainfrom
dev/sg/query-result-columnar-decode
Open

ui: Bulk columnar decode for QueryResult#7277
stevegolton wants to merge 3 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, most tracks ultimately require the data in columnar TypedArrays in order to load into WebGL buffers, so round tripping to a row object is wasteful.

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 as a set of columnar based TypedArrays. Building arrays directly allows certain shortcuts to be taken such as avoiding creating bigints and simply copying bytes.

decodeColumns() runs around 2-4x faster than iter(), depending on the row spec.

This patch also migrates SliceTrack, CounterTrack, GroupSummaryTrack, and CpuFreqTrack over to decodeColumns(). For compatibility, some row oriented work is still done in SliceTrack which leaves some performance on the table but changing this would involve changing track API, and this is out of scope of this PR.

Added a benchmark utility to compare the relative performance of iter() and decodeColumns(), which can be run using the following command:

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

Note: Changes in unrelated parts of the codebase are related to the fact that the type of the row spec has been decoupled from the type of the row itself. This was a neat trick but doesn't work for decodeColumns as we need to translate the spec to columnar TypedArrays - e.g. NUM -> Float64Array. Unfortunately Typescript cannot tell the difference between the NUM and NUM_NULL types when doing type based metaprogramming.

The spec types (NUM, STR, LONG, etc...) how now been changed to tagged types - so that each one is distinct and can be translated to the equivalent TypedArray properly.

@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
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

@stevegolton
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch 3 times, most recently from 63d7551 to 4a4e43a Compare August 31, 2026 09:35
@stevegolton
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch from 4a4e43a to 4314b80 Compare August 31, 2026 09:46
@stevegolton stevegolton changed the title ui: Add bulk columnar decodeColumns() funciton to QueryResult and use it in track hot paths ui: Bulk columnar decode for QueryResult Aug 31, 2026
@stevegolton
stevegolton marked this pull request as ready for review August 31, 2026 10:01
@stevegolton
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch from 7759661 to 3809763 Compare August 31, 2026 11:41
The spec constants (NUM, LONG, STR, ...) previously doubled as the value
types they decode to (number, bigint, string, ...), with synthetic
brands layered on top (e.g. NUM = 0 as number & {__brand: 'NUM'}). This
let callers accidentally treat the spec as the row: reading a spec
value where a decoded value was expected type-checked because the brand
was erased onto a real number/string/bigint.

Replace the constants with plain marker objects ({__brand: 'NUM'} as
const). The spec side is now formally SpecValue/SpecType, mapped to the
decoded side via DecodeIterType/IterResultFor. iter(), firstRow(),
maybeFirstRow() and materializeRows() all return decoded row types
(RowIterator<T> = RowIteratorBase & IterResultFor<T>), so reading a row
gives number/bigint/string, never a marker.

This surfaces and fixes the type abuse in consumers:
- SliceTrack: the row passed to callbacks (colorizer, sliceName,
  sliceSubtitle, tooltip, detailsPanel, fillRatio, slicePattern,
  OnSlice*) and stored in Slice/Instant.row is the decoded
  IterResultFor<T> rather than the spec type; RowSchema and the
  data-driven plugins (GpuByProcess, StackSamples, TraceInfoPage) use
  the spec constants in their schema types.
- SearchProvider.getSearchFilter() FilterExpression.columns is a schema
  (SpecValue), not decoded values.
- ChromeScrollJank utils.rows() returns decoded IterResultFor<R>[].
- AndroidInputLifecycle: InputLifecycleSpec is a spec (SpecType) and its
  fixed fields are read via it.get() casts like the dynamic stage
  fields.
- Dynamic schema builders (AndroidLongBatteryTracing, Smaps) use
  SpecValue; tests pass NUM instead of Number() to firstRow().
Add a decodeColumns() API to QueryResult that decodes whole columns at
once into typed arrays (Float64Array, BigInt64Array, etc.) instead of
row-by-row iteration, with the return type fully mapped from the spec
(ColumnarResultFor<T>) so callers get correctly-typed columns without
casts. Cell types are validated up-front per batch with the same checks
and errors as iter().

Internals:
- LONG columns are written straight into the BigInt64Array backing
  buffer via varint lo/hi int32 stores (readVarIntIntoInt32s), bit
  identical to the bigint path but skipping per-cell bigint allocation,
  with a runtime IS_LITTLE_ENDIAN fallback.
- decodeInt64Varint() is refactored to take a cursor, matching
  readVarIntAsNumber() and avoiding a double skip past the varint.
- Cell type validation is factored out of iter() into shared helpers
  (scanCellTypeMasks / isMaskCompatible / throwOnIncompatibleCell), one
  cheap byte scan per batch building per-column bitmasks with the
  compatibility check running once per column. throwOnIncompatibleCell
  computes the row from the within-batch offset, fixing a latent
  row-miscount in iter()'s error path for multi-batch results. NULL
  cells for non-nullable spec types now throw instead of decoding to
  NaN/0n, matching iter().

Includes a RUN_BENCH-gated micro-benchmark suite comparing iter() vs
decodeColumns() across four representative workloads (instant/slice
mixed, long-only, num-varint-only, num-float64-only; ~2.3-3.3x faster)
and unit tests for the columnar decode, error cases and the varint
cursor path.
… tracks

Convert the track rendering hot paths from per-row iter() loop to the
bulk decodeColumns() API:

- SliceTrack (getInstantBuffers/getSliceBuffers): bulk-decode the fixed
  columns (id/ts/count/depth/start/end/incomplete) plus the dataset
  schema columns in one pass, and build the renderer buffers with native
  Float64 -> Float32 typed-array conversion instead of per-row copies.
  The schema columns are read out of the decoded arrays per row to build
  the object handed to the getTitle/getSubtitle/getColor/pattern
  callbacks (keeps the chunked task loop as rows are still built
  per-row).
- CounterTrack: switch to decodeColumns() and drop the
  deferChunkedTask() machinery entirely, since the remaining min/max
  pass is a tight typed-array loop that no longer needs to yield.
- CpuFreqTrack: decode both the freq and idle results columnarly and
  copy the plain data columns with direct typed-array conversions
  (BigInt64Array reuse for timestamps, Uint32/Int8 for the rest),
  leaving the step-area computation in the loop.
- GroupSummaryTrack: decode the count/ts/dur/lane/utid columns into
  typed arrays and fill the derived buffer in a single pass.

Also use direct copies of decoded columns where possible and trim the
change-narration comments left over from the conversion.
@stevegolton
stevegolton force-pushed the dev/sg/query-result-columnar-decode branch from 3809763 to 7ca61ac Compare August 31, 2026 12:00
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.

2 participants