Repository navigation
perf: the first view of a detected_level volume over | json runs cheaper probes - #764
Conversation
… of phrases The keys probe ran two regexps that scan the rest of the line after a level key over every line of a window that holds one, which is every line of a body-only window: 320 CPU-ms of 1h of rows, the heaviest probe of the first view of a window. Only a line holding two level keys can match them, so they now run behind a filter that passes the lines in which one key name occurs twice (a sequence of the phrase after itself) or two of the names occur, answered from VictoriaLogs' token index: 50 CPU-ms. The gate is a superset of the patterns, never a second decision; the probe's verdict is the patterns'.
…out the gate The gate adds 2.5 KB to the keys probe, so a selector of 11.8 KB to 14.3 KB, which fit VictoriaLogs' 16 KiB query length before, would have had its probe refused. The gate only narrows what the patterns run on, so the text without it gives the same verdict.
…d the chain A plan that groups by a label and reads detected_level asks twice on rows with no stored level, and the verdict of its label probes (main's partial-parse probe among them) was remembered under a key that told the two asks apart, so the same query ran twice in a first view. The label probes and the level probes are remembered apart now: the chain takes the verdict of the label probes from the stored-level query and asks only for the level probes. The verdict is risky if either is.
Its probes read nothing else, and the shared helper pushes the lines to Loki and flushes it, which other tests of the suite do not expect of a test that runs before them.
…n before #760, in both measurement orders
⚪ Performance A/B: no change beyond noise77 shape×range — 77 ⚪ same · results vs Loki: 62 same, 0 new difference(s), 15 pre-existing Every shape is within noise of the base build (±30% and 100 ms). 77 unchanged shape×range (within noise)
Cold (first-run) timings — what a user opening a panel sees
Why these shapes
base |
✅ Visual smoke: passedBase
Gate: a base-vs-PR data difference passes when the PR is closer to Loki, and fails when the PR diverges from Loki or Loki cannot decide it (at a range Loki does not hold, a difference passes when the same page proved it closer to Loki at a shorter range; otherwise label the pull request Core set montages (5): base | PR | Lokidd-fields-1h dd-landing-1h dd-service-logs-1h explore-logs-plain-1h explore-metric-plain-1h All montages of this run: |
PR Quality ReportCompared against base branch Coverage and tests
Compatibility
Performance smokeLower CPU cost (
State
|
…word The one occurrence of log.level holds the word level, so the pair of level and log.level admitted every line with that single key and the two-keys regexps ran over it. A line with both keys holds the word twice and passes the sequence of level after level; the gate stays a superset of the patterns.
… refuses the longer text An operator may have lowered -search.maxQueryLen below the gated probe, which is 2.5 KB longer than the probe was. VictoriaLogs' 400 for a query that is too long is told apart from other refusals, and the probe goes out once more without the gate, the same verdict. The edge of the 16 KiB default is pinned with bases of exactly the longest that fits and one byte more.
…filters A window found clean for one base, or for one set of filters that reject unparsed lines, must not clear another base or set in the same tenant and window that holds a risky line: it goes to the raw-row evaluator with the answer it gets asked alone. Both scopes were unpinned: a key without the base, or without the filters, passed every test.
…tarted fresh before each request
…n and this branch started fresh before each request
…mples and say what the probes do on a refusal





Summary
Follow-up to #760. The first view of a window (nothing remembered; probed again
every five minutes) of
sum by (detected_level)andsum by (level, detected_level)over| json, on rows with no stored level, ran thestored-level query, the level probes and the presence probe before the chain,
and was several times slower than main before #760. Three cuts, none of which
changes an answer:
of phrases (
logsqlTwiceFilter); a base too long for the gated text, or aVictoriaLogs that refuses it as too long, gets the probe without the gate.
from the level probes, so the chain takes their verdict from the stored-level
query instead of asking the same question again.
Explore's JSON volume on the harness's backfill without the level stream label, a
proxy started fresh before each request, VictoriaLogs capped at 2 CPUs, median
[min-max] of 7 (1h) and 5 (24h) requests, VictoriaLogs cgroup CPU in brackets.
Every sample is committed:
bench/ab/results/oneoff/2026-10-16-dlfirstview-coldrestart.json,produced by
python3 bench/ab/cold_first_view.py cold --build before=054d1706 --build main=d12a062c --build head=HEAD --runs 1h=7,24h=5 --out <dir>:054d1706d12a062csum by (level, detected_level)) 1hsum by (detected_level)) 1hIt is still several times slower than main before #760 on this data (the chain
reads every line, where main's query answered with the wrong series; see "What
is left"). Warm times do not move: this branch against
d12a062c, both orders,D 24h on this data 2.80 s -> 2.84 s and 3.05 s -> 3.04 s (the 2.48 s -> 3.26 s
widening seen in #760's measurement was a busier host: the same two builds are
flat here).
Changes
perf: the two-keys patterns of the level keys probe run behind a gate of phrasesdetectedLevelKeysRisk(a level key twice in a JSON or logfmt line,key(?s:.*)key) scans the rest of every line that holds a level key, which is every line of a body-only window: 325 CPU-ms of an hour of rows, the heaviest probe of the first view. It now runs behindlogsqlTwiceFilter(detectedLevelKeyNames)(one source with the patterns):_msg:seq("a","a")for a name twice and(_msg:"a" (_msg:"b" or ...))for two names, answered from the token index; 45 CPU-ms (the scan alone is 21). The gate is a superset of the patterns, never a second decision: each occurrence a pattern matches is a key name between non-word characters, which is a phrase of VictoriaLogs' tokenizer (getPhrasePosinlib/logstorage/filter_phrase.go, identical in v1.40, v1.47 and v1.52), and two occurrences do not overlap, so eitherseqfinds the second name after the first or two names are both present. 2.5 KB of query text.perf: the gate leaves out the pair of a dotted key name and its last wordlog.levelholds the wordlevel, so the pair of the two admitted every line with that single key. A line with both holds the word twice and passesseq("level","level"); the gate stays a superset.fix: a selector too long for the gated keys probe gets the probe without the gateandfix: the keys probe is asked again without its gate when VictoriaLogs refuses the longer text-search.maxQueryLen(16 KiB): it gets the text without the gate, the same verdict; the edge is pinned with bases of exactly the longest that fits and one byte more. An operator who lowered-search.maxQueryLenbelow the gated probe gets VictoriaLogs' 400 for a query that is too long; that refusal (and no other) sends the probe once more without the gate.perf: the probes of the labels run once for the stored-level query and the chaincachedStatsPushdownRisk,rememberedRisk,anyRisk); the verdict is risky if either is. The part of a window younger than five minutes is still asked in both, as every remembered verdict is.test: the remembered risk verdicts are scoped to the base and to the filterstest: the e2e of the keys gate writes to VictoriaLogs onlytest: a script that measures the first view ...,test: the first view of a window, every sample ...bench/ab/cold_first_view.py(usage inbench/ab/README.md) and its two outputs,2026-10-16-dlfirstview-coldrestart.jsonand-probes.jsonunderbench/ab/results/oneoff/.test: A/B results ...,docs: ...,test: visual proof pages ...bench/ab/results/2026-10-16-dlfirstview-*.json(the other order and the body-only runs underoneoff/), the[Unreleased]entry, the registry row, two Explore pages and a 5m range forbench/visual/spec.json.Per probe, VictoriaLogs CPU per request (one first view of D at 1h, each request
replayed 10 times on windows shifted by a minute, median; main now
d12a062candthis branch;
2026-10-16-dlfirstview-probes.json, frompython3 bench/ab/cold_first_view.py probes --build main=d12a062c --build head=HEAD --out <dir>):Tests (each fails without its change)
TestTwiceGateIsASupersetOfTheKeyPatterns(every ordered pair of the 14key names in 16 JSON and logfmt shapes, with tab, CR, form feed, newline,
no-break space and near spellings, and 300,000 random lines; the gate evaluated
from the text
detectedLevelTwiceGate()returns, withgetPhrasePosandmatchSequenceof VictoriaLogs written out from their source, an invalid bytecounting as a token rune),
TestTwiceGateRejectsSingleKeys(a singlelog.levelkey is rejected; twolog.levelkeys, andlog.levelwithlevel, pass),TestTwiceGateOmitsTheDottedPair,TestTwiceGateNarrowedIsCaught(six narrowings of the gate text, each caught by the corpus),
FuzzTwiceGateIsASupersetOfTheKeyPatterns(30 s, no finding),TestKeysRiskProbeGatesTheTwoKeyPatterns. e2e on real VictoriaLogs v1.52.0and v1.47.0:
TestRangeMetricCompatibilityDetectedLevelKeysGate(18 lines, onestream each: the probe the proxy sent and the same without the gate give the
same verdict, which is the table's, per line).
TestKeysRiskProbeGateEdgeOfTheQueryLength(bases ofexactly 16384 minus the gated probe's length and one byte more),
TestLongSelectorKeepsEveryRequestWithinTheQueryLimit(a 12.5 KB selectorthrough the pushdown: every request within the limit, the right answer),
TestKeysRiskProbeRetriesWithoutTheGateWhenRefusedAsTooLong(a fakeVictoriaLogs that refuses long queries: one retry without the gate; a refusal of
both is returned; another 400 is not retried).
TestLabelProbesRunOnceForTheAttemptAndTheChain(a cold window:one partial-parse request, none on the next ask; a line VictoriaLogs does not
parse as Loki does, text after the closing brace, still sends the query to the
raw-row evaluator), and the three guard-count tests that pinned the repeat
(
TestOrderedJSONFilterPushdownUsesStatsBuckets,TestOrderedJSONLogsVolumeUsesStatsBuckets,TestLevelVolumePlainAndLogfmtUseStatsBuckets) now expect one.TestRiskVerdictOfOneBaseDoesNotClearAnother(two bases in one tenant and window, only the second holds two level keys: the
clean one is asked first, the risky one goes to the raw-row evaluator with the
answer it gets alone) and
TestRiskVerdictOfOneFilterDoesNotClearAnother(thesame for two filters over the same fields, one clean and one on a field a row
stores while its body has none).
unit tests and the e2e; the last key out of the pairs, 3 unit tests and the e2e
subtest of
severity_textwithSEVERITY_TEXT; the sequences never matchingtheir own key, 3 e2e subtests; the dotted pair back in the gate, 2 tests; no
fallback for a base too long, 2 tests; no retry on the refusal, 1 test (2
subtests); the edge off by one, 1 test; the label key told apart by whether the
level probes run again, 4 tests; the level key without the base, 1 test; the
labels key without the filters, 1 test.
e2e against real Loki v3.7.7
Isolated compose project, image built from this branch,
/readyand/healthchecked, VictoriaLogs restart count 0 before and after every run, a fresh stack
per run (the two runs below are of the first-view cuts as pushed first; after the
last fixes
TestRangeMetricCompatibilityDetectedLevelKeysGatewas run again on afresh v1.52.0 stack: pass):
ci.yamlTestSetup_IngestLogs|TestRangeMetricCompatibility.*|TestDrilldown_LongRangeVolume_.*|TestCompat_HeavyQueryProtection|TestCompat_PatternLineFilter.*|TestCompat_DetectedLevel.*A/B against
d12a062c(this branch's own delta; VictoriaLogs v1.52.0, capped at 2 CPUs)Warm p50 after a cold run (5 runs at 1h, 3 at 6h and 24h; a shape the first pass
moved is measured again), targets interleaved per request, one fresh stack per
run,
docker update --cpus 2on VictoriaLogs, noise settings are CI's (30% and100 ms). Every run in both orders (base first, branch first), standard data
(a level stream label on every row) and the data without it (
--no-level-label).20 runs, restarts 0 -> 0 on every one, 0 new differences from Loki (the control
set's
topk rate by appand DL6's sum by 0.2% to 0.4% differ on main too). Thecontrol set at 1h: 20 shapes, all
same, in both orders. Saved asbench/ab/results/2026-10-16-dlfirstview-*.json(the other order and the body-only runs underoneoff/). Runs:python3 bench/ab/pr_smoke.py --base d12a062c --ranges 1h --runs 5 --confirm-runs 7 --selection <sel.json> --out <dir> --loki-mem 3g --vl-mem 4g [--no-level-label].Data with no stored level (what the change acts on)
Warm, with Loki in the loop at 1h:
6h and 24h (VictoriaLogs only):
Warm, VictoriaLogs alone, 15 runs, 1h, against
d12a062c(flat) and against mainbefore #760
054d1706(the same gap #760 disclosed, unchanged on the warm path):Cold, from the harness (the first request of a shape in a fresh process, one
sample per run; the restart measurement above is the figure to read):
Data with a level stream label on every row (stored-level plan, untouched)
Control set at 1h, both orders:
The three proofs
Isolated compose project (
bench/visual/stack.py up --hours 168 --main-ref d12a062c,body-only data: the generator's
LOG_NO_LEVEL_LABEL=1; Loki holds the last 1.5 h asthe tool documents, so ranges beyond that compare main against this branch),
Grafana with the pinned plugins, three datasources: main (
d12a062c), this branch,Loki direct.
a. Visual and data-to-data
Two Explore pages were added to
bench/visual/spec.jsonfor the shapes this changetouches (
explore-D,sum by (level, detected_level), andexplore-DLV5,sum by (detected_level), both| json | drop __error__), next to the existing pages.Screenshots of main, this branch and Loki side by side, per page and range
(80 montages, captured locally; the visual-smoke run of this pull request posts its own; for example
explore-D-1h.png,explore-DLV5-1h.png,dd-service-logs-1h.png,dd-landing-1h.png), and the captured/api/ds/querybodies and responses with thecomparison (
bench/visual/compare.py). This change has novisible effect: the first view of a window is latency, and the data on screen is
the same, so main and this branch render the same panels (pixel difference
explore-Dandexplore-DLV50.04%, which is the datasource name in the header;dd-service-logs0.11%,dd-landing0.14%, 2.1% at 7d). The proof is the data: in all 80 page and range combinations (16 pages x5m, 1h, 6h, 24h, 7d) the requests of main and of this branch are identical
("main = PR" in the comparison; three cells were captured again after a one-off 500 on
main's high-cardinality
span_id/trace_idbreakdown at 24h and a retried requestat 7d, and are identical). Against Loki, at 5m and 1h (the ranges Loki holds), 163 of
236 captured requests are identical, the rest explained, history-dependent or
differences that main has too (for example Loki returns no series for the plain
sum by (level, detected_level) ... | drop __error__on this data and thelogfmt level!=""count differs, 42 against 14). For the shapes this change reads,Explore D and DLV5 at 1h, the series Grafana receives equal Loki's; at 5m one
series differs from Loki's in its timestamps (30 against 30 points), on main too.
The A/B harness's own comparison against Loki is
samefor every shape at 1h, onboth orders, for base and branch (series count and sums; DL6 within 0.2% to 0.4% on
both).
b. Real Grafana range matrix
The 16 pages (Explore logs and volumes: plain,
| json,| logfmt, the grouped andungrouped sums; the label browser; Drilldown landing, service logs, labels, fields,
a field's values and patterns) at 5m, 1h, 6h, 24h and 7d, through Grafana's
datasources (its query splitting included), main against this branch against Loki. Result: main and this branch send the same requests and
receive the same answers in every cell, so no status differs from main's; all 80
captures completed.
c. Backend-safety load
240 s per build against the same VictoriaLogs (2 CPUs not applied here; the stack's
default), 8 concurrent clients drawing from nine shapes (D and DLV5 at 24h on the
body-only data, D at 6h, plain volume, rate by level and bytes at 24h, topk and the
json field volume at 6h, a json log query at 15m), each request with a window of its
own so that no cached answer is served;
/labelsprobed every 2 s; VictoriaLogsrestart count read before and after. Both builds: every request 200, restart count
0 -> 0 (no OOM),
/labelsanswered 119 of 119 times (p50 0.011s for main and 0.009 s here, max 0.128 s and
0.062 s):
Registry
The row changed (text only; nothing pending, no new rows):
What is left
data, which is the cost of an exact answer (main's query returns 4 series where
Loki returns 8): what runs before the chain is the stored-level query (221
CPU-ms, which answers nothing on rows with no stored level), the presence probe
(86), the partial-parse probe (55), the keys probe (46) and the unpack probe
(35), and the stored-level query, the presence probe and the chain run one after
the other. Estimates, not measured: running the stored-level query beside the
probes and the presence probe takes its 70 to 140 ms off the wall time of a first
view at the price of the same CPU, which on 2 CPUs saves about half of that;
skipping it for a window the presence probe says has no stored level saves its
221 CPU-ms and about 90 to 140 ms, but needs the presence probe first. Neither is
done: they change the order of the plan, a separate decision.
a window ending now the label probes still run twice over that part.
probe's
detected_levelcount instead would save that request, but only ifdetected_levelis a stored row field of every stream, which VictoriaLogs'ignore_fieldsbreaks: not done.