mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 05:20:03 +00:00
## Problem `relayCandidates` runs once per hop of every observation the cold loader resolves, and again from the direct-heard and neighbor analytics. Once any listener-only observer was marked (#1290), every call built a filtered copy of the candidate `nodeInfo` structs, even when none of the candidates was a listener. On a week of a busy regional network that accounted for 1.1 GB of the 5.4 GB allocated over one startup, the biggest single source of startup churn. ## Change `markNonRelay` now builds a map once, holding the filtered list for each prefix whose candidates include a listener. A listener can only be in the lists of its own prefixes and its full key, so only those are filtered: at most 9 lists per listener, not every list in the prefix map. `relayCandidates` returns either that list or the prefix map's own list, without copying. Every caller only reads the slice (checked: direct-heard, neighbor persist, node hop analytics and `resolveWithContext`). ## Measured on a copy of that week | | before | after | |---|---|---| | allocated over startup | 5.44 GB | 4.32 GB | | server CPU over the run | 175.6 s | 169.1 s | | peak RSS | 1074 MB | 1077 MB (unchanged; the GC limit sets it) | ## Cost per rebuild against cost per lookup `markNonRelay` now does once per node cache rebuild (as often as every 30 s, `nodeCacheTTL`) the filtering that master did on every `relayCandidates` call. 5,000 repeaters and 3 listeners, median of 6 runs (`BenchmarkMarkNonRelay5000`, `BenchmarkRelayCandidates256`): | | master | first version of this PR | this PR now | |---|---|---|---| | `markNonRelay`, once per rebuild | 0.59 µs, 320 B, 3 allocs | 14.8 ms, 6.57 MB, 36,285 allocs | 20 µs, 20.6 KB, 33 allocs | | one `relayCandidates` lookup | ~7.8 µs, 6.7 KB | ~30 ns, 0 B | ~30 ns, 0 B | | 256 lookups (one per 2-character hop) | 2.0 ms, 1.71 MB, 1,453 allocs | 7.7 µs, 0 B | 7.7 µs, 0 B | The extra ~20 µs per rebuild is paid back after about 3 lookups. Every ingested observation looks up each hop of its path, so a running server makes far more than that between rebuilds, and a cold load makes millions (the 1.1 GB above). ## Tests - `TestRelayCandidatesDoesNotAllocate`: zero allocations per call, and listeners still filtered. - `TestRelayCandidatesAfterSecondMark`. - `TestMarkNonRelayVisitsOnlyListenerPrefixes`: at most 80 allocations for 3 listeners among 5,000 repeaters (36,285 before the fix). It has its own red commit. - `BenchmarkMarkNonRelay5000` and `BenchmarkRelayCandidates256` give the table above. - `TestMarkNonRelayMatchesFilteringEachList`: every prefix of the 5,000-repeater map matches filtering its list on the spot. - Red commit first, then green. - `go test ./cmd/server` passes. No config or UI changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>