Skip to content

LogQL: index the label vector inside the | json flatten (#447) - #448

Merged
hshimizu merged 2 commits into
mainfrom
issue-447-json-label-index
Aug 13, 2026
Merged

LogQL: index the label vector inside the | json flatten (#447)#448
hshimizu merged 2 commits into
mainfrom
issue-447-json-label-index

Conversation

@hshimizu

Copy link
Copy Markdown
Contributor

Closes #447.

What was slow

A LogQL query using | json on a stream with many distinct labels re-scanned the label vector for every extracted field. The cost grew with the product of the two, so a wide stream with a wide | json spent most of its time re-walking a list it had already walked.

What changed

Commit 1 indexes the label vector once inside the flatten, so lookups are a hash probe rather than a scan. Commit 2 is unrelated to the index: the /metrics gzip conformance test was comparing two separate scrapes, which cannot be equal because counters move between them.

The measurement, and how to read it

At the 4 MB rung of the row-expansion ladder, one process per run:

before (n=4) after (n=60)
4 126 651 B 35.8–42.0 s 110 / 128 / 231 ms (min / median / max)

These are ranges from a shared build machine, not single figures. Wall-clock timings here carry contention from whatever else is building — during this work the 15-minute load average reached 33, and a run at load 5.58 gave 163.7 ms, inside the range. Pinning to one logical CPU made it worse (median 161 ms) while running at the lowest load of three batches, so load does not predict this figure at this scale. Every timing figure in the branch is published as a range with its run count and that condition; the superseded single figures are kept in place as the record.

Two figures are deliberately not ranges: F = 3.934 and 264 006 270 B. They are deterministic byte ratios, not wall times, and they are now pinned by equality assertions (F_MILLI, WORST_CASE_BYTES) rather than by the broad bounds that previously surrounded them — an earlier round found the docs described them as assertion-checked when nothing checked them.

Enumeration as a count, not a list

The doc that told a future re-derivation where these figures are quoted was a hand-written list, and it was wrong — it named three sites when there are nine occurrences of one figure and three of the other. A list of occurrences has no failure mode: the next edit that adds a mention silently falsifies it.

It is now a test that reads its own source and counts, asserting against named constants. Search needles are built at run time from the constants themselves, so the needle cannot drift from the value it tracks and the searcher cannot find itself. Breaks were observed in both directions — adding a mention (10 vs 9, 4 vs 3) and removing one (8 vs 9) — on the shipping source.

Review trail

Four rounds on the code, ending ## Review — code (round 4) — VERDICT: PASS on be94743. Rounds 2 and 3 each produced findings that were fixed; two findings in round 2 were withdrawn by ruling because the review brief was over-specified, recorded on the issue with the reasoning.

Workspace suite reconciles at 6430 passed / 25 skipped across fourteen crates. Both commits revert independently on disjoint file sets.

https://claude.ai/code/session_01B1xu1frZL9oF8MYRhkiMhv

hshimizu and others added 2 commits August 13, 2026 12:21
`insert_flattened` resolved every emitted key with `label_position`'s
`labels.iter().position(..)`, so a row emitting m labels cost m(m-1)/2
string comparisons. One uninstrumented `run_into` over a 4 126 651 B flat
object (m = 67 650) cost 34-42 s before this and 0.11-0.23 s after — the
`LADDER-WALL ... B=4126651` line of `CARGO_INCREMENTAL=0 cargo test -p
pulsus-read --test logql_row_expansion_ladder -- --include-ignored
--nocapture --test-threads=1`, 4 runs before and 60 after, one process
per run.

Both figures are RANGES because the measuring machine is shared: several
builds run on it concurrently, the 4 MiB rung's spread is about 2x, and
no single value inside either range is reproducible on demand. The gap
between the ranges is the finding — every point in one is two orders of
magnitude from every point in the other — and the file's docs now say so
at each place a wall time appears.

Adds `LabelIndex`: an open-addressed `Vec<u32>` holding positions, not
names, so every probe compares against `labels[i].0` — the same
comparison `label_position` makes, which is why the answers coincide by
construction — and the table allocates nothing per key. Built once per
flatten and dropped with it, because positions are stable only while the
vector is append-or-overwrite-in-place, which is exactly that walk's
shape. First occurrence wins, matching `position`.

Behaviour is unchanged; the differential test pins the equivalence over
duplicates, `_extracted` collisions, empty and non-ASCII names, and the
load-factor test pins the invariant that makes the probe loop terminate.

Two knock-on records in `tests/logql_row_expansion_ladder.rs`: `F` moves
3.583 -> 3.934 (worst case 240 451 059 B -> 264 006 270 B) on the index
table's doubling transient, with the pre-committed rule still selecting
the same branch; and `zz_the_full_precommitted_ladder` loses its
`#[ignore]`, which the file itself records as wall-time-only, because the
test now costs 1.2-2.0 s.

Those two byte figures are the only numbers in that file still written as
single values, and they earn it by being gated: `apply_the_rule` now
asserts them by EQUALITY against new `F_MILLI` and `WORST_CASE_BYTES`
constants, alongside the pre-existing bounds and branch-selection
assertions, which pass over a wide band and would not have caught a stale
figure. They are deterministic allocation counts under a pinned
toolchain, so an exact gate is available for them in a way it is not for
anything timed.

The prose repeating those two figures is gated too, by a count rather
than by a list. `the_quoted_figures_are_counted_so_a_new_quotation_cannot
_go_unchecked` reads the file's own source through `include_str!` and
counts occurrences of each figure's rendering, against `F_QUOTATIONS` and
`WORST_CASE_QUOTATIONS`; the needles are built from the two constants at
run time, so neither literal appears in the searcher and the count is of
quotations elsewhere. A hand-written inventory of sites was tried first
and was wrong on the day it was written — it has no failure mode, and the
edit that adds a mention silently falsifies it. Issue #241's closeout
comment is the one quotation outside any test's reach; it stays named in
prose because nothing in the tree can check it.

Claude-Session: https://claude.ai/code/session_01B1xu1frZL9oF8MYRhkiMhv
`assert_gzip_identity_metrics` scraped `/metrics` twice and required the
two decoded bodies to be equal after filtering out exactly one metric
name. `ops::metrics_handler` bridges live counters and gauges into every
scrape, so any second value that starts moving fails it; it flaked on PR
#446 on a change touching no metrics code.

The race was only ever in the body comparison, so only that changes: the
body equality now comes from ONE response gzipped locally. The filter is
deleted rather than left dead.

Both legs keep their own header cells, each asserted against its own
response and never against the other's — the identity leg's
Content-Encoding ABSENCE is a per-route cell, and the matrix exists to
catch a route that stops going through the global CompressionLayer, which
the other routes' passing legs cannot show. Headers of two scrapes do not
race.

Claude-Session: https://claude.ai/code/session_01B1xu1frZL9oF8MYRhkiMhv
@hshimizu
hshimizu merged commit 6847826 into main Aug 13, 2026
10 checks passed
@hshimizu
hshimizu deleted the issue-447-json-label-index branch August 13, 2026 11:50
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.

LogQL: | json is O(m²) per row — 41 s at 4 MB, and a fragile /metrics conformance test

1 participant