v0.3.0: complete rewrite from first principles - #55
Merged
Merged
Conversation
v0.2 ran whatever each adapter happened to support and printed the results next to each other. This rewrite defines each query once, asks it of every engine in that engine's dialect, and checks every answer against an engine-independent reference before publishing a number. Architecture: * engine SPI is Engine/Session/Tx/Result plus an Info struct carrying name, plane, dialect chain and capabilities. Adding an engine is one package and one registration. The runner has no per-engine branches. * dialect resolution: a query carries texts per dialect, an engine carries a preference-ordered chain, first match wins. No match is a SKIP with reason no-dialect-text, not a silent fallback. mage is now its own dialect, so Memgraph's procedure calls stop resolving as cypher on engines that can't parse them. * verify gate runs every query's oracle before measurement. One FAIL drops measurement for the whole workload x engine. A SKIP doesn't. * planes (inproc/bolt/subprocess/native) are stamped per row so an in-process number never gets compared to a wire number by accident. Engines: zu, neo4j 2026.06.0, ladybug 0.19.1, memgraph. Bolt and cgo adapters are behind the bolt and ladybug build tags. Workloads: micro-read/write, SNB Interactive v2 (short, complex, update, mix), FinBench-derived, LinkBench-derived, Graphalytics, GAP, Graph500. Three bugs that made the old write and cold numbers wrong: Setup/Teardown only ran during verification. They now wrap every firing of a write (warmup, measured, verify), untimed, with repetitions of a bracketed query serialized. Without that, repeated writes aren't a distribution. An engine that enforces the key errors from repetition 2 on (ladybug's snb-iu1 errored 100/100). An engine that doesn't enforce it piles up duplicates while we measure. Worse, a write whose Setup staged its input matches nothing once that input is gone, so later repetitions time an index probe over zero rows. Most setup-dependent SNB and LinkBench writes were reading about 25x fast. Verification can't catch this since it fires each write once, and the first repetition is the one that always works. ColdRun tracked Max, labeled it P99, and never set P50, so every cold p50 we ever printed was 0.00ms. It now summarizes through the same code path as the warm run. A query where every repetition errored rendered as 0.00ms, which made it look like the fastest cell in the row. Now it renders ERR(n/n). Spec: notes/Spec/2064g/bench/00-10.
Two changes that let zu answer more than the four primitive-mode queries. engine/zu now passes parameters to zu query as repeated -p name=value flags, sorted by name so the command line is stable across runs. int64, int, float64 and string are carried; a bool is rejected with an error naming the query and the parameter, because zu's parser types a bare token and there is no spelling of true that survives the round trip. The dialect chain drops Cypher. zu accepts a Cypher-shaped syntax for the subset it implements but it is not Cypher: labels are case-sensitive and the loader names its tables node and edge, so a text written for Neo4j fails on the label. Keeping Cypher in the chain turned every zuQL-less query into a FAIL, and one FAIL discards the measurement for the whole workload. With a single-dialect chain those queries SKIP with reason no-dialect-text instead. The micro texts follow: the existing zuQL ones are lowercased, and scan-stats and the two triangle counts gain zuQL texts.
zu's binder takes a bare aggregate call in a RETURN item and nothing else, so count(*) > 0 AS found came back as "aggregate item 'found' must be a bare call for now". Counting in a WITH and comparing in the RETURN asks for the same two index probes and the same boolean. All nine micro-read queries verify against zu now, as do micro-uniform, micro-er, micro-mix, and micro-powerlaw (where the two shortest-path queries skip on the declared capability, as intended).
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.
What this is
v0.2 measured whatever each adapter happened to run and printed the results side by side. This rewrite makes the comparison defensible. A workload defines a logical question once, the harness asks it of every engine in that engine's dialect, and every engine is validated against one engine-independent reference before any number gets published.
Spec is in
notes/Spec/2064g/bench/00-10.Architecture
Engine/Session/Tx/Result, plus an explicitInfo{Name, Plane, Dialects, Caps}. Adding an engine is one package and one registration. The runner has noif name == "zu"branches left.Textsper dialect, an engine carries a preference-ordered chain. First match wins. No match is a SKIP with reasonno-dialect-text, never a silent fallback.magegets its own dialect so Memgraph's procedure surface stops resolving ascypherand failing to parse. A parse failure is a FAIL, and one FAIL discards the whole workload.Algorithm,NeedsPathPredicate,NeedsShortestPath, and write-without-transactions all SKIP rather than handing an engine a text it cannot parse.inproc,bolt,subprocess,native) are stamped per row, so an in-process number is never quietly compared against a wire number.Engines: zu, Neo4j 2026.06.0, Ladybug 0.19.1, Memgraph. The Bolt and cgo adapters sit behind the
boltandladybugbuild tags.Workloads: micro (read, write, mix, power-law, uniform, ER), SNB Interactive v2 (short, complex, update, mix), FinBench-derived, LinkBench-derived, LSQB, Graphalytics, GAP, Graph500.
Three fixes that invalidated published numbers
Write bracketing was verification-only.
SetupandTeardownnow surround every firing of a write, including warmup and measured repetitions, untimed, with repetitions of a bracketed query serialized.Without the bracket a keyed insert is a trajectory, not a distribution. An engine that enforces the key errors on repetitions 2..n and reports the cost of a constraint violation (Ladybug's
snb-iu1errored 100/100). An engine that does not enforce it accumulates duplicates and reports inserts into a table growing under the measurement. The silent case is worse: a write whoseSetupstaged its input matches nothing once that input is consumed, so every later repetition reports the cost of an index probe that found no rows, which renders as a very fast write. Every setup-dependent SNB and LinkBench write was measuring roughly 25x under its true cost.Verification cannot catch this. It fires each write exactly once, and once is the repetition that always succeeds.
ColdRunnever assigned P50. It accumulatedMax, called itP99, and leftP50at the zero value, so every cold p50 ever printed was0.00ms. That reads as an instant cold first access, the opposite of what a cold pass exists to show. It now collects samples and summarizes through the same code as the warm run.An all-errors query rendered
0.00ms, the fastest-looking cell in its row, because errors are excluded from the percentile slice. It now rendersERR(n/n).Rebase notes
Rebased onto main after #49 and #54 landed. #49's
micro-powerlawandmicro-uniformworkloads are ported into the new API, along with themicro-khop3,micro-varlen,micro-sp, andmicro-sp-bidirqueries they need. The two path queries are the reasonNeedsShortestPathexists: zu declares no shortest-path support, and without the flag its dialect chain falls through to the Cypher text and fails onshortestPath(), which would discard the entire micro measurement instead of skipping two queries.The LSQB brute-force validation from #51 and #52 is rewritten against this branch's nine query texts (main and the rewrite define different queries under the same
lsqb-qNids). 23 fixtures, shaped to break the shortcuts a grouped count can take: an edge that must not join, a hub that fans out, a duplicate counted with multiplicity, and cycles sharing edges.#53 is not merged. It fails to compile against the current
tamnd/gr(unknown field MaxPoolPages), and it lives entirely inadapter/gr/, which this branch deletes.Verification
go build ./...,go vet ./...,gofmt -l .clean. Full test suite passes untagged and with-tags "bolt ladybug", including new regression tests:TestBracketMakesWriteRepetitionStationary,TestBracketNotTimed,TestColdRunReportsP50.Fast-profile runs on two hosts (server3, gamingpc) show 0 FAILs and 0 errored repetitions. A bounded full-profile pass (7 workloads x 3 engines) agrees with the fast-profile numbers within noise, which confirms the truncated fast counts are sound.
Benchmark output (
results/,results-full/) is gitignored now. The previously committed run artifacts are removed, except one kept as areport/testdata/fixture.zu now answers in zuQL
zu ran four queries before this.
zu shellandzu querywere both listed inzu helpand both replied "unknown command", so the probe fell back to primitive mode and everything outsidemicro-point,micro-point-miss,micro-khop1,micro-edgeskipped withzu-no-query-verb. tamnd/zu#72 adds the query verb, and this branch drives it: parameters go across as repeated-p name=valueflags sorted by name, so the command line is stable run to run.The dialect chain drops Cypher. zu accepts a Cypher-shaped syntax for the subset it implements but it is not Cypher: labels are case-sensitive and the loader names its tables
nodeandedge, so a text written for Neo4j fails on the label before it reaches anything interesting. Keeping Cypher in the chain turned every query without a zuQL text into a FAIL, and one FAIL discards the whole workload. With a one-dialect chain those queries skip withno-dialect-text, which is the honest outcome.The micro texts follow. The existing zuQL ones are lowercased,
micro-scan-statsand the two triangle counts gain zuQL texts, andmicro-edgecounts in aWITHand compares in theRETURN, because zu's binder takes a bare aggregate call in a projection and nothing else.All nine
micro-readqueries verify against zu now, as domicro-uniform,micro-er,micro-mix, andmicro-powerlaw, where the two shortest-path queries skip on the declared capability as intended.