Skip to content

perf(store): on-demand subpath counts instead of permanent indexes - #529

Merged
dborup merged 9 commits into
masterfrom
perf/413-subpath-on-demand
Oct 24, 2026
Merged

dborup merged 9 commits into
masterfrom
perf/413-subpath-on-demand

Conversation

@dborup-agent

@dborup-agent dborup-agent commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Relates to #413

Summary

Route Patterns (/api/analytics/subpaths, /subpaths-bulk, /subpath-detail) no longer read the two permanent indexes spIndex / spTxIndex. They count subpaths from s.packets per request. Only the hop lists are copied under PacketStore.mu; enumerating, resolving and ranking run after the lock is released.

The index cost 51 MB of heap per 30K transmissions, about 6 µs per ingested and 13 µs per evicted transmission, and a 285 ms rebuild under the write lock. All of that is gone. Endpoint latency stays the same for the list and goes up by about 2 ms for the detail. Counts, ranking, tie-breaks, window semantics and the API shape are unchanged: on the e2e fixture, 52 of 52 API responses are byte-identical to master.

Plan (milestones)

  1. Measure first on master 0350322e, using synthetic data with 30,000 tx and 2,000 nodes. Path lengths follow the hop-count histogram of test-fixtures/e2e-fixture.db (median 7 hops). Hops are upper-case 1- and 2-byte prefixes, laid out as random walks over a repeater mesh. Measured: index size, per-ingest and per-eviction maintenance, rebuild time, and endpoint latency.
  2. Tests first (commit fd61b924). A frozen copy of the current index is the oracle, plus lock, eviction, memory and ranking tests. This commit is red on master.
  3. Implementation (commit cb8ad366). Remove the index and its maintenance, count on demand, rank with a bounded heap, drop the subpath ready gate, and update the memory estimate.
  4. Validate: Go suites, -race, mutants, test-all.sh, frontend helpers, E2E against a local server on the CI-prepared fixture, an API diff against a master binary, and a browser check.

No config or customizer implications: nothing new is configurable. The 8-hop cap is the index's existing cap, now the named constant subpathMaxHops.

Design choice: incremental vs on-demand

