mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 16:09:57 +00:00
## Problem `buildHopContextPubkeys` (#1197) runs for every transmission in each topology and distance pass, inside `buildAggregateHopContextPubkeys` for every subpath query, and for each matched packet in subpath detail. On every call it: - made a de-dup map sized for 16 keys, to de-duplicate what's usually 2–5 keys; - lowered every hop into a new string just to look it up in the prefix map. Hops are upper-case hex on live data, so `strings.ToLower` always allocates; - lowered the observer ID twice, once to check it's hex and once to add it. ## Change - Duplicates are found by scanning the result slice. - Hops are lowered into a 64-byte stack buffer for the prefix-map lookup. `m[string(buf)]` doesn't allocate, and non-ASCII input falls back to `strings.ToLower`, so lookups are unchanged. - The observer ID is checked as hex in either case (`isHexAnyCase`) and lowered once. - The result is the same keys in the same order: sender, observer, then unambiguous hop anchors. ## Perf justification (AGENTS.md rule 0) - **Complexity.** The de-dup scan is O(k²) for the k keys of one transmission. k is at most 2 + path hops; MeshCore paths are under 64 hops and real values are 2–10. It replaces a map allocated on every call, and nothing grows with the number of transmissions or nodes. - **Scale.** It runs for every stored transmission in each analytics pass (topology and distance) and per request in subpath analytics, so the saving recurs on every recompute, not just at startup. Benchmarks, current master vs this branch, same machine: | | master | this PR | |---|---|---| | `BenchmarkBuildHopContextPubkeys` | 830 ns, 1192 B, 4 allocs | 507 ns, 128 B, 1 alloc | | `BenchmarkBuildAggregateHopContextPubkeys` | 5.6 ms, 6.65 MB, 20.4k allocs | 3.7 ms, 1.33 MB, 5.3k allocs | On a copy of a busy regional network's 7-day window (64k transmissions): | | master | this PR | |---|---|---| | this function's allocations over startup and the first analytics pass | 317 MB | 80 MB | | server CPU over the same run | 175.6 s | 165.6 s | ## Tests - `TestBuildHopContextPubkeysAllocatesOnlyItsResult` uses a fixture cased like live data (upper-case hops and observer ID): at most 2 allocations, the result and the lowered observer ID. Red commit first (9 allocations), then green. - `TestBuildHopContextPubkeysOrderAndDedup` pins the same keys in the same order. - The existing hop-context and disambiguation tests (#1198, #1202) pass. - `cd cmd/server && go test ./...` passes. - SonarQube: quality gate passed, no issues on added lines. Backend only: no UI, API shape or config change. 🤖 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>
54 lines
2.1 KiB
Go
54 lines
2.1 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
func hopContextFixture() (*StoreTx, *prefixMap) {
|
|
pm := buildPrefixMap([]nodeInfo{
|
|
{PublicKey: "aa11000000000000000000000000000000000000000000000000000000000001", Role: "repeater"},
|
|
{PublicKey: "bb22000000000000000000000000000000000000000000000000000000000002", Role: "repeater"},
|
|
{PublicKey: "bb33000000000000000000000000000000000000000000000000000000000003", Role: "repeater"},
|
|
})
|
|
tx := &StoreTx{
|
|
DecodedJSON: `{"pubKey":"cc44000000000000000000000000000000000000000000000000000000000004"}`,
|
|
ObserverID: "DD55000000000000000000000000000000000000000000000000000000000005",
|
|
PathJSON: `["AA","BB","aa","AA11"]`,
|
|
}
|
|
tx.ParsedDecoded()
|
|
txGetParsedPath(tx)
|
|
return tx, pm
|
|
}
|
|
|
|
// buildHopContextPubkeys runs per transmission in every topology and
|
|
// distance pass and per matched packet in subpath detail. Its de-dup map,
|
|
// sized for 16 keys, was allocated on every call (150MB of churn per
|
|
// analytics recompute on a busy week) to de-duplicate a handful of keys,
|
|
// and live hops and observer IDs are upper-case, so each was lowered into
|
|
// a new string just to look it up. The fixture is cased like live data.
|
|
func TestBuildHopContextPubkeysAllocatesOnlyItsResult(t *testing.T) {
|
|
tx, pm := hopContextFixture()
|
|
// The result slice, and the lowered observer ID it holds.
|
|
if n := testing.AllocsPerRun(200, func() { _ = buildHopContextPubkeys(tx, pm) }); n > 2 {
|
|
t.Fatalf("buildHopContextPubkeys allocates %.0f times per call, want at most 2", n)
|
|
}
|
|
}
|
|
|
|
// Same keys, same order: sender, observer, then unambiguous hop anchors,
|
|
// lower-cased and de-duplicated.
|
|
func TestBuildHopContextPubkeysOrderAndDedup(t *testing.T) {
|
|
tx, pm := hopContextFixture()
|
|
got := buildHopContextPubkeys(tx, pm)
|
|
want := []string{
|
|
"cc44000000000000000000000000000000000000000000000000000000000004",
|
|
"dd55000000000000000000000000000000000000000000000000000000000005",
|
|
"aa11000000000000000000000000000000000000000000000000000000000001",
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|