Repository navigation
v0.15.0
GoGraph v0.15.0 — see CHANGELOG.md for the per-release entry.
0.15.0 — 2026-09-17
33 commits — 21 perf, 8 docs, 1 fix, 1 build and 2 merges. Counted at 19138042,
the develop tip before release preparation; the release-preparation commits that carry this
entry are necessarily not in their own count. One sprint delivered this window — 362,
performance laboratory 20260915 — and all 33 of its tasks closed. 468 files changed,
238 586 insertions, 963 deletions (git diff --shortstat v0.14.2..19138042).
Sprint 361 is not in this window. Its twelve tasks closed at a7f06ec3, df0b1866 and
02c08e44, and all three are ancestors of v0.14.2 — verified with
git merge-base --is-ancestor. It shipped in v0.14.2 and is recorded in that entry.
238 586 insertions is not 238 586 lines of engine, and the difference is larger here than in
any previous release. Bucketed by path from git diff --numstat v0.14.2..19138042:
| bucket | files | insertions | deletions |
|---|---|---|---|
docs/benchmarks/ (raw measurement artefacts) |
373 | 217 756 | 0 |
*_test.go + testdata/ |
33 | 7 648 | 21 |
examples/ (exercise harnesses) |
13 | 4 739 | 440 |
docs/ (prose) |
10 | 3 605 | 38 |
engine *.go (non-test, shipped) |
24 | 2 270 | 381 |
scripts/ (laboratory drivers) |
9 | 2 150 | 0 |
build/CI (Makefile, .github/) |
2 | 219 | 30 |
internal/ (not shipped API) |
1 | 154 | 43 |
| root Markdown + misc | 3 | 45 | 10 |
| total | 468 | 238 586 | 963 |
91.3 % of the insertions are measurement data. The raw benchstat series, profiles and host
telemetry under docs/benchmarks/ are reproduction input for the three campaign records, not
code and not documentation; goreleaser excludes them from the release tarballs by design.
The exported API gained exactly two identifiers and lost none. Measured by parsing every
non-test Go file outside internal/, examples/, cmd/ and any gen/ or tck/ directory at
both v0.14.2 and 19138042, and diffing the rendered declarations: 4 820 exported
declarations before, 4 822 after, with two additions and zero removals, renames or
re-signings. That file set is a superset of the API surface
docs/semver.md defines — it retains all of bench/, of which the policy
counts only bench/ldbc, bench/dimacs9 and bench/rmat — so it cannot miss a change inside
the surface. No bench/ file changed in this window, so the two-addition result is the same
under either reading. Under docs/semver.md a new exported identifier is the MINOR
trigger, so 0.15.0 is a MINOR by the rule rather than by decision — unlike v0.14.1,
where a MINOR surface shipped as a PATCH deliberately.
No change is breaking. cypher/exec.ExpandConfig.EmitStoredDir defaults to false, so
every existing caller's row width and chunk schema are unchanged; graph/index/label's
Intersect and Union keep their caller-owned-result contract, and a test deliberately
mutates a single-label Intersect result to assert it. No on-disk format changed — no file
under store/ was modified in this window.
go.mod and go.sum are byte-identical to v0.14.2 — same pinned toolchain go1.27.0,
same go 1.26 directive, same dependency set at the same versions
(git diff v0.14.2..19138042 -- go.mod go.sum is empty). Nothing in the supply chain moved.
The openCypher TCK gate is unchanged at 3 897/3 897. const tckExecutionBaseline = 3897
in cypher/tck/runner_test.go is untouched and no .feature file changed in this window,
so the scenario population is the one v0.14.2 was measured against. Every campaign re-ran it
and it held throughout.
Every performance claim below rests on an interleaved A/B against a byte-identical noise
control measured in the same session. That discipline is not decoration: on this host a
same-versus-same arm has produced 22 of 36 flat-by-construction rows as statistically
significant, so an uncontrolled comparison on it means nothing. Where a figure is a share of a
profile rather than an absolute, it is labelled as such.
Seven premises were refuted on measurement and shipped no code, including two float
reassociations that were built in full, validated, measured and then discarded. They are
itemised under Notes rather than dropped, because a refuted premise is a result.
All 33 commits in this window are unsigned (git log --format=%G? returns N for every
one). docs/release.md asks for this to be re-measured per release rather
than assumed; this is that measurement, and it has not changed.
Added
graph/index/label.(*Index).BitmapShared(label uint32) *roaring64.Bitmap— returns the
memoised, immutable label bitmap image without copying it. The caller must not mutate the
result. It exists becauseIntersectandUnionpromise a caller-owned result and that
promise is kept: the new method is the opt-in way to read the shared image on a scan path that
does not mutate.*roaring64.Bitmapwas already in this package's exported signatures
(Intersect,Union), so no new third-party type enters the public API (#2863).cypher/exec.ExpandConfig.EmitStoredDir bool— opt-in, defaulting tofalse. When set,
Expandappends a fourth column to every emitted row carrying the edge's stored
orientation relative to the emitted traversal orientation:falsefor a row the forward pass
produced,truefor one the reverse pass produced. It is meaningful only forDirBoth, the
one direction under which the orientation varies row by row; aDirOutorDirInhop's
orientation is a property of the hop and is resolved at plan time. A caller that sets it
owns registering the fourth schema column, or the row andschemaWidthdisagree. The cell
is anexpr.BoolValue, read by type assertion, so a coordinate gone stale across a
projection falls back to the old ladder instead of reading a node id as a direction (#2864).- Two counters,
cypher.csr_pair_cache.bypasses_no_cacheand
cypher.csr_pair_cache.bypasses_own_writes. Together with the pre-existinghitsand
missesthey form an exact partition of every cache consultation, which is what makes a
measured hit rate readable at all. Both are documented in
docs/metrics.md (#2866). -contentionflag onexamples/internal/exprof, writing mutex, block and goroutine
profiles, off by default.examples/is explicitly outside the public API in
docs/semver.md — the module neither imports nor depends on it — so this is
recorded for completeness and is not API surface (#2856).- Three campaign records and three profiling reports under
docs/, plus two prior-art
studies read from Neo4j, Memgraph and PostgreSQL source at pinned tags:
campaign-bolt-extreme-concurrency-2026-09-16.md,campaign-cypher-parsing-2026-09-16.md,
campaign-whole-surface-2026-09-16.md,profile-bolt-concurrency-2026-09-16.mdand its
round-2 and round-3 successors,prior-art-cypher-parsing-2026-09-16.mdand
prior-art-mvcc-visibility-2026-09-16.md.
Changed
- The Bolt server's per-rejection log is bounded to one line per second, and its argument is
lazy (#2835). Refusing 500 connections now produces 1 log line where it produced 500;
bolt.server.conn.rejectedstill counts exactly 500, so the operator keeps the signal
and loses only the storm.RemoteAddris resolved 0 times atLevelErrorand 1 time
atLevelWarnacross 100 refusals. The bound is on time, not a one-in-N sample: a
count-based sample still grows linearly with the flood's rate, which is the property that had
to go.rejectLogIntervalis a fixed one-second package constant following the
handshakeTimeoutprecedent, deliberately not anOptionsfield, so the change does not
widen the public API. If you parse or alert on this log line's volume, read this entry.
Admission, the semaphore and its capacity are byte-identical. - The Bolt server writes one response per query instead of two (#2834).
sendResponseno
longer flushes; the session loop carries one obligation instead — the response buffer is
drained before the goroutine can block waiting for the client, and before teardown —
discharged in exactly two named places. Teardown flushes under a fixed deadline, so a peer
that has stopped reading can no longer pin a handler goroutine and its semaphore slot. RECORD
batching is unchanged and its three regression tests are untouched. Protocol bytes are
unchanged; what changed is how manywrite(2)calls deliver them. propMapShardsmoves from 64 to 256 (#2839), and the cost is recorded rather than
hidden: an empty graph grows 91 KiB andNewtakes 57 % longer, becauseNeweagerly
allocates one map per stripe in three of the nine shard arrays. This is a level shift, not
a change of scaling behaviour — the collapse from 64 to 1 024 connections moves only 0.660
to 0.698 of peak. 4 096 shards was rejected on measurement for penalising the
single-threaded scan (+43.8 %) and write (+41.7 %); a full-scan read at 256 stays
indistinguishable from 64, so the read path the 16→64 widening bought is not surrendered.make ciruns the module suite exactly once and fails cheap first (cd5cee02). The
coverage gate leaves the automatic gate by user decision, as benchmarks did inv0.14.2;
make cover-gate,scripts/cover_gate.shand their thresholds are untouched and are run
deliberately.CI_STAGESnow carries membership and order —shell-guard tidy fmt vet build ci-kg-verify vulncheck lint test-uninstrumented test-timing test-short— driven by the
newscripts/ci_stages.sh, which stops at the first failure, times each stage and stamps the
ones that pass.vulncheckandci-kg-verifyare never stamped, their verdicts depending on
state outside the tree. Becausecover-gatewas the only phase built without-race, and so
the only one that compiled the//go:build !racefiles, six tests that ran nowhere else moved
intoUNINSTR_PKGS— two of them bounding the allocation a forged length prefix can provoke.
A green run measured 941.9 s → 821.2 s; the real saving is on a red one, where thev0.14.2
lint violation now fails at 18.0 s instead of after ~815 s of test phases.
Fixed
- The Cypher plan cache's read path did not scale with the cores given to it (#2852) —
filed and worked as a defect rather than a missed optimisation, because a hot path that
serialises every caller on one lock is a defect under this project's concurrency mandate.
A single mutex, LRU list and map carried 98.46 % of all mutex delay, and a lookup cost
15.04 ns at one core against 92.14 ns at ten. The cache is now sharded 16 ways, each shard
with its own mutex, LRU list and map, padded to a cache line: 32 keys at ten cores fall
119.10 ns → 42.42 ns (−64.38 %) with no allocation added on any row. 16 shards, not the
256 that reaches the −83.7 % headline ceiling, because a second ladder priced the
difference: at 75 % load the hit rate falls from 99.5 % at 16 to 66.0 % at 256, and one
lost point costs about 18 ns per lookup against a measured 1.8 µs miss. The ceiling is missed
at the shipped count and that is stated rather than smoothed — it was derived from the
one-key case, which sharding provably cannot address. - The lint gate on the
cypherfrontend-share benchmark was restored (2944b184), red on
the working branch since3ab42f2b. Test-only; no shipped code path is affected.
Performance
Twelve further measured improvements, each with interleaved A/B evidence against a
byte-identical noise control. Full method, artefact paths and refuted alternatives are in the
three campaign records under docs/.
Bolt server, at the 1/8/64/256/1024-connection ladder plus saturation
(docs/campaign-bolt-extreme-concurrency-2026-09-16.md):
- Response writes coalesced to one per query (#2834). Write syscalls per query fall
2.000 → 1.001 at 64 connections, 1.002 at 256 and 1.005 at 1 024, counted from
runtime/trace. Throughput +20.66 % at 64, +19.61 % at 256, +19.71 % at
saturation and +16.99 % at 1 024, against a same-versus-same noise floor of 0.57 %;
p99 −17.84 % at 64. Verified independently at 256 connections: 192 540 q/s against a
161 283 q/s baseline. Round 2 re-measured the gains at HEAD as +20.19 / +19.45 / +19.94 /
+16.80 %, with the noise floor re-derived as 1.08 % — a faster server has a wider floor. - The per-rejection log costs 93.6 % less (#2835): 547.3 ns and 80 B / 4 allocs →
34.9 ns and 0 B / 0 allocs, measured with all arms in one binary so the comparison is
interleaved by construction, and reproduced at-count=5(unbounded 546.6–550.1 ns, bounded
34.47–35.09 ns). With the level off, 4.5 ns. - Concurrent property writes contend on 256 stripes instead of 64 (#2839): throughput
+7.0 % at 64 connections and +13.3 % at 1 024, n=8 interleaved order-rotated runs per
arm with no overlap at all between treatment and control at either rung, against a noise
floor set by two byte-identical binaries that stayed indistinguishable (p=0.93 at 64, p=0.17
at 1 024). p99 −16.1 %, p999 −14.8 %; one reader contending with one writer
−36.5 %. Verified independently at +6.1 % and +12.9 %, on a host at loadavg 12.19 against
2.19 for the baselines — the busier arm was the faster one.
Cypher statement parsing, over an 83-statement corpus harvested from four examples
(docs/campaign-cypher-parsing-2026-09-16.md):
StripLiteralsclassifies clause keywords without allocating (#2849). It called
strings.ToUpperon every identifier token — 78.6 % of all objects it allocated, on every
execution including plan-cache hits. An ASCII fold applied during comparison replaces it:
corpus 5.449 µs → 2.843 µs (−47.83 %), allocations 148 → 32, the twelve quoted
statements −52.02 %, and all 78 non-hoisting statements now allocate nothing.
Equivalence is proven over 213 749 exhaustive identifiers plus every case permutation of
all 23 keywords, and a second test walks all 256 byte values to pin the ASCII premise, so a
future widening of the alphabet fails there rather than silently invalidating the fold.- Two-stage parsing: a bailing stage first, the full error strategy only on retry (#2850).
Accepting path 1.798 ms → 1.690 ms (−6.01 %), allocations +0.31 %. The error path costs
+57.4 %, and that price is measured rather than assumed. Prediction mode stays LL:
PredictionModeSLL— the prior art's most promising lever — was refuted at −0.49 %, p=0.529,
below the 1.05 % noise floor and on two independent runs. Error fidelity is fenced by 21
statements compared field-for-field against literals captured before the change; the two
stages agree over 5 912 corpus prefixes; fuzzing ran 1 035 449 executions with no hang
or panic. - The plan-cache hit rate was measured for the first time in this project (#2851): the four
corpus examples hit at 95.51 %, 45.16 %, 0.00 % and 0.00 %, with zero evictions
everywhere, measured by test overlay with no example modified and bit-identical before and
after the sharding. It refutes the campaign's own ranking rationale, which had put the
keyword fold ahead of two-stage parsing on a 97.62 % break-even that no example reaches.
Both had already landed, so nothing needed undoing; what is refuted is the reasoning.
Search, centrality and the Cypher execution row path
(docs/campaign-whole-surface-2026-09-16.md):
- The BFS frontier queue is caller-owned scratch (#2861) — the largest single result of the
window, and an allocation defect rather than a CPU one.bfsFarthestbuilt its queue inside
the function at capacity one and grew it once per source. Pre-sized ton(a hard bound,
since a BFS enqueues each vertex once) and threaded through both 2-sweep calls and the level
walk, with a private queue per worker on the parallel arm: on11_social_network -users 100000over three interleaved rounds,diameter.allocs2 026 768 → 433 (4 681×), total
allocation −99.49 %, profile CPU 221.18 s → 145.72 s (−34.1 %), and the Go-runtime
bucket 146.76 s (66.35 %) → 6.58 s (4.52 %). Benchmarks −25.64 % and −38.10 %
sec/op on the two diameter rows. Traversal order is unchanged, so results are
bit-identical. - The label bitmap is shared as an immutable memoised image instead of cloned per scan
(#2863). Each label entry memoises an image published under its existing lock and dropped by
the four mutators through the singleIndex.mutatefunnel; the warm path takes a read lock,
returns the pointer and allocates nothing. −28.71 % geomean over 10 interleaved
rounds, all four phases at p=0.000, against a noise control with zero significant rows;
the roaring clone chain leaves the allocation profile entirely (0.00 % at
-nodefraction=0); allocation 15 223 → 9 742 MB at 1.431× the operations. All 36
Intersectcall sites were resolved. - The stored direction is carried on the expand triplet instead of recovered per row by an
O(deg) topology probe (#2864). Direction is resolved at plan time forDirOut/DirIn— no
probe, no column — and carried as a bool cell only forDirBoth. Geomean −27.02 % over 8
shapes, all p=0.000, n=12, four arms in one binary interleaved per repetition against a
same-arm control at +0.11 %; allocs/op +0.00 %. TheDirBothcolumn pays for itself:
−16.68 % to −41.44 % on undirected shapes, p=0.64–0.98 (flat) on directed ones.
relStoredInvertedandslotHoldsHandleleave the profile entirely; the surviving
relStoredInvertedForHopis 0.032 % against a 1 % target, and the reference benchmark
falls 118.05 s → 95.19 s of CPU. - The per-row binding facts are resolved once instead of per row (#2865).
rowBindPlan
resolves under async.Once, with the kind held as a bitmask rather than an enum because
the original ladder falls through rather than branching.mapaccess2_faststrattributable to
populateRowCtxwent from a 8.37 s mean (7.57–9.69 s over seven baseline runs) to
absent from the caller list entirely at-nodefraction=0— and the absence is not the
loop having stopped, because the function is still 22.28 s cum (26.22 %) of the profile.
Collateral over 5 interleaved repetitions with complete separation, p=0.0079 by exact
permutation: total CPU 96.76 s → 85.77 s (−11.35 %), wall −13.29 %, and all 79
result lines identical in all five. Only ~8.4 s is attributed to the removed probes; the
rest is stated as unattributed rather than claimed. - The Dijkstra working set is sized to the span the query reached (#2862).
newDistancesCopysizes its three arrays to the reached span andacquireDijkHeaptakes a
size hint, so the pooled backing is pre-sized rather than climbing from zero. Delivered where
reach is much smaller than the id span:SSSP_RepeatedFrom−31.29 % sec/op and −99.76 %
B/op,Dijkstra_RepeatedRevalidate−99.72 % B/op,Dijkstra_Small−4.52 % / −12.63 %,
with no ns/op regression anywhere. The call sites insearch/astar.goand
search/prim.gopass the same hint. - PageRank's residual sign branch became
math.Abs(#2869).runRange's flat share falls
36.72 / 37.35 / 35.21 % → 28.69 / 29.34 / 28.87 % and its absolute flat CPU −30.3 %;
PageRanker_PowerLaw50K_Repeated−17.55 % andPageRank_PowerLaw50K−9.90 %, both
p=0.008, withPageRank_Cycle1Kflat as the reachability control. A premise was corrected in
passing:math.Absis not universally bit-identical — it differs on-0.0and on NaN —
but neither is reachable here, and all three facts are now asserted over 220 000 bit
patterns. An end-to-end digest pin over 3 200 scores at fixed iterations holds at
GOMAXPROCS2, 3, 4, 7 and 10. - Brandes's loop-invariant distance is hoisted out of the neighbour loop (#2868):
geomean −0.70 %,2k_deg16−2.01 % and5k_deg16−3.52 %, both p=0.001, against
a noise control at −0.02 % where every row is flat. Thedwcache the task also mandated
was tried, measured and dropped — a single-variable split showed it carried the whole
sparse-graph regression. Bit-identity is asserted againstbetweennessLegacyover five
risk-chosen shapes plus a 24-graph fuzz, on both call sites. - An 80-byte binding is passed by pointer (#2866), repairing three
gocritic hugeParam
findings that would have turned the sprint-close gate red, with a two-sided mutation gate.
Notes
Seven premises were refuted on measurement, and the code written to test four of them was
discarded. Each is recorded because a refuted premise is a result:
- Two non-bit-reproducible reassociations of Betweenness and PageRank were built in full,
validated, measured and then discarded (#2870). Two independent realisations, 7 interleaved
samples each with rotated arm order: every row flat, geomean +0.24 % and +0.22 %, against
a byte-identical control reading +0.81 % at p=0.017 — the noise floor is wider than the effect
sought, on rows proven able to resolve 2–3.5 %. From the assembly: the removed division
hides behind thedelta[v]read-modify-write and thesigma[v]gather, and PageRank's
pull loop is bound by two random gathers, not by the arithmetic between them. The 8.89 s
and 1.87 s that motivated the work were self-time on instructions off the critical path. An
entry point named for speed that is not faster would have cost +4.85 % B/op and
bit-reproducibility for nothing measured. No default numeric result moved in this release. Snapshot.visible's contention does not exist (#2867). It accounts for 0.046 % /
0.027 % / 0.196 % of37_mvcc_write_contention's mutex delay across three passes and does
not appear at all in36_mvcc_snapshot_topology. None of it is thesync.Mutexthe report
named: the children areruntime.mapassign_fast64ptr(93.64 %) andmakemap_small— the
runtime heap lock inside the map insert, which is the allocation the memo causes, not the
lock guarding it. The allocation cost is real and is filed as #2873.- Throttling the MVCC vacuum cannot help (#2840). A ceiling probe ran with
reclaimPropVersionsdisabled entirely, not throttled: 12 543 q/s against a base of
12 669 — −1.0 %, exactly the noise floor — although it held 21.0 % of all Go-mutex delay
at 1 024 connections. That bounds every possible scheduling fix from above, and it corrects the
premise the task was written on: mutex delay is not a cost function, because delay counts
time goroutines spent waiting rather than time the workload lost. The campaign's ranked tables
were re-read against this and no other row depended on the mistaken reading. - Removing Bolt's per-chunk reassembly allocation is not worth its regression risk (#2836).
The weight reproduces at HEAD — 1.18 KB and 25.9 objects per query, 1.42 % of allocated bytes
— but GC mark CPU is only 0.62 / 0.92 / 1.06 % of process CPU at 64 / 256 / 1 024 connections,
so the gain has a ceiling of roughly 0.015 % of process CPU, about 70× below the noise
floor, on a path guarded by the size-budget and length-bound tests. - The Bolt 256→1024 throughput knee is a measurement artefact, not a defect: varying only
the measurement window gives −14.90 % at 20 k queries, −6.89 % at 100 k and −4.40 % at 400 k,
a fixed 24–31 µs per-connection start-up amortised over fewer queries. Half of the remainder
was the laboratory's own/dev/fddescriptor walk, whichos.ReadDirlstat-ed 40×/s for
0.62 / 2.32 / 5.78 % of CPU at 64 / 256 / 1 024. - The second
read(2)per query is the Go runtime's own EAGAIN retry ininternal/poll,
not a protocol read, and is unreachable from module code: an AF_UNIX control differing
only in blocking mode measures 2.0004 reads per logicalReadthrough the poller against
1.0002 blocking. - Two standing backlog items were refuted on their own numbers (#2859): #2661
(canonicalRelTypesKeyhas zero occurrences at HEAD) and #2391 (buildReadPhysical25.61 %
not 59.56 %,ResolveLabelBitmap52.75 % not 10.42 %, which inverts its own priority
ordering). Seven of the thirteen reconciled items were supported and one was refined.
The MVCC visibility direction was closed by prior art rather than opened by it (#2860).
GoGraph's predicate tests a contiguous frontier where PostgreSQL (REL_17_5) and Neo4j
(5.26.0) both test a high-water mark plus an exception array — there is no array to
search, so the predicate was never the problem. Four options are costed in
docs/prior-art-mvcc-visibility-2026-09-16.md; the decision is left to the user.
The CSR pair cache's hit rate was measured and no new key was implemented (#2866):
read-only 99.98 %; the topology row 19.5 % at -spokes 400 and 8.5 % at 4 000.
Three candidate keys are stated with their soundness and rebuild cost, four are rejected — one
on measurement rather than argument — and the recommendation is to implement none yet,
because the ceiling of two of the three is a quantity no row measured here supplies.
The example sweep's coverage of the module was measured for the first time (#2858):
54.8 % of statements, reaching 55 of 99 module packages, proven non-empty by 79 counter
files merged into 54 148 lines. Of the 44 unreached, 41 are explained by structure (20
bench/*, 6 cmd/*, 15 internal/* test-infrastructure packages, and cypher/tck).
graph/io and store/bulkimport are the only substantive production packages the sweep
never reaches, and neither has a named exercising harness — recorded as unattributed.
One commit in this window closes no roadmap task. cd5cee02, the make ci stage
reordering described under Changed, has no rmp task behind it: a search of 500
TASK_STATUS_COMPLETED audit rows returns no transition carrying that hash. The change is
described by its own commit message, which is detailed; what is missing is the planning record,
not the rationale.
The measured record for this release is docs/benchmarks/v0.15.0.md,
and it is required. Benchmark execution left the release path in v0.14.2 — neither
make release-accuracy nor make release-preflight runs a benchmark — but the report did
not: internal/docscheck's TestPerReleaseBenchmarkReportExists runs inside make ci and
fails the gate when docs/benchmarks/<version>.md is absent, guarding rmp #1398 on the rule
that "each release must record its benchmark/load-test numbers". The gate derives the version
from the newest release-notes/*.md, so it flips the moment the release notes are written. It
did exactly that while this release was being prepared, and the entry it produced is this one.
What that report does and does not contain, stated here because it bears on every figure
above. No release-time campaign was run: every number is a development-time A/B measured by
the task that shipped the change, against that task's own byte-identical control. No
whole-release delta is claimed and the thirteen rows are not additive. Raw series for the
Cypher-parsing and plan-cache work are in this repository under docs/benchmarks/; the Bolt and
whole-surface campaigns wrote theirs outside it, so those figures are attributed to the task
closing summaries and campaign documents and are marked as such. No load test at the published
1/8/64/256/1024 concurrency ladder was run for this release — the Bolt campaign drove that
ladder as campaign evidence at its own commits, and the report records the absence explicitly.
Further reading
- Narrative release notes:
release-notes/v0.15.0.md - Measured benchmark report:
docs/benchmarks/v0.15.0.md - Changelog file at this tag:
CHANGELOG.md - Commits in this release: https://github.com/FlavioCFOliveira/GoGraph/commits/v0.15.0
Reproducibility: built from commit dfeacab with Go 1.26.