Option Permanent heap (30K tx) Ingest / eviction upkeep Rebuild under write lock
master (spIndex + spTxIndex) 50.9 MB (414,243 keys, 628,455 tx refs) 6.0 µs / 13.0 µs per tx 285 ms
Incremental: drop the duplicate count map, count = len(spTxIndex[key]) ~37.5 MB (frees 13.4 MB) about the same (the eviction scan of each key's tx list stays) about the same
On-demand (this PR) 0 0 0

On-demand doesn't make the endpoints slower (numbers below), so the bigger win was chosen and no new response cache was needed. The existing bounded subpathCache (#370: 256 entries, 60 s TTL) still covers repeated renders.

Readers of the indexes, and what replaced them

Reader Before After
/subpaths, unfiltered (computeAnalyticsSubpaths) walk spIndex under RLock countSubpathGroups: snapshot under RLock, then count outside it
/subpaths-bulk, unfiltered walk spIndex under RLock same, one count for all groups
/subpaths with region or window per-tx scan under RLock (computeSubpathsSlow / computeAnalyticsSubpathsWindowed, duplicates of each other) one function on the snapshot, counting outside the lock; semantics unchanged
/subpath-detail spTxIndex[key] lookup, everything under RLock match and copy the needed fields under RLock; resolve and aggregate outside it
ingest, reindexTxPath, eviction, hash merge add/remove per tx removed
Load, background chunk load buildSubpathIndex removed
index-ready (Kpa-clawbot#1008) SubpathIndexReady gate + 503 removed; the path-hop gate and its test hook (pathHopBuildGate) stay
memory budget (estimateStoreTxBytes) charged O(hops²) per tx for spTxIndex linear: path hops only

The neighbour graph, other analytics and /api/nodes/{pk}/paths never read these indexes. They use byPathHop, which is untouched.

Contract: what is preserved

  • Unfiltered list keeps the index semantics exactly: lower-cased, comma-joined raw keys of 2 to 8 hops; every occurrence counted, so a path that repeats a subpath counts it twice; rawHops is the smallest raw key behind a shared name.
  • Window and region keep their per-transmission semantics: original-case rawHops and every length up to maxLen. The difference from the unfiltered list existed before this PR and is kept as-is.
  • Ranking: rankSubpaths keeps the top N in a bounded heap ordered by (count desc, path asc). Paths are distinct map keys, so the order is total: the heap selects exactly what the full sort cut at N selected, ties are broken on the path before the cut, and the result is the same on every run (flaky: #226 Hash Stats adopters sort E2E — server sorts multiByteNodes on packets only, ties come out in random order #256).
  • Detail: same totalMatches (one per occurrence), the same 8-hop cap, and case-insensitive matching.
  • API shape: unchanged.

Deliberate differences

  1. totalPaths after eviction. The index's eviction removed a transmission's subpaths but never decremented spTotalPaths, so totalPaths grew and every pct shrank over a long-running server. The old 215 test documented this. It is now the count of held transmissions with 2+ hops.
  2. No 503 at startup for the three Route Patterns endpoints. There is no index to wait for. This also means that between the first chunk (FirstChunkReady, when HTTP binds) and the end of LoadChunked, the three endpoints answer from the chunks merged so far, where master answered 503 until the whole hot window was merged and indexed. During the later background fill both trees answer from what is loaded. A partial answer can sit in subpathCache (60 s TTL) until StartupLoadDone, which clears it (invalidateCachesFor{eviction} in signalStartupLoadDone); the same applies to the other analytics caches.
  3. Detail matched-transmission order is now packet order instead of index insertion history. Only the float summation order of avgSnr/avgRssi can differ, by an ulp at most.
  4. Memory budget: the per-tx O(hops²) charge is gone (28 MB of estimate on the 30K set), so the same maxMemoryMB now holds more packets.

Benchmarks (proof, AGENTS.md rule 0)

Setup: go test -bench, Go 1.27.1, linux/amd64, i7-1260P VM with 4 vCPU, 3 runs each. The data is subpath413Data(30000, 2000, 413), the same in both columns. Master was measured back-to-back with the branch.

master 0350322e this PR
/subpaths 2–8, limit 100, uncached 577–592 ms, 178 MB/op 462–598 ms, 270 MB/op
/subpaths-bulk, Route Patterns 4 groups, uncached 584–669 ms, 204 MB/op 486–582 ms, 281 MB/op
…of which PacketStore.mu (RLock) is held the whole request 60–69 ms (snapshot + hop context)
/subpath-detail, hot pair (most frequent 2-hop) 0.94 ms 2.7–3.3 ms
/subpath-detail, cold 5-hop 9 µs 1.6–2.0 ms
permanent index heap 50.9 MB 0
index upkeep per ingested tx 6.0 µs, 53 allocs 0
index upkeep per evicted tx 13.0 µs 0
index rebuild (Load / chunk loader, write lock) 285 ms, 106 MB 0

Reproduce with cd cmd/server && go test -run '^$' -bench '_413' -benchtime 2s .. The BenchmarkLegacySubpathIndex_413 rows time the removed index through its frozen copy. They match the master measurements: 6.1 µs, 13.0 µs and 51.0 MB.

Perf justification. An uncached list request is O(Σ min(path,8)·path) to enumerate, plus O(distinct keys) to resolve. The distinct-key work is the same work master did over spIndex; the enumeration replaces it with a heap-bounded selection instead of a full sort of ~400K entries. Keys are built in a reused buffer, and a string is allocated only for a key not seen before. The transient maps are about the size of the old index (~400K keys) and are freed after the request, which is the +90 MB/op above. Results are cached per key for 60 s in the bounded subpathCache.

The detail matches the subpath against every packet's path, O(N·path), outside the lock (round 2, review F2). Under the first RLock it only copies each transmission's pointer and hop list into a slice allocated beforehand (~8 ns per packet); a second RLock copies what the detail reads of the matched transmissions, O(matches). Worst writer wait during one detail request on 30K tx (BenchmarkSubpathDetailWriterWait_413, GC off, 3 runs): hot pair 2.5–3.3 ms → 1.06–1.25 ms, cold 5-hop 1.8–2.2 ms → 0.48–0.62 ms.

Concurrent cache misses for one key share one count (round 2, review F1): GetAnalyticsSubpaths, GetAnalyticsSubpathsWithWindow and the unfiltered bulk run their miss through one singleflight.Group (subpathSF, keyed on the cache key, bulk on its groups' keys), which checks subpathCache again inside the flight. Peak heap during N concurrent cold /subpaths requests for one key on 30K tx (BenchmarkSubpathsColdConcurrent_413, 3 runs): N=1 141–146 → 136–147 MB, N=4 441–482 → 140–143 MB, N=8 907–914 → 137–143 MB.

Tests

New: cmd/server/subpath_on_demand_413_test.go, subpath_fixture_413_test.go, subpath_on_demand_413_bench_test.go.

Test Covers On master
TestSubpathsOnDemandMatchLegacyIndex_413 /subpaths (9 length/limit groups incl. ties at the cut, maxLen 63, minLen 9), bulk, window, region and detail (hot, cold, upper-case, 9-hop, 1-hop, unknown) vs the frozen index, before and after ingest, 100 path changes and eviction of 300; 2 seeds red
TestSubpathTotalPathsFollowsEviction_413 real EvictStale; totalPaths and pct red (totalPaths = 2, want 1)
TestSubpathCountsKeepNoPermanentIndex_413 no spIndex/spTxIndex/spTotalPaths; linear memory estimate red
TestSubpathCountingReleasesStoreLock_413 a request parked inside hop resolution (the test holds the neighbour-graph lock) must not block a store writer; 5 cases: list, bulk, window, region, detail red: writer blocked in all 5 (verified with the master index built)
TestSubpathRankTopNMatchesFullSort_413 heap == full sort, 50 repeat runs per case green (preservation guard)
TestSubpathOnDemandConcurrentWriters_413 -race: concurrent path, SNR, observer, append and evict writers green (preservation guard)

Existing tests updated where they reached into the removed fields:

Mutants: 18 run, 18 killed.

Mutant Killed by
heap orders on count only RankTopN, Equivalence
final order without path tie-break RankTopN, Equivalence
rawHops not the smallest raw key Equivalence
window ignored Equivalence, TestSubpathsHonorsTimeWindow_StoreLevel
8-hop cap applied to windowed counts Equivalence, MaxLenFirmwareCeiling
8-hop cap dropped (list) Equivalence
case not folded (list) Equivalence
one count per transmission (list) Equivalence
short paths counted in totalPaths TotalPathsFollowsEviction, Equivalence
region filter ignored Equivalence
8-hop cap dropped (detail) Equivalence
one match per transmission (detail) Equivalence
case not folded (detail) Equivalence
list counting under RLock ReleasesStoreLock
window/region counting under RLock ReleasesStoreLock
detail resolved under RLock ReleasesStoreLock
detail reads a tx after unlock ConcurrentWriters (-race)
O(hops²) charge restored NoPermanentIndex, EstimateStoreTxBytes ×2

Validation

  • cd cmd/server && go test ./...: pass. cd cmd/ingestor && go test ./...: pass.
  • -race on the affected tests (_413, perf: move post-load index builds to background goroutines (startup Q1) Kpa-clawbot/CoreScope#1008, Deterministic order in GetSubpathDetail (same tie-order bug class as #256) #273, flaky: #226 Hash Stats adopters sort E2E — server sorts multiByteNodes on packets only, ties come out in random order #256, hash migrate, eviction, chunked load, tracked bytes): pass.
  • sh test-all.sh: 259/259 files pass. node test-frontend-helpers.js: 719 passed.
  • E2E against a local Go server on e2e-fixture.db, prepared as in CI (freshen, seed SQL, corescope-migrate, the four seed files):
    • test-e2e-playwright.js: 154/157 passed, 3 skipped, including "Analytics Route Patterns tab renders content".
    • test-a11y-1705-subpath-hop-prefix-e2e.js: pass.
    • test-issue-1705-subpath-contrast-e2e.js: pass.
  • API diff on the same prepared fixture, master binary vs this branch: 52/52 responses byte-identical. The requests were bulk with 4 groups (unfiltered and window=7d), /subpaths with 4 length/limit combinations (including 2–63 and 9–63) plus window=24h, and 45 subpath-detail queries (the top 40 raw keys plus mixed-case and unknown ones).
  • Browser: the Route Patterns tab in headless Chromium renders 115 rows, and the first rows (d6 → SHL 🌳 30 / 8.1%, …) are the same as on master.
  • cmd/server stays read-only: no new writes.
  • map[string]interface{} in non-test Go code: 732 → 731.
  • No frontend change; scripts/check-xss-sinks.sh --diff origin/master is clean.
  • Workflows untouched (fork guards 9 in deploy.yml, 1 in release-fast-path.yml).

Coordination

#411 (PR #527) is merged and origin/master is merged into this branch (004a5644). The conflicts were comment-only. The combination turned TestMemoryEviction_CrossesHighAndReachesLow_113 red: its maxMemoryMB sat at a MiB boundary, so one pass had to free 25.03% against the 25% cap. The test now sizes its store, so that share stays between 15% and 20.4% (0aa39c41).

🤖 Generated with Claude Code

dborup and others added 2 commits October 23, 2026 14:21
… alone (#413)

Equivalence of /subpaths, /subpaths-bulk, windowed, region and
subpath-detail results with a frozen copy of the spIndex/spTxIndex the
store keeps today, through ingest, best-path changes and eviction on
synthetic 30K-tx-shaped data; totalPaths after a real eviction; no
permanent index and no O(hops^2) memory charge; bounded top-N ranking vs
full sort with ties at the cut; counting outside PacketStore.mu; and a
-race test with concurrent writers.

Red on master: the endpoints read an index the packets do not feed,
eviction leaves spTotalPaths behind, the store still holds the index and
charges it, and the counting runs under the read lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TxIndex (#413)

The store kept every 2-8 hop subpath of every transmission in two
permanent maps, updated on ingest, best-path change, hash merge and
eviction and rebuilt under the write lock at load. On 30K transmissions
with the fixture's path lengths that is 414K keys / 628K tx refs, ~51 MB
of heap, ~7 us per ingested and ~15 us per evicted transmission, and a
~325 ms rebuild.

Route Patterns now count from the packets per request:
- only the hop lists (shared, never edited in place) and the aggregate
  hop context are copied under PacketStore.mu; enumerating, resolving
  and ranking run after it is released (subpath-detail copies the few
  fields it reads of the matched transmissions);
- the unfiltered list keeps the index's semantics exactly: lower-cased
  comma-joined raw keys, 2 to 8 hops, every occurrence counted, rawHops
  the smallest raw key behind a name; window and region queries keep
  their per-transmission semantics;
- rankSubpaths keeps the top N in a bounded heap with the full (count,
  path) order, so ties break on the path before the cut, as the sort did;
- totalPaths is the held transmissions with 2+ hops; the index's
  eviction never decremented it, so pct drifted on a long-running server.

The subpath ready gate and its 503 go (nothing to build); the path-hop
gate and its test hook stay. The memory budget no longer charges
O(hops^2) per transmission for the index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dborup-agent

Copy link
Copy Markdown
Collaborator Author

Rapport — CS-pve-agent2 PR#529 #413 — head cb8ad36

Status: Draft, every CI job green on the first run. All requirement tests are red before and green after, except two preservation guards that are already green on master (see below). 18 of 18 mutants are killed. Not merged, not marked ready.

Evidence tags: [T] = test or measurement run, output observed. [A] = analysis or derivation from measured data. [K] = verified by reading code or diff.

Requirements

# Requirement Test Red before → green after Mutant(s), all killed Evidence
1 Measure first: index size, upkeep, endpoint latency (30K tx / 2K nodes) BenchmarkLegacySubpathIndex_413, BenchmarkSubpaths*_413, plus the same benches run on master 0350322e n/a (measurement). Master: 414,243 keys / 628,455 tx refs / 50.9 MB heap; ingest 6.0 µs, eviction 13.0 µs per tx; rebuild 285 ms n/a [T]
2 Choose a design from the numbers (incremental vs on-demand; bounded cache if too slow) measured heap split: the count map alone is 13.4 MB of the 50.9 MB n/a. On-demand list 462–598 ms vs master 577–592 ms; detail +1.6–2.3 ms. No new cache needed; the existing bounded subpathCache (#370) stays n/a [T] [A]
3 Find every reader of spIndex/spTxIndex grep of the whole tree; table in the PR body n/a. All readers are in store.go (list, bulk, detail) plus maintenance in ingest, reindex, eviction, hash merge, Load, chunk loader and the Kpa-clawbot#1008 gate. The neighbour graph and node paths use byPathHop only n/a [K]
4 Identical counts, rawHops, 8-hop cap, case folding, per-occurrence counting; same API shape TestSubpathsOnDemandMatchLegacyIndex_413 (counts, bulk, region, detail subtests) against a frozen copy of the index red (endpoints read an index the packets do not feed) → green 8-hop cap dropped (list, detail); case not folded (list, detail); one count per tx (list, detail); region ignored; rawHops not the smallest [T]
5 Deterministic ranking and tie-break before the top-N cut TestSubpathRankTopNMatchesFullSort_413 (heap vs full sort, 50 runs each); the equivalence test uses limits that cut through ties ranking guard green on master (master full-sorts; preservation guard); equivalence red → green heap on count only; final order without path tie-break [T]
6 Same window semantics equivalence window subtest, TestSubpathsHonorsTimeWindow_StoreLevel, MaxLenFirmwareCeiling (63-hop via ?window=) window subtests green on master (preservation); the test as a whole is red → green window ignored; 8-hop cap applied to window [T]
7 totalPaths correct after eviction TestSubpathTotalPathsFollowsEviction_413 (real EvictStale) red (totalPaths = 2, want 1; pct 50 vs 100) → green short paths counted in totalPaths [T]
8 No permanent index; accurate memory accounting TestSubpathCountsKeepNoPermanentIndex_413, TestEstimateStoreTxBytes* red (fields present; 8-hop minus 4-hop estimate 1100 B, want 220) → green O(hops²) charge restored [T]
9 #495 lock rule: copy under the lock, count outside TestSubpathCountingReleasesStoreLock_413, 5 cases (list, bulk, window, region, detail) red: in all 5, a writer is blocked while the request resolves hops (master run with its index built) → green list counted under RLock; window/region counted under RLock; detail resolved under RLock [T]
10 -race on the affected tests TestSubpathOnDemandConcurrentWriters_413, plus -race over the _413, Kpa-clawbot#1008, #273, #256, hash-migrate, eviction, chunked-load and tracked-bytes tests concurrency guard green on master (master reads everything under the lock; preservation guard) → green detail reads the tx after unlock (killed by the race detector) [T]
11 Benchmark before/after in the PR description before/after table in the PR body, master re-run back to back n/a n/a [T]
12 Real-data equivalence master binary vs this branch on the CI-prepared e2e-fixture.db 52 of 52 API responses byte-identical (bulk, /subpaths variants incl. 2–63 and 9–63, 24h/7d windows, 45 detail queries) n/a [T]

The commit sequence follows the rows above. Test commit fd61b924, run alone: the equivalence, eviction, no-index and lock tests fail; the ranking and concurrency guards pass. Implementation commit cb8ad366: all pass [T].

Invariants

Check Result Evidence
cmd/server read-only no writes added; the read path only drops index maintenance [K]
new map[string]interface{} outside tests none; the non-test count went 732 → 731 [T]
hardcoded colours no frontend change [K]
scripts/check-xss-sinks.sh --diff origin/master clean ("no public/**/*.{js,html} changes to scan") [T]
fork guards 9 in deploy.yml, 1 in release-fast-path.yml; workflows untouched [T]

Local runs

Suite Result Evidence
cmd/server go test ./... pass [T]
cmd/ingestor go test ./... pass [T]
sh test-all.sh 259/259 [T]
node test-frontend-helpers.js 719 passed [T]
test-e2e-playwright.js (local server, CI-prepared fixture) 154/157 passed, 3 skipped (fixture-dependent) [T]
test-a11y-1705-subpath-hop-prefix-e2e.js pass [T]
test-issue-1705-subpath-contrast-e2e.js pass [T]
browser: Route Patterns tab in Chromium 115 rows, values equal to master [T]
local servers stopped by pid/port afterwards [T]

CI per job (run 39970068648, head cb8ad36)

Job Result
Go Server Tests 1/3, 2/3, 3/3 pass
Go Ingestor Tests pass
Go Libraries & CLI pass
Go Build & Test pass
JavaScript Unit Tests & Lint pass
Script & Container Unit Checks pass
Verify Go Test Assignments pass
Playwright E2E 1/3, 2/3, 3/3 pass
Verify E2E Shards & Coverage pass
Validate Docker Image pass
Build & Publish Docker Image pass
Release Artifacts skipped
Deploy Staging skipped
Publish Badges & Summary skipped

No reruns were needed, and the known flaky test #271 did not fail [T].

Remaining items

  1. Intentional behaviour changes [K]:
    • totalPaths/pct no longer drift after eviction.
    • The three Route Patterns endpoints no longer return 503 at startup.
    • Detail averages are summed in packet order, so they can differ by at most an ulp.
    • The memory budget no longer charges the removed index, so the same maxMemoryMB holds more packets.
  2. Trade-off [T]: an uncached list request allocates ~270 MB/op transiently, against 178 MB on master. Detail is +1.6–2.3 ms, and its O(N·path) match scan runs under RLock (~1.6 ms on 30K tx).
  3. Pre-existing divergence, kept [K]: the unfiltered list (lower-cased rawHops, 8-hop cap) and window/region queries (original case, no cap) still behave differently. This existed before the PR; it could be a follow-up.
  4. Overlap with perf(store): avoid eager per-transmission observation dedup maps #411 / PR perf(store): avoid eager per-transmission observation dedup maps #527 [T]: a dry-run merge gives comment-only conflicts in store.go (memory-estimate comments) and eviction_test.go. Whichever lands second resolves them. Recheck the TestEstimateStoreTxBytes bounds after both are merged.
  5. Unchanged doc [K]: docs/specs/startup-performance.md still lists "Build subpath index" in its historical timing table.
  6. Hot-path cost under the lock [K]: the aggregate hop context is still built under RLock (~60 ms of the 69 ms snapshot on 30K tx), as before.

@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Review — CS-Macmini PR#529 — head cb8ad36

Dom: REQUEST CHANGES

Independent, read-only review. git ls-remote gave the same head before and after. Reviewed through git archive of the head and of the merge tree against current master e14cac27 (git merge-tree --write-tree is clean, no conflicts). Evidence tags: [T] run and observed, [A] derived from measured data, [K] read in the code or the diff.

The core of the change holds up. I reproduced the equivalence claim far past the author's 52 responses, and the list-side lock win is real and large. Two things need a decision before this merges: the per-request full scan has no concurrency bound, and the combination with #527 turns a test red deterministically.

Findings

# Severity Where What
F1 must fix store.go GetAnalyticsSubpaths, GetAnalyticsSubpathsBulk Every uncached Route Patterns request is a full store scan, and N concurrent misses for the same key each run it. Measured: the PR's 58 MB resident saving is consumed at ~4 concurrent requests and inverts beyond it.
F2 should fix / decide store.go SubpathDetail The detail's match scan under s.mu.RLock is now O(all packets) instead of O(matches). Worst writer wait on 30K tx: 0.60–0.62 ms → 0.97–1.15 ms, and it scales with the store.
F3 must fix (coordination) tracked_bytes_accounting_113_test.go #527 + #529 fails TestMemoryEviction_CrossesHighAndReachesLow_113 5/5, while each alone is green. The PR's coordination note names a different test (TestEstimateStoreTxBytes), which is in fact fine.
N1 nit store.go matchLowerHop, appendLowerHop The new non-ASCII case-folding fallback has no test; my mutant survives the whole cmd/server suite.
N2 nit store.go countSubpathGroups The maxLen = max(...) reduction over groups has no test; my mutant survives the whole suite.
N3 nit PR body, index_ready_1008.go Dropping the gate also means the three endpoints answer from the hot chunk only while the background chunk load is still merging, where master answered 503. Defensible, but not stated.
N4 nit docs/specs/startup-performance.md:12 Still lists "Build subpath index
N5 nit (pre-existing) store.go The unfiltered list (folded case, 8-hop cap) and the window/region path (original case, every length to maxLen) still disagree. Author disclosed; pre-dates the PR.

F1 — the full scan per request has no concurrency bound

All on subpath413Data(30000, 2000, 413), Go 1.27.0 darwin/arm64, master measured back to back with the merge tree [T].

master e14cac27 this PR
resident heap at rest (HeapInuse, after GC) 101.1–101.7 MB 43.4–44.6 MB
sum(estimateStoreTxBytes) vs that resident heap 72.6 MB → ratio 0.71 44.4 MB → ratio 1.00
cold list 2–8 limit 100 274–279 ms, 166 MB/op 231–236 ms, 257 MB/op
cold bulk, the tab's 4 groups 286–298 ms, 191 MB/op 247–252 ms, 268 MB/op
warm list / warm bulk (subpathCache hit) <1 ms, 0 MB <1 ms, 0 MB

Peak live heap during N concurrent cold requests for the same uncached key, 3 runs each [T]:

N master peak this PR peak
1 213.3 / 218.0 / 219.8 MB 151.5 / 158.7 / 163.6 MB
4 456.2 / 474.1 / 476.3 MB 463.2 / 471.8 / 489.8 MB
8 794.2 / 813.5 / 823.1 MB 886.0 / 951.6 / 954.5 MB

Per-request allocation is flat in N in both trees (166 MB and 257 MB respectively), so neither dedupes [T]. The read: the PR is clearly better at one request in flight, about even at four, and ~90–130 MB worse at eight — the resident saving is spent on transient per-request maps as soon as a few misses overlap [A].

This is not a regression in kind: master stampedes identically, and GetAnalyticsSubpaths had no singleflight before either [K]. What changes is that the scan is now the only path and costs 1.55× the allocation. Context for asking anyway: subpathCache is 256 entries / 60 s TTL and the compute runs outside the cache lock [K]; the three routes are public and the router carries no admission gate (cors, perf, no-store, cdn-detection only) [K]; the repo already gates two comparable full-store scans — perfMemLimiter (#371), whose own doc note is "N overlapping or rapidly repeated mem=1 requests run N full scans", and reachColdLimiter (#341) — and already uses singleflight.Group in four caches [K]; and #413 asks to "keep response caches and scans bounded". Wrapping the subpathCache miss in a singleflight keyed by the cache key (and by the group key for bulk) would make the PR strictly better than master at every N.

F2 — the detail's lock hold now scales with the store

SubpathDetail walks every packet under s.mu.RLock calling subpathOccurrences, where master did one spTxIndex[spKey] lookup [K]. Worst s.mu.Lock() wait by a writer during one hot-pair detail request, 30K tx, 3 runs [T]:

  • master: 604 µs / 619 µs / 610 µs
  • this PR: 1.15 ms / 1.05 ms / 967 µs

Master's hold is O(matches); this one is O(N_packets), so it grows with the store rather than with the answer. The list direction is the opposite and is the big win — worst writer wait during one cold list request: 267 ms over 1 acquisition on master (a writer was blocked for the whole request) → 26.5 ms over 172 acquisitions here [T]. TestNoSQLUnderStoreLock and TestNoSQLUnderStoreLockDetects pass [T].

Two related observations under that same RLock [K]: buildAggregateHopContextPubkeys(s.packets, pm) inside snapshotSubpathPaths is the dominant remaining list hold (the 26.5 ms above), and buildHopContextPubkeys runs once per occurrence, so a path that repeats a subpath 20 times rebuilds and stores the same context 20 times before the unlock. Both are disclosed in remaining items 2 and 6; TestSubpathCountingReleasesStoreLock_413 pins that resolution happens outside the lock, but nothing pins a bound on the scan itself.

F3 — the #527 combination is deterministically red

git merge-tree origin/master <#527 head> <#529 head> gives exactly 3 conflicts, and I confirmed each is comment-only: 1 in eviction_test.go, 2 in store.go [T][K]. The author's claim is accurate.

After resolving those comments, though, TestMemoryEviction_CrossesHighAndReachesLow_113 fails 5/5 on the combined tree, while #527 alone on master and #529 alone on master are both green [T]:

after the pass trackedBytes=6244604 is above the low watermark 6239027

Cause [T][A]: the test sets maxMemoryMB = floor(trackedBytes/1 MiB), so the share one pass must free is (tracked − 0.85·maxMemoryMB·1 MiB)/tracked, and evictStaleInternal caps a pass at 25 % of packets. Both PRs remove a per-transmission term from the estimate, and the combination lands just the wrong side of a MiB boundary:

tree trackedBytes maxMemoryMB share one pass must free
master 10,369,965 9 22.65 %
#529 only 9,559,965 9 16.09 %
#527 only 9,132,465 8 21.92 %
both 8,322,465 7 (7.937 MiB) 25.03 % — over the cap

Merge order does not help: the arithmetic depends on the combined estimate, so whichever lands second turns master red. The test's own comment ("one pass needs about 15-24% of the store, under the 25% safety cap") states the assumption that breaks, and master is already at 22.65 %, so the right fix is to derive maxMemoryMB in that test so the required share stays a fixed, comfortable fraction instead of riding the MiB boundary. On merge order itself I have no preference on the conflicts — they are comment-only either way — but the PR that lands first should carry that test fix.

Answers to the review points

Equivalence. Reproduced and extended, byte-identically, two ways.

  1. The CI-prepared e2e-fixture.db (freshen, the workflow's seed SQL, corescope-migrate), two local Go servers from the master binary and from the merge-tree binary against separate copies of the same DB: 356 of 356 API responses byte-identical [T]. That covers 115 /api/analytics/subpaths length/limit combinations including 2–63 and 9–63, defaults, minLen=0/1/64, maxLen=200 (ceiling clamp), limit=0, five windows (15m/1h/24h/7d/30d/0), all four configured regions with and without a window, nine subpaths-bulk group sets plus the two error shapes, and 215 subpath-detail queries (every raw key the top-200 list and the top-200 windowed list produce, upper-cased variants of the top 10, unknown hops, a single hop, an empty hops=, and a 9-hop key).
  2. The fixture's longest path is 5 hops [T], so it cannot exercise the cap or ties at all. I wrote my own cross-tree harness — one identical Go file compiled in the master tree and in the merge tree, differing only in a one-line prepare that builds master's index — over a hand-written corpus with no RNG: 3-way count ties straddling the limit cut, mixed-case hops of the same prefix, a path repeating a subpath, two nodes sharing a display name, unresolvable hops, 2-byte prefixes colliding with 1-byte ones, an empty hop, a non-ASCII hop, and paths of 8, 9, 12, 20, 40 and 63 hops. 251 of 251 responses byte-identical (115 list, 18 region, 80 windowed, 15 bulk, 23 detail) [T]. Spot checks confirm the data bites: the unfiltered 2–63 list tops out at hops=8 and 9–63 returns 0 rows, the same query through ?window= reaches 34 hops, 2-2 at limit 3 and 5 cuts through a 33/33/33 tie, detail totalMatches is 108 and 63 for the repeating paths, and the shared-name rows merge two raw keys to the smallest.

Note on deliberate difference 3 (float summation order in avgSnr/avgRssi): I did not observe any divergence across those 607 responses [T]. The risk is real in principle but did not materialise.

Latency, cold and warm, and the stampede. Table and peak-heap numbers in F1. Warm is a subpathCache hit and costs nothing in either tree; cold is 15 % faster than master on my hardware and allocates 1.55× as much. Concurrent requests do each count in parallel — see F1.

The #495 lock rule. Copy under the lock, count outside, is implemented as described for the list, bulk, window and region paths [K], and the measured writer wait drops 267 ms → 26.5 ms [T]. The detail is the exception, see F2. TestNoSQLUnderStoreLock passes [T]. -race over 252 tests matching Subpath|_413|1008|273|256|HashMigrate|Evict|Chunked|TrackedBytes|EstimateStoreTx|NoSQLUnderStoreLock|TieOrder: pass, 0 data races [T]. I also checked the sharing claim in snapshotSubpathPaths — parsedPath is only ever replaced with a fresh slice (setTxPath, txGetParsedPath, and the save/restore in reindexTxPath), never edited in place, so handing the slice out past the unlock is sound [K].

trackedBytes and eviction. Better calibrated than master, not worse. On the 30K set sum(estimateStoreTxBytes) against resident HeapInuse goes from 72.6 MB vs 102.3 MB (ratio 0.71, under-charging) to 44.4 MB vs 44.4 MB (ratio 1.00) [T]. The estimate drops 28.2 MB while the real heap drops 57.9 MB, so removing the term moved the budget toward the truth rather than away [A]. Charge and uncharge stay symmetric: eviction still subtracts txChargedBytes and never re-estimates [K], and the whole suite including the _113 accounting tests and "emptied store: trackedBytes=0" is green on the merge tree [T]. The updated assertions are tightened, not loosened — est10 == est2 + 8*perPathHopBytes replaces a "at least 1500 more" bound. The only budget consequence is the one the author states: the same maxMemoryMB now holds more packets, which interacts with F1 because the scan is proportional to the packets held [A].

#527 overlap. See F3.

Tests and mutants

Everything below on the merge tree against current master e14cac27 unless stated.

Suite Result
cmd/server go test ./... ok, 61.5 s [T]
cmd/ingestor go test ./... ok, 107.6 s [T]
go vet ./... (both modules) clean [T]
-race, 252 affected tests ok, 57.9 s, 0 data races [T]
sh test-all.sh 260 passed, 0 failed (260 files) [T]
node test-frontend-helpers.js 719 passed, 0 failed [T]
test-e2e-playwright.js (local server, CI-prepared fixture) 154/157 passed, 3 skipped, incl. "Analytics Route Patterns tab renders content" [T]
test-issue-1705-subpath-contrast-e2e.js pass [T]
test-a11y-1705-subpath-hop-prefix-e2e.js pass [T]
combined tree with #527 1 deterministic failure, see F3 [T]
CI per job, run 39970068648 15 success, 3 skipped (release, deploy, badges); none of the known flaky tests appeared [T]

Red before, green after. Verified independently on the test-only commit fd61b924: TestSubpathsOnDemandMatchLegacyIndex_413 (both seeds), TestSubpathTotalPathsFollowsEviction_413, TestSubpathCountsKeepNoPermanentIndex_413 and TestSubpathCountingReleasesStoreLock_413 (all 5 subtests) fail; TestSubpathRankTopNMatchesFullSort_413 and TestSubpathOnDemandConcurrentWriters_413 pass [T]. That matches the author's disclosure exactly — the two that pass are preservation guards for properties master already had (master full-sorts and reads everything under the lock), so no red-before state exists for them, which is the right call rather than a gap. On the issue's own acceptance criteria: the measurement-first requirement is met by the benchmarks (and independently by the numbers above), tied queries returning the same paths, counts and order is covered by TestSubpathRankTopNMatchesFullSort_413 plus the equivalence subtests whose limits cut through ties, and "no route/query semantics silently change" is covered by the equivalence tests and by my 607 byte-identical responses.

My own mutants. Two, both in store.go on the merge tree. Both survive the complete cmd/server suite and are killed only by my harness, so both are test gaps rather than defects in the implementation.

Mutant Repo suite My harness
M-A: delete the >= utf8.RuneSelf fallback from both matchLowerHop and appendLowerHop, so every byte is treated as ASCII survives — go test ./... ok, 61.3 s [T] killed: 20 of 251 responses change. A hop pair ÄÖ/äö splits into two rows of 3 where master merges them into one of 6, and subpath-detail?hops=ÄÖ,10 reports totalMatches 3 instead of 6 [T]
M-B: maxLen = max(maxLen, g.MaxLen) → maxLen = g.MaxLen in countSubpathGroups survives — go test ./... ok, 61.9 s [T] killed: with groups ordered 5-8:15,4-4:20,3-3:30,2-2:50 the 5–8 group returns 0 rows where clean returns 15; the tab's own ascending order hides it [T]

M-A is N1: hops are hex prefixes in practice so the real exposure is nil, but it is new code with no test. M-B is N2: groups= order is caller-supplied on a public endpoint, so one group-order assertion would close it.

Invariants

Check Result
cmd/server stays read-only no DB write added anywhere in the diff; the read path only drops index maintenance [K]
new map[string]interface{} outside tests none — 732 on master, 731 here [T]
hardcoded colours no public/** change at all [T]
scripts/check-xss-sinks.sh --diff origin/master clean ("no public/**/*.{js,html} changes to scan") [T]
fork guards 9 repository == in deploy.yml, 1 in release-fast-path.yml; .github/ untouched by the diff [T]
closing keywords none; the body says "Relates to #413" [T]
commit author both commits dborup <kontakt@meshview.dk> [T]
leftover references to the removed index none in non-test Go; only comments, the test oracle, and the one stale docs line in N4 [T]
openapi.go never documented a 503 for these three routes, so nothing stale there [K]

The #1008 test was retargeted rather than deleted: /api/nodes/{pubkey}/paths still asserts 503 + Retry-After: 5 + the triage body, and now also asserts Route Patterns answer 200 throughout [K]. test-1659-analytics-warmup.js mocks fetch, so the frontend's 503 retry path is still exercised and still applies to other endpoints [K].

Not verified

  • Anything above 30,000 transmissions. The whole F1 trade-off scales with distinct subpath keys, and I have no data at the scale the configuration docs warn about, so the crossover point in production may differ from the ~4 concurrent requests I measured.
  • Real multi-client load against a live server; the concurrency numbers come from in-process goroutines, not HTTP.
  • The author's ingest, eviction and rebuild upkeep figures (6.0 µs / 13.0 µs / 285 ms) — I confirmed the resident heap the index held (101.1 MB vs 43.4 MB at rest) but did not re-time the upkeep itself.
  • LoadChunked behaviour during the background fill window (N3) was read, not exercised.
  • Anything on staging or production, and no browser check beyond the three E2E files listed.

dborup and others added 7 commits October 24, 2026 03:21
Brings in #527 (#411). Conflicts were comment-only: the Kpa-clawbot#743 list and
the estimateStoreTxBytes doc comment in store.go, and the manual-calc
comment in eviction_test.go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cap (#413)

TestMemoryEviction_CrossesHighAndReachesLow_113 set maxMemoryMB to the
whole MiB below the tracked size T, so one pass must free 1-0.85*floor(T)/T
of the store. With #411 and #413 both removing a per-tx term from the
estimate, the 3000-tx store tracks 7.94 MiB and the share is 25.03%, over
the 25% per-pass safety cap: the pass stops 5.6 KB above the low
watermark. obsIndexBytes plays no part (the fixture has at most 4
observations per tx, below obsIndexThreshold).

The store is now sized from a first load to track at least 16 MiB, where
the share stays between 15% and 20.4% whatever the per-tx estimate, and
the setup fails loudly if the share comes within 4 points of the cap. The
assertions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…413)

An uncached Route Patterns request scans the whole store, and N
concurrent misses for one key each ran that count (PR #529 review F1).
The misses of GetAnalyticsSubpaths, GetAnalyticsSubpathsWithWindow and
the unfiltered GetAnalyticsSubpathsBulk now go through one
singleflight.Group, keyed on the cache key (bulk: its groups' keys), the
statsSF pattern: the flight checks subpathCache again first, so a caller
that missed just before an earlier count stored its result does not
count again.

TestSubpathColdMissesShareOneCount_413 holds the first count until all
8 callers have missed (through the new test-only subpathHook) and wants
exactly 1 count for list, window and bulk; with the flight bypassed it
gets 8, 3 runs of 3.

BenchmarkSubpathsColdConcurrent_413, 30K tx, peak heap of 3 runs:
  n=1: 140-146 MB -> 131-141 MB (1 count either way)
  n=4: 441-482 MB -> 131-148 MB (4 counts -> 1)
  n=8: 907-914 MB -> 138-147 MB (8 counts -> 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SubpathDetail compared every packet's path under PacketStore.mu, so a
writer's wait grew with the store, not with the answer (PR #529 review
F2). Under the lock it now only copies each transmission's pointer and
hop list into a slice allocated beforehand; matching runs after the
unlock, and a second RLock copies what the detail reads of the matched
transmissions, O(matches). A transmission's per-tx hop context is built
once instead of once per occurrence.

TestSubpathCountingReleasesStoreLock_413 gets a subpath-detail-match
case that parks the request inside matching (subpathHook "match") and
wants a writer to take the lock; red with matching under the lock.

BenchmarkSubpathDetailWriterWait_413, 30K tx, GC off, worst writer
wait over 50 requests, 3 runs:
  hot pair (98 matches): 2527/3279/2674 us -> 1231/1202/732 us
  cold 5-hop (1 match):  1834/2201/1831 us ->  641/471/795 us
Request latency is unchanged (hot 2.8 ms, cold 1.8 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th counts (#413)

PR #529 review N1/N2. TestSubpathNonASCIIHopsFoldCase_413 checks
appendLowerHop and matchLowerHop against strings.ToLower for non-ASCII
hops (including "ẞ", which folds one byte shorter) and that list and
detail merge differently-cased hops; killed: the utf8.RuneSelf fallback
removed from both. TestSubpathsBulkGroupOrderDoesNotCapLength_413 wants
every bulk group equal to the same group on its own for group orders
that put the longest group first or in the middle; killed:
maxLen = g.MaxLen in countSubpathGroups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…413)

PR #529 review N4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…413)

The singleflight helpers spelled the result type out eight times; they
now hand around the *cachedResult that subpathCache stores, so
map[string]interface{} in non-test cmd/server code goes 733 -> 732
instead of 741. Ranking stays outside cacheMu. No behaviour change:
the same tests and the no-flight mutant (8 counts) as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dborup-agent

Copy link
Copy Markdown
Collaborator Author

Rapport — CS-pve-agent1 PR#529 runde-2 — head b3ab527

Review feedback addressed (commit b3ab5278)

Evidence tags: [T] run and observed, [A] derived from measured data, [K] read in the code. All timings are from Go 1.27.1, linux/amd64, a 4-vCPU VM, on subpath413Data(30000, 2000, 413) unless stated.

origin/master (8798f74e, which includes #527 / #411) is merged in with a merge commit (004a5644). The three conflicts were comment-only, as the review said: the Kpa-clawbot#743 list and the estimateStoreTxBytes doc comment in store.go, and the manual-calc comment in eviction_test.go [T].

  1. F1: concurrent cold misses now share one count (dfdec4ea, then b3ab5278).

    • The misses of GetAnalyticsSubpaths, GetAnalyticsSubpathsWithWindow and the unfiltered GetAnalyticsSubpathsBulk go through one singleflight.Group (subpathSF). The key is the cache key; for bulk it is the joined group keys, and the two key spaces have the prefixes 1: and n:. I followed the in-store statsSF / loadObsCounts pattern, so the flight checks subpathCache again before counting. A caller that missed just before an earlier flight stored its result therefore doesn't count again [K].

    • Region bulk already delegates per group to GetAnalyticsSubpaths, and windowed bulk to GetAnalyticsSubpathsWithWindow, so both are covered [K].

    • Test: TestSubpathColdMissesShareOneCount_413 runs list, window and bulk with N=8. A test-only subpathHook ("miss" / "count", like statsHook) holds the first count until all 8 callers have missed. The test then wants exactly 1 count, identical results for all 8 callers, and a cached result afterwards. It uses no sleep. With -race -count=30 it passed every time [T].

    • Mutant (flight bypassed, fn() called directly): 8 counts in all 3 subtests, on 3 of 3 runs. It was killed again after the b3ab5278 refactor [T].

    • Peak heap during N concurrent cold /subpaths?minLen=2&maxLen=8&limit=100 requests for one key (BenchmarkSubpathsColdConcurrent_413, -benchtime 1x -count 3, runtime/metrics heap objects sampled every 200 µs) [T]:

      N before (flight bypassed = PR head behaviour) after (b3ab5278)
      1 146.0 / 146.1 / 140.5 MB, 1 count 147.3 / 135.9 / 145.0 MB, 1 count
      4 481.5 / 441.3 / 467.9 MB, 4 counts 142.6 / 139.9 / 139.6 MB, 1 count
      8 913.7 / 911.6 / 907.1 MB, 8 counts 137.7 / 142.8 / 137.2 MB, 1 count

      The "before" column reproduces the review's numbers (463–490 MB at N=4 and 886–955 MB at N=8 on darwin/arm64) [A]. With the flight, peak heap is flat in N, so the PR is at or below the master peaks the review measured at every N (213–220 / 456–476 / 794–823 MB) [A]. Wall time at N=8 also drops, from 1.35–1.38 s to 0.46–0.48 s [T].

    • Distinct keys still count independently. A flood of different uncached keys is not bounded by this change. That is the same as master, and the review asked only for same-key dedup [K].

  2. F2: the detail no longer scans the packets under s.mu.RLock (17d8401e).

    • Under the first RLock, SubpathDetail now only copies each transmission's pointer and hop list into a slice. The slice is allocated before the lock, with a length read under a brief RLock [K].

    • Matching (subpathOccurrences) runs after the unlock, on the hop lists. They are shared, immutable slices, the same argument as snapshotSubpathPaths [K].

    • A second RLock copies what the detail reads of the matched transmissions, which is O(matches) as on master. Which transmissions matched, and on which path, is as of the first copy [K].

    • The per-tx hop context is now built once per transmission instead of once per occurrence. This addresses the review's side observation [K].

    • Test: I extended the existing TestSubpathCountingReleasesStoreLock_413 with a subpath-detail-match case. It parks the request inside matching (subpathHook("match")) and requires a writer to take PacketStore.mu. Before the fix (the hook at the start of the old scan, under RLock) it was red: "a store writer could not take PacketStore.mu while the detail matched". After the fix it is green [T].

    • Mutant (RUnlock moved after the matching loop): killed by the same case [T]. TestSubpathOnDemandConcurrentWriters_413 passes under -race [T].

    • Worst writer wait during detail requests (BenchmarkSubpathDetailWriterWait_413: a writer loops Lock/Unlock while 50 requests run, GC off so collector pauses don't count, 3 runs) [T]:

      before after
      hot pair (98 matches) 2527 / 3279 / 2674 µs 1071 / 1252 / 1056 µs
      cold 5-hop (1 match) 1834 / 2201 / 1831 µs 623 / 577 / 484 µs

      Request latency is unchanged: hot about 2.8 ms, cold about 1.8 ms [T].

    • Hold times from temporary instrumentation (not committed, 200 requests) [T]: the first hold has a median of about 250 µs (one pointer dereference per packet, about 8 ns/tx). The second hold has a median of 375 µs for the hot pair and 9 µs for the cold key.

    • Partial disagreement: the first hold is still O(N) pointer copies. It is no longer O(N·path) matching, but it is not O(matches). Getting there would mean taking candidates from byPathHop (rarest requested hop). I didn't do that here. It needs a full-scan fallback while the background path-hop build is running (pathHopReady), and it changes the order of matched transmissions from packet order to index insertion order. That would undo the byte-identical equivalence this PR is built on. It is a reasonable follow-up if the detail's lock hold matters at larger scale [K].

    • Not addressed: the list snapshot's buildAggregateHopContextPubkeys under RLock (about 26 ms of the list hold, review item 6). It is unchanged from round 1 and pre-dates this PR [K].

  3. F3: TestMemoryEviction_CrossesHighAndReachesLow_113 red on master + perf(store): avoid eager per-transmission observation dedup maps #527 (0aa39c41).

    • Reproduced on the merge commit: "after the pass trackedBytes=6244604 is above the low watermark 6239027" [T].
    • Cause. It is not obsIndexBytes. The fixture (acct113CreateDB) gives each transmission 1–4 observations, under obsIndexThreshold = 32, so obsIndexBytes is 0 for every tx [K]. The cause is the test's setup arithmetic, as the review describes:
    • Fix. This is a setup fix, not a looser test. The assertions are unchanged. A helper, acct113LoadTracking, sizes the store from a first load to track at least 16 MiB. From there the required share always stays between 15% and 20.4%, whatever the per-tx estimate is [A]. A new setup guard fails loudly if the share comes within 4 points of the cap, instead of failing as an eviction bug [K]. The test now loads about 6,400 tx and runs in 0.6 s [T].
    • Mutants:
      • code mutant lowWatermark := highWatermark: killed ("trackedBytes=16774859 is above the low watermark 14260633") [T];
      • setup mutant (the old 3000-tx sizing): caught by the guard ("one pass must free 25.03% of the store, too close to the 25% cap") [T].
    • TestEstimateStoreTxBytes is green on the merged tree; the review already noted it was fine [T].
  4. N1: non-ASCII case folding (5555886e).

    • TestSubpathNonASCIIHopsFoldCase_413 checks appendLowerHop and matchLowerHop against strings.ToLower for ÄÖ, äö, aÄ, ẞx (which folds one byte shorter, so the match position must follow the folded length), an ASCII hop and an empty hop. It also checks a non-match.
    • Store level: ÄÖ/äö/Äö merge into one row of 3 and ẞX/ßx into one row of 2. The detail returns totalMatches 3 and 2 for the mixed-case queries.
    • Mutant M-A from the review (the utf8.RuneSelf fallback removed from both functions): killed [T].
  5. N2: maxLen reduction over groups (5555886e).

    • TestSubpathsBulkGroupOrderDoesNotCapLength_413 requires every bulk group to equal the same group requested on its own. It covers three group orders: longest first (the review's 5-8,4-4,3-3,2-2), longest in the middle, and a 2-8 group in the middle. It asserts that each group is non-empty.
    • Mutant M-B (maxLen = g.MaxLen): killed. Four groups differ [T].
  6. N3: PR description. "Deliberate differences" item 2 now states the behaviour:

    • Between FirstChunkReady (HTTP binds) and the end of LoadChunked, the three endpoints answer from the chunks merged so far. Master answered 503 there until the whole hot window was merged and the index was built (buildSubpathIndex and then markIndexesReadySync at the end of LoadChunked on master) [K].
    • A partial answer can stay in subpathCache until StartupLoadDone, whose signalStartupLoadDone clears it through invalidateCachesFor{eviction: true} [K].
    • The "Perf justification" and "Coordination" sections are updated for round 2.
  7. N4 (f4b81ed1). docs/specs/startup-performance.md:12 now reads "Build subpath index (removed by perf(store): evaluate on-demand subpath counts instead of permanent indexes #413: Route Patterns now count on demand)". I kept it as a historical measurement in the 7.3M-observation timing table rather than deleting the row [T].

  8. N5 (pre-existing case/cap divergence between the unfiltered list and window/region queries): unchanged, as disclosed in round 1.

  9. map[string]interface{} count. In non-test cmd/server code: master 8798f74e 734, merge commit 733, head 732 [T]. The first F1 commit spelled the result type out 8 times (741). b3ab5278 passes *cachedResult through the shared count instead, with no behaviour change; the F1 test and mutant were rerun [T].

Tests

Suite Result
cd cmd/server && go test ./... ok, 54.6 s, on f4b81ed1 (before the b3ab5278 refactor) [T]. On b3ab5278: 3 local runs failed; in the 2 I inspected, the only failure was TestRepeaterRelayInfoMap_ConcurrentIngestAndEviction, which is pre-existing (see below). With -skip on that one test the suite passes, ok, 55.1 s [T]. CI's three server shards passed [T]
cd cmd/ingestor && go test ./... ok, 210.6 s [T]
go vet ./... (server, ingestor) clean [T]
-race on Subpath|_413|1008|273|256|HashMigrate|Evict|Chunked|TrackedBytes|EstimateStoreTx|NoSQLUnderStoreLock|TieOrder|_113 ok, 176 tests passed, 0 data races, on b3ab5278 [T]
TestNoSQLUnderStoreLock, TestNoSQLUnderStoreLockDetects (-race) pass [T]
sh test-all.sh 260 passed, 0 failed (260 files) [T]
node test-frontend-helpers.js 719 passed, 0 failed [T]
test-e2e-playwright.js (local Go server, fixture prepared as in CI: freshen, the workflow's seed SQL, corescope-migrate, the four seed files) 154/157 passed, 3 skipped, incl. "Analytics Route Patterns tab renders content" [T]
test-a11y-1705-subpath-hop-prefix-e2e.js, test-issue-1705-subpath-contrast-e2e.js pass [T]
API diff: master binary vs this branch, separate copies of the same prepared fixture 394 of 394 responses byte-identical [T]. Covered: 10 /subpaths length/limit sets incl. 2–63, 9–63 and 0–0; 5 windows; 4 bulk sets incl. the reversed tab order and window=7d; 359 subpath-detail queries (every raw key of the top-200 list and the top-200 30-day list, plus mixed case, unknown, single-hop and empty)

Pre-existing flaky test (not #271 or #301). TestRepeaterRelayInfoMap_ConcurrentIngestAndEviction ("bulk relay info matches no store state between rounds") is load-dependent and unrelated to this PR:

  • isolated, 100 runs: master 2, PR head cb8ad366 3, this head 3 [T];
  • master and this head run side by side under the same load, 200 runs each: master 63, this head 57 [T].

It touches no subpath code [K]. It did not fail in CI. I have not filed an issue for it.

Mutants (round 2): 6 run, 6 killed

Mutant Killed by
F1: singleflight bypassed TestSubpathColdMissesShareOneCount_413 (8 counts, want 1)
F2: detail matching back under RLock TestSubpathCountingReleasesStoreLock_413/subpath-detail-match
F3: lowWatermark := highWatermark TestMemoryEviction_CrossesHighAndReachesLow_113
F3: old 3000-tx setup the new setup guard in the same test (25.03%)
N1 (M-A): non-ASCII fallback removed TestSubpathNonASCIIHopsFoldCase_413
N2 (M-B): maxLen = g.MaxLen TestSubpathsBulkGroupOrderDoesNotCapLength_413

CI per job: run 40060602106, head b3ab5278, first attempt, no reruns

Job Result
Go Build & Test pass
Go Server Tests (1/3), (2/3), (3/3) pass, pass, pass
Go Ingestor Tests pass
Go Libraries & CLI pass
Verify Go Test Assignments pass
JavaScript Unit Tests & Lint pass
Script & Container Unit Checks pass
Playwright E2E Tests (1/3), (2/3), (3/3) pass, pass, pass
Verify E2E Shards & Coverage pass
Validate Docker Image pass
Build & Publish Docker Image pass
Release Artifacts, Publish Badges & Summary, Deploy Staging skipped

15 passed, 3 skipped, 0 failed. Neither #271 nor #301 appeared [T].

Commits this round

004a5644 merge origin/master · 0aa39c41 F3 · dfdec4ea F1 · 17d8401e F2 · 5555886e N1/N2 · f4b81ed1 N4 · b3ab5278 F1 refactor (no new map[string]interface{}). All are authored by dborup <kontakt@meshview.dk>. There was no rebase, amend or force-push. The PR stays a draft.

Not verified

  • Anything above 30,000 transmissions.
  • Real multi-client HTTP load: the concurrency numbers come from in-process goroutines.
  • The startup window in N3 was read in the code, not exercised.
  • No staging or production access.
  • There was no browser check beyond the three E2E files; no frontend file changed.

@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Review — CS-Macmini PR#529 — head b3ab527

Dom: APPROVE med nits

Independent, read-only re-review of round 2 against my round-1 review of cb8ad366. git ls-remote gave b3ab5278 before and after; cb8ad366 is still an ancestor, so there was no rebase, amend or force-push. origin/master (8798f74e, which carries #411) is already merged in, and git merge-tree --write-tree origin/master b3ab5278 returns exactly the head tree, so the merged tree and the head tree are the same thing — everything below ran on git archive of b3ab5278 and, where a comparison was needed, on git archive of origin/master. The round-2 delta I assessed is git diff 004a5644 b3ab5278 (7 files: store.go, 4 test files, tracked_bytes_accounting_113_test.go, one docs line), i.e. the master merge excluded. Evidence tags: [T] run and observed, [A] derived from measured data, [K] read in the code or the diff.

All three findings are resolved, and the two measurements I pushed back on in round 1 now come out better than master rather than merely better than the round-1 head. The remaining items are test gaps and one documentation number.

Findings

# Round 1 Severity now Where Status
F1 must fix resolved store.go loadSubpaths, shareSubpathCount Peak heap is flat in N and below master at every N. Reproduced.
F2 should fix / decide resolved for merge store.go SubpathDetail Worst-case lock hold now 2.4–2.5× better than master at 30K/100K/200K tx. The partial disagreement is accurate and the right call.
F3 must fix resolved tracked_bytes_accounting_113_test.go Cause confirmed exactly; assertions unchanged; the 25 % cap stays covered elsewhere.
N1 nit resolved matchLowerHop, appendLowerHop My M-A now dies 10 ways.
N2 nit resolved countSubpathGroups My M-B now dies 5 ways.
N3 nit resolved PR body The startup window is stated.
N4 nit resolved docs/specs/startup-performance.md:12 Annotated as removed, kept as a historical row.
N5 nit (pre-existing) unchanged store.go Unchanged, as disclosed.
R1 — nit (new) store.go shareSubpathCount The 1: / n: flight-key prefixes are load-bearing and untested: drop them and the server panics.
R2 — nit (new) store.go shareSubpathCount The re-check of subpathCache inside the flight has no test.
R3 — nit (new) store.go SubpathDetail Match membership is as of the first RLock, the tx field values as of the second.
R4 — nit (new) tracked_bytes_accounting_113_test.go The new setup guard cannot fire, and its comment says 20.4 % where the bound is 20.0 %.
R5 — nit (residual) store.go SubpathDetail The hold is now O(N_tx) uniformly, so a 1-match key costs 1.83 ms at 200K tx where master cost 6–64 µs.

F1 — concurrent cold misses

Correctness. I went through the four things worth worrying about:

  • Across keys. The flight key is a pure function of the cache key ("1:"+cacheKey), and the cache key already fully determines (region, minLen, maxLen, limit, window) [K]. A zero window delegates to GetAnalyticsSubpaths, so the |w= suffix only ever appears for a non-zero window; region bulk delegates per group to GetAnalyticsSubpaths and windowed bulk to GetAnalyticsSubpathsWithWindow, so both are covered by the 1: space, and the n: space is only reachable with region == "" [K]. The n: key is strings.Join(cacheKeys, ";") where every element is |<min>|<max>|<limit>; ; cannot occur inside an element, so the join is injective and all sharers of one flight see the same group list in the same order [K]. Two bulk calls listing the same groups in different orders get different flight keys — a missed dedup, never a wrong answer [K]. The flight therefore adds no collision class that subpathCache did not already have on master.
  • Panic or error in the flight. count never returns an error, and singleflight.Group.doCall (x/sync v0.10.0) deletes g.m[key] in a deferred function on the normal, panic and runtime.Goexit paths alike, and re-panics in every waiter before Do returns a value — so a panic cannot poison a later call and v.(T) can never run on a nil value [K].
  • The re-check inside the flight. subpathCachedLocked is called under s.cacheMu with defer Unlock for the single-key path, and allCached does the same for bulk [K]. The returned *cachedResult has data written before the flight returns (so the sharers read it behind the singleflight happens-before) and only expiresAt is touched afterwards, always under cacheMu; putCachedResultLocked only evicts and assigns, it never mutates an entry [K]. -race is clean (below).
  • Metrics. cacheMisses is still incremented once per caller that missed, exactly as on master, so the hit-rate counters do not drift — only the number of counts changed, and that was never a counter [K].

Peak heap, reproduced. My own probe, not the author's benchmark: the identical Go file compiled in both trees, differing only by a one-line prepare that calls buildSubpathIndex on master. subpath413Data(30000, 2000, 413), Go 1.27.1 darwin/arm64, peak live heap (/memory/classes/heap/objects:bytes sampled every 200 µs) during N concurrent cold /subpaths?minLen=2&maxLen=8&limit=100 for the same uncached key, 3 runs each [T]:

N master 8798f74e this head round-1 head (cb8ad366)
1 185.2 / 208.3 / 208.4 MB 136.1 / 147.2 / 157.5 MB 151.5 / 158.7 / 163.6 MB
4 450.1 / 454.7 / 448.1 MB 153.2 / 147.3 / 154.6 MB 463.2 / 471.8 / 489.8 MB
8 824.4 / 793.0 / 764.7 MB 159.3 / 145.2 / 137.3 MB 886.0 / 951.6 / 954.5 MB

Peak is flat in N, 5.1–5.8× below master at N=8, and the N=8 crossover I measured in round 1 is gone [A]. Wall time at N=8 drops from 587–607 ms (master) to 247–256 ms [T], and at-rest HeapInuse on the same data is 90.7 MB on master against 25.2 MB here [T]. The claim that the saving now holds at every N is correct.

Tests. TestSubpathColdMissesShareOneCount_413 is red before and green after: with shareSubpathCount reduced to a direct count() call, all three subtests report "8 concurrent misses ran 8 counts, want 1" [T]. The test uses a WaitGroup rather than a sleep, and its closing cache-hit assertion cannot underflow that WaitGroup because rfCacheTTL is a minute in the fixture store [K].

F2 — the detail's lock hold

I accept the partial disagreement. The first hold is indeed still O(N) pointer-and-slice-header copies rather than O(matches) [K], but the measured consequence is that the endpoint is now better than master in the worst case, and the author's reason for stopping there (taking candidates from byPathHop needs a full-scan fallback while pathHopReady is false and changes the matched-transmission order, which would give up the byte-identical equivalence the PR rests on) is sound [K].

Worst s.mu.Lock() wait a writer sees during one detail request, same probe in both trees, 3 runs, identical hop lists in both trees [T]:

store key master this head
30K tx hot pair (9e,9f) 595–700 µs 267–285 µs
30K tx cold 5-hop, 1 match 6–29 µs 236–252 µs
100K tx hot pair 2.39–2.49 ms 918–945 µs
100K tx cold 5-hop 10–32 µs 884–890 µs
200K tx hot pair 4.56–4.57 ms 1.81–1.88 ms
200K tx cold 5-hop 12–64 µs 1.82–1.83 ms

Read: for a key with matches the hold is 2.4–2.5× shorter than master at every size, because master held one RLock across the whole request including the per-occurrence context build, whereas this one holds it for a pointer copy. Head's curve is ~9 ns per transmission, which confirms the author's "about 8 ns/tx" figure [A]. The regression is confined to rare keys, where master's spTxIndex lookup was essentially free; the crossover is at roughly 0.15 % of the store in matches [A]. The absolute worst case at 200K tx is 1.88 ms here against 4.57 ms on master, and both are an order of magnitude under the list path's 26.5 ms hold [A]. That is comfortably good enough to merge; R5 records the residual.

Three things I checked in the refactor itself:

  • The new len(hops) >= 2 filter is behaviour-preserving: subpathOccurrences loops for start := 0; start+2 <= len(hops); start++, so a path shorter than two hops could never produce a match [K].
  • The hopContext sharing between adjacent occurrences of one transmission is safe: occurrences of a tx are appended consecutively, and hopContext is only ever read (resolveWithContext), never appended to, so the aliasing cannot corrupt a sibling [K].
  • -race over the affected tests is clean, and TestSubpathOnDemandConcurrentWriters_413 passes [T].

Red before, green after. With the first RUnlock moved back behind the matching loop (the round-1 structure), TestSubpathCountingReleasesStoreLock_413/subpath-detail-match fails with "a store writer could not take PacketStore.mu while the detail matched" while the other five subtests stay green [T].

F3 — the #113 watermark test

The explanation holds; I reproduced every number. On this head, 3000 acct113CreateDB transmissions track 8,322,465 B (7.937 MiB), maxMemoryMB lands on 7, high is 7,340,032 and low 6,239,027, so one pass must free 25.03 %. evictStaleInternal caps a pass at len(s.packets)/4, the pass evicts exactly 750 of 3000 (25.00 %) and stops at 6,244,604 — 5,577 B above low [T]. That is the author's failure message to the byte, and my round-1 arithmetic.

It is not obsIndexBytes. I instrumented the fixture: obsIndexBytes(tx) != 0 for 0 of 3000 transmissions, with at most 4 observations per tx against obsIndexThreshold = 32 [T]. What moved T is that #411 stopped charging the eager per-tx dedup maps into chargedBytes; obsIndexBytes is the replacement term and it is zero at this fixture's observation counts [K]. The author's phrasing is precise.

The fix does not weaken the test. Every assertion after the setup is byte-identical to master's: crossing the high watermark (tracked <= high → Fatal), reaching low (trackedBytes > low → Error), not overshooting low by more than eight average packets, and something actually being evicted, plus acct113Check [K]. Only the sizing changed, and the overshoot tolerance is unchanged in absolute terms while low is now twice as large, so that bound is relatively tighter than before [A]. Both of the author's mutants reproduce: lowWatermark := highWatermark gives "after the pass trackedBytes=16774859 is above the low watermark 14260633" [T], and reverting the setup to the literal 3000-tx sizing trips the new guard with "one pass must free 25.03 % of the store, too close to the 25 % cap" [T].

And the 25 % cap keeps its own coverage. eviction_test.go pins it independently in two places, including TestEvictStale_MemoryBasedEviction_UnderestimatedHeap, which asserts evicted != 250 on a 1000-packet store [K]. So moving the _113 test off the cap boundary gives up no coverage; it just stops conflating the watermark contract with the cap contract. That is the right call.

The helper is also robust to the estimate moving in either direction: it rescales n from a first load and Fatals after three attempts, so a much larger per-tx estimate keeps n at 3000 and a much smaller one scales n up [K]. The test now loads 6,351 transmissions tracking 16.81 MiB, with a required share of 19.09 % [T].

N1, N2 — my round-1 mutants

Both now die, and specifically:

  • M-A (the >= utf8.RuneSelf fallback removed from both matchLowerHop and appendLowerHop): TestSubpathNonASCIIHopsFoldCase_413 fails on 10 assertions across all three levels — the helpers (appendLowerHop("ÄÖ") = "p,ÄÖ", want "p,äö"), the store list (5 rows of 1 instead of 2 merged rows of 3 and 2) and the detail (totalMatches = 1, want 3) [T]. The ẞ → ß case is the valuable one, since it is the fold that shortens the byte length and so pins the match position rather than just the bytes [K].
  • M-B (maxLen = max(maxLen, g.MaxLen) → maxLen = g.MaxLen): TestSubpathsBulkGroupOrderDoesNotCapLength_413 fails on 5 groups across the 3 orders [T].

New nits in the round-2 delta

R1 — the 1: / n: flight-key prefixes are load-bearing and untested. shareSubpathCount is generic and ends in v.(T), with T = *cachedResult for the single-key space and T = []*cachedResult for bulk. The two spaces are kept apart only by those prefixes. Without them the single-key cache key for region == "" is |2|8|100, which is character-for-character the joined key of a one-group bulk request for the same lengths — and /subpaths-bulk lets a caller ask for exactly one group. I removed both prefixes and the whole cmd/server suite stayed green (ok, 64.5 s) [T]; a focused probe that parks a /subpaths and a one-group /subpaths-bulk in the same flight slot then panics with interface conversion: interface {} is []*main.cachedResult, not *main.cachedResult [T]. Nothing is wrong with the shipped code — the prefixes are there — but the one invariant whose failure mode is a server panic rather than a wrong number has no test. One assertion that a concurrent single-key and one-group-bulk request for the same lengths both return would close it.

R2 — the in-flight cache re-check has no test. Disabling the if v, ok := cached(); ok branch inside the flight leaves the full suite green (ok, 65.0 s) [T]. The property is perf-only (the caller counts again instead of reusing a result stored a moment earlier, and the answer is still correct), which is why I file it as a nit rather than alongside R1, but the author's claim for it is a [K] and it is worth a [T].

R3 — the detail now spans two lock holds. Which transmissions matched, and on which hop list, is as of the first RLock; FirstSeen, ObserverID, SNR and RSSI are read under the second [K]. A writer that re-resolves a path in between (setTxPath / reindexTxPath) can leave a transmission in the answer whose current path no longer holds the subpath. Master held one RLock and had no such window. It is the same class of trade the list path already makes and the code comment says so plainly, so I would not change it — but it is a behaviour difference beyond "the lock is held less", and the "Deliberate differences" list does not mention it.

R4 — the new setup guard cannot fire. With acct113LoadTracking(t, 16*1048576) in place, T ≥ 16 MiB so maxMemoryMB ≥ 16, and the required share is bounded by 1 − 0.85·16/17 = 20.0 % whatever the per-tx estimate does — strictly under the guard's 0.21. I confirmed this: lowering the helper's target to 8 MiB does not trip the guard (the ×1.05 overshoot keeps the share at ~19 %), and only reverting to the literal 3000-tx sizing does [T]. So the helper is the protection and the guard documents the invariant rather than enforcing it, which is fine as defensive code but is not the loud-failure net the report describes. Related: the comment's "between 15 % and 20.4 %" should read 20.0 % — the bound is 1 − 0.85k/(k+1) at k = 16 [A].

R5 — residual, the detail hold is now O(N_tx) for every key. Numbers in the F2 table. At 200K transmissions a 1-match detail request holds the store lock for 1.83 ms where master held it for 12–64 µs. The worst case is still better than master's and the figure is 14× under the list path's hold, so this is not a merge blocker — it is the shape of the trade, and the byPathHop follow-up the author describes is the thing that would remove it.

Equivalence, re-checked for the round-2 delta

The delta rewrites SubpathDetail, so I re-verified equivalence two ways rather than relying on round 1.

  1. In-process, cross-tree, on the synthetic corpus. One identical probe file compiled in both trees, with the key list derived from the data (every distinct 2-to-8-hop raw key of every packet, 414,243 of them — which matches the PR body's own key count for the removed index) rather than from the API, so the list is identical in both trees by construction. 408 SubpathDetail responses — a deterministic spread of 401 keys plus upper-cased variants, a concatenated key, an unknown key, an empty key, a single hop and a 9-hop key past the cap — serialise to 736,061 bytes in both trees and cmp reports them identical [T].
  2. Over HTTP, on the CI-prepared fixture. Two local Go servers from the origin/master binary and this head's binary against separate copies of the same e2e-fixture.db, prepared as in CI (freshen, the workflow's seed SQL, corescope-migrate, and all four seed files including 2073 and 199): 409 of 409 responses byte-identical [T] — 10 /subpaths length/limit sets including 2–63, 9–63, 0–0 and the maxLen=200 ceiling clamp, 6 windows, 7 subpaths-bulk sets including the reversed tab order, a single group, a group in the middle, a windowed set and the two error shapes, and 380 subpath-detail queries (371 raw keys from the unfiltered and 30-day top-200 lists, upper-cased variants of the first 10, and the hand-made edge cases).

The live fixture has no configured regions, so the HTTP pass did not exercise region filtering this round; the region path is covered by the repo's own _413 equivalence subtests, and my R2 mutant below was caught there.

Tests and mutants

All on b3ab5278 (= the merged tree against origin/master 8798f74e), Go 1.27.1 darwin/arm64.

Suite Result
cd cmd/server && go test ./... ok, 65.2 s — no flake, TestRepeaterRelayInfoMap_ConcurrentIngestAndEviction included [T]
cd cmd/ingestor && go test ./... ok, 108.1 s [T]
go vet ./... (server, ingestor) clean [T]
-race, affected tests (`Subpath _413
TestNoSQLUnderStoreLock, TestNoSQLUnderStoreLockDetects (-race) pass [T]
sh test-all.sh 260 passed, 0 failed (260 files) [T]
node test-frontend-helpers.js 719 passed, 0 failed [T]
test-e2e-playwright.js (local Go server, CI-prepared fixture) 154/157 passed, 3 skipped, incl. "Analytics Route Patterns tab renders content" [T]
test-a11y-1705-subpath-hop-prefix-e2e.js, test-issue-1705-subpath-contrast-e2e.js pass [T]
CI per job, run 40060602106, head b3ab5278, run_attempt=1 15 success, 3 skipped (release, badges, deploy), 0 failed. Neither #256 nor #267 appeared; nothing failed at all, so there were no reruns to inspect [T]

My own mutants this round: 6 run, 4 killed, 2 survive as test gaps.

Mutant Result
R-A: both 1: / n: flight-key prefixes removed survives the full suite; panics under a concurrent single-key + one-group-bulk request → R1 [T]
R-B: in-flight cached() branch disabled survives the full suite (perf-only) → R2 [T]
R-C: if i > 0 && hits[i-1].tx == tx → if i > 0 (share hopContext across different transmissions) killed by TestSubpathsOnDemandMatchLegacyIndex_413/seed2/detail [T]
R-D: shareSubpathCount reduced to a direct count() killed by TestSubpathColdMissesShareOneCount_413, all 3 subtests [T]
R-E: first RUnlock moved behind the matching loop killed by TestSubpathCountingReleasesStoreLock_413/subpath-detail-match [T]
R-F: lowWatermark := highWatermark (both sites) killed by TestMemoryEviction_CrossesHighAndReachesLow_113 [T]

I also re-ran my two round-1 survivors, M-A and M-B; both are now killed, as recorded under N1/N2 [T].

Invariants

Check Result
cmd/server stays read-only the round-2 delta adds no Exec/INSERT/UPDATE/DELETE/CREATE in non-test Go [T]
new map[string]interface{} outside tests none — 715 in cmd/server non-test on master, 713 here; the one added line is the pre-existing results slice moved inside the new branch [T][K]
hardcoded colours no public/** change in the round-2 delta at all [T]
scripts/check-xss-sinks.sh --diff origin/master clean ("no public/**/*.{js,html} changes to scan") [T]
fork guards 9 repository == in deploy.yml, 1 in release-fast-path.yml; .github/ untouched by the delta [T]
closing keywords none in the PR body; it says "Relates to #413" [T]
commit author all 7 round-2 commits are dborup <kontakt@meshview.dk>, author and committer [T]
stale index references one line left in *.md, and it is the N4 line annotated as removed [T]
PR state still a draft [T]

Not verified

  • Anything above 200,000 transmissions. The F2 hold is linear in the store, so the R5 figure extrapolates, but I have no data at the scale the configuration docs warn about.
  • Real multi-client HTTP load. The F1 concurrency numbers come from in-process goroutines, not from concurrent HTTP clients.
  • The author's upkeep figures (6.0 µs ingest, 13.0 µs eviction, 285 ms rebuild). I confirmed the at-rest heap the index held (90.7 MB against 25.2 MB on the same data) but did not re-time the upkeep.
  • Region filtering over HTTP this round — the live fixture has no configured regions; see the note under Equivalence.
  • The LoadChunked background-fill window (N3) was read in the code, not exercised.
  • TestRepeaterRelayInfoMap_ConcurrentIngestAndEviction, which the author reports as a pre-existing load-dependent flake, did not fail in any of my runs, so I neither reproduced nor refuted it.
  • No staging or production access, and no browser check beyond the three E2E files listed.

@dborup
dborup marked this pull request as ready for review October 24, 2026 05:46
@dborup
dborup merged commit c48510b into master Oct 24, 2026
18 checks passed
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.

3 participants