Conversation
traffic_share_score grows with uptime because every observation of a transmission appends it to its relays' byPathHop buckets again, while the denominator counts it once. These tests are red on master: - a transmission ingested once and then heard by 10 more observers through the same relays is in each relay bucket exactly once, via the late-observation path (IngestNewObservations) and the live-ingest batch path (IngestNewFromDB); - the traffic share of a fixed transmission set is identical right after Load and after any number of extra observations, equal to the definition, sums to at most the longest path and never hits the 1.0 clamp; the three score functions agree; - byPathHop does not grow with observations of known transmissions; - the score counts distinct transmissions even if the index held duplicates. Two further tests (green on master) pin that GetNodeHopAnalytics, GetRepeaterNodeStatsBatch relay info and computeMultiByteCapability are unaffected by duplicate entries. Benchmarks for the late-observation index step, the real IngestNewObservations path and the bulk score pass give before/after numbers. setupTestDB takes testing.TB so the DB-backed benchmark can reuse it. Relates to #158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykybhB9Hkxz692fbADZMi
indexResolvedPathHops runs once per observation, and every observation of a transmission that resolved to the same relays appended it to their byPathHop buckets again. traffic_share_score divides that entry count by the number of non-advert transmissions, so it grew with uptime until a restart rebuilt the index (prod: the sum over all repeaters reached 235.8 after 98 h, many clamped at 1.0). - addResolvedPubkeysToPathHopIndex keeps, per transmission, the hashes of the resolved keys it has appended the transmission under (PacketStore.pathHopResolved) and skips keys already there. Bounded: one 8-byte hash per relay key per live transmission. StoreTx is unchanged (its 320-byte layout is pinned by tests). - Eviction deletes the record of evicted transmissions next to the #115/#144 bucket sweep; retainResolvedPathHops drops the record of transmissions that are no longer in s.packets, so it stays in step with what a rebuild carries over. - Defence in depth: GetRepeaterUsefulnessScore, computeRepeaterUsefulnessScoreMap and GetRepeaterNodeStatsBatch count distinct non-advert transmissions (countDistinctNonAdvert), matching the denominator. - GetNodeHopAnalytics, the batch relay info and computeMultiByteCapability already deduplicate (or only take a maximum over raw prefix buckets) and are unchanged. - The #115 eviction tests encoded the duplicates (3+3*2 entries per transmission); they now expect one entry per key. A repeat observation through known relays no longer mutates byPathHop, so it no longer invalidates the batch relay-stats cache either (the Kpa-clawbot#1164 contract: invalidate when byPathHop changes). Relates to #158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykybhB9Hkxz692fbADZMi
) The defensive distinct count from the previous commit used a map per byPathHop bucket. BenchmarkTrafficShareScoreMap_158 showed the bulk traffic-share pass going from 1.4 ms to 9.5 ms (20k txs) and 8.8 ms to 100 ms (100k txs, 18.7 MB allocated per pass), all under the read lock. countDistinctNonAdvert now collects IDs into a reused slice; buckets are appended in ingest order, so strictly increasing IDs already prove them distinct and only out-of-order buckets (background chunk loads) are sorted. The bulk pass counts distinct transmissions for full-pubkey keys only: those are the resolved hops that could hold duplicates and the only keys a repeater's score is read under; raw hop buckets get one entry per transmission from addTxToPathHopIndex and are counted directly, as before. Bulk pass vs master (benchstat, n=6+8): +26..31% at ingest order, +25..56% with every bucket reversed (8.8 -> 11.1 ms at 100k txs); allocations unchanged (213 KiB). A unit test pins the helper on ascending, descending, adjacent and interleaved duplicates, adverts and nils; the benchmark gains an order=reversed case. Relates to #158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QykybhB9Hkxz692fbADZMi
…ies (#158) After #158 the #115 fixture holds no duplicates, so a sweep that removed only the first occurrence of an evicted transmission per bucket survived. Insert the same transmission twice more into a resolved bucket (the shape of an index built before #158) and require every occurrence to go and an emptied bucket to be deleted. Behaviour is unchanged, so the test also passes on master. Relates to #158 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRiEwpZQZpYPxKTJRJAvc5
Owner
Author
|
Review feedback addressed (commit
The commit changes one test file only; no production code. Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relates to #158
Cause
traffic_share_scoreis|non-advert txs in byPathHop[pubkey]| / |non-advert txs in byPayloadType|, clamped to 1. The two sides counted different things:indexResolvedPathHopsruns once per observation, on both the live-ingest path (IngestNewFromDB, several observations in one batch) and the late-observation path (IngestNewObservations).addResolvedPubkeysToPathHopIndexonly deduplicated within one call (hopsSeenis cleared on entry). So every further observation through the same relays appended the transmission to their buckets again.Loadand the background chunk load end inbuildPathHopIndex, and itsretainResolvedPathHopsdeduplicates by*StoreTx. That is why values looked right after a restart and then drifted upwards with uptime (prod: the sum over all repeaters was 235.8 after 98 h, many repeaters clamped at 1.0).Reproduced on master
be35eefb: after 11 observations, every relay bucket holds the transmission 11 times, and all four fixture relays sit at the 1.0 clamp (their correct values are 0.4–0.5).Design
Plan as given in the issue and the task. The work was requested autonomously, so the plan is written here instead of waiting for sign-off (AGENTS.md rule 5).
1. Idempotent insert (
cmd/server/store.go).addResolvedPubkeysToPathHopIndexkeeps, per transmission, the hashes of the resolved keys it has already appended the transmission under, in the new fieldPacketStore.pathHopResolved map[*StoreTx][]uint64, and skips keys already recorded.StoreTxis unchanged. Its 320-byte layout is pinned byTestStoreTxLayoutFits*, so the record is a side map rather than a struct field.*StoreTx, like theevictedTxSetthat eviction already uses and the dedupe inretainResolvedPathHops.2. Consistent with eviction (#115/#144) and rebuild.
evictStaleInternaldeletes the record of evicted transmissions, next to the existing prefix-scoped bucket sweep. That sweep is unchanged and still removes any legacy duplicates.retainResolvedPathHopscarries the resolved entries of live transmissions over, so their record stays valid. It drops the record of transmissions no longer ins.packets, and clears it when there is no previous index.3. Defence in depth.
GetRepeaterUsefulnessScore,computeRepeaterUsefulnessScoreMapand the score inGetRepeaterNodeStatsBatchcount distinct non-advert transmissions viacountDistinctNonAdvert.addTxToPathHopIndexand are counted as before.4. Audit of the other
byPathHopconsumers.GetRepeaterNodeStatsBatch,ScoreTestTrafficShareCountsDistinctTransmissions_158,TestTrafficShareStableAcrossLateObservations_158GetRepeaterNodeStatsBatch,Info(collectRelayEntriesLocked)tx.IDTestPathHopConsumersIgnoreDuplicateEntries_158GetNodeHopAnalyticstx.IDTestPathHopConsumersIgnoreDuplicateEntries_158computeMultiByteCapabilityTestPathHopConsumersIgnoreDuplicateEntries_158,TestMultiByteCapabilityIgnoresDuplicateEntries_158handleNodePaths,computeRepeaterRelayInfoMap,updateIndexedRelayActivityLockedtx.ID(read, no test added)Behaviour change to note.
byPathHop, so it no longer invalidates the 300 s batch relay-stats cache either. This follows the perf(nodes): batch relay stats to fix O(N×M) /api/nodes regression Kpa-clawbot/CoreScope#1164 contract (invalidate whenbyPathHopchanges) andTestAddResolvedPubkeysToPathHopIndex_NoMutation_PreservesCache. Before, practically every late observation cleared that cache.3+3*2entries per transmission). They now expect one entry per key.TestEvictRemovesDuplicateResolvedEntries_115(commit 4) inserts duplicates itself, so eviction of a legacy index is still pinned.No change to the score definition or weights. No new
map[string]interface{}. No writes from the server (mode=rois untouched).Commits
610e70e4: tests, red on master. Also benchmarks;setupTestDBnow takestesting.TBso the DB-backed benchmark can reuse it.b671ee14: the fix, the record tests, and the updated fix(store): remove evicted transmissions from resolved byPathHop entries #115 expectations.682cd623: perf. The distinct count no longer uses a map per bucket; adds a helper unit test and a reversed-order benchmark case.baa68cdd: test only.TestEvictRemovesDuplicateResolvedEntries_115(review nit N1).Tests
The fixture is a real SQLite DB with 4 relays (distinct first bytes, so a raw 1-byte hop resolves uniquely), 11 observers and 20 non-advert transmissions with paths of 1–3 hops. Both live-ingest paths are covered, and both are compared to a
Loadof the same data with persistedresolved_path.TestPathHopIndexOncePerTx_LateObservations_158IngestNewObservations→ once per relay bucketTestPathHopIndexOncePerTx_LiveIngestBatch_158IngestNewFromDBbatch → onceTestPathHopIndexOncePerTx_AfterLoad_158Load(with rebuild), then a live observation → onceTestTrafficShareStableAcrossLateObservations_158TestPathHopIndexSizeBoundedByTransmissions_158byPathHopentries do not grow with observationsTestTrafficShareCountsDistinctTransmissions_158TestCountDistinctNonAdvert_158TestPathHopResolvedRecordBounded_158TestPathHopResolvedRecordAcrossRebuild_158TestPathHopConsumersIgnoreDuplicateEntries_158,TestMultiByteCapabilityIgnoresDuplicateEntries_158TestEvictRemovesDuplicateResolvedEntries_115727efca0as well (behaviour unchanged)Red before / green after
be35eefb(the 8 tests that compile there)The 2 that pass on master are the audit tests for unaffected consumers, which is the intent.
Runs
-run '_158|_115'):-count=10ok, and-race -count=10ok (also onbaa68cdd).cmd/server,go test -race -count=1 ./...:ok github.com/corescope/server 847.813s;origin/master727efca0:ok github.com/corescope/server 881.643s(merge commit16a65007).baa68cdd, non-racego test -count=1 ./...incmd/server: ok (68.8 s). One earlier run failedTestHandleNodePaths_HopName_CanonicalPathShowsTarget_1144with a 503index loading; it failed 2 of 20 runs on master727efca0too, and passed 20 of 20 in isolation on this branch, so it is an existing timing flake, not caused by this PR.cmd/ingestorgo test ./...ok.Merge with
origin/master727efca0(one new commit, which only changestest-analytics-fluid-charts.js):go vetok,cmd/ingestorgo test ./...ok,node test-packet-filter.jsok,node test-aging.jsok.node test-frontend-helpers.js: 705 passed, 2 failed (favStar …). It fails identically on plain masterbe35eefb. Unrelated; this PR touches no frontend file.Mutants
addResolvedPubkeysToPathHopIndex_158, 5 ×_115)TestPathHopResolvedRecordBounded_158countDistinctNonAdvertcounts entriesTestTrafficShareCountsDistinctTransmissions_158…RecordAcrossRebuild_158,…OncePerTx_AfterLoad_158…RecordAcrossRebuild_158TestTrafficShareCountsDistinctTransmissions_158TestTrafficShareCountsDistinctTransmissions_158TestTrafficShareCountsDistinctTransmissions_158<=→<TestCountDistinctNonAdvert_158TestEvictRemovesDuplicateResolvedEntries_115(only that test)Perf (AGENTS.md rule 0)
Correction. An earlier version of this section claimed the branch is faster on the late-observation paths (−10 to −20 %). That is not reproduced and is withdrawn. A reviewer measured on darwin arm64 (10 cores, load < 4, n=6, median) and found the opposite direction on the bulk score pass; remeasured below.
Method. Master
727efca0(with this branch's_158benchmark file and thesetupTestDB(testing.TB)change copied in so the same benchmarks compile) against branch682cd623(the production code ofbaa68cdd; that commit is test-only). Test binaries built once, then 6 interleaved rounds (master, head, master, head, …),-test.benchtime 1s -test.benchmem.benchstatis not installed here, so: median of n=6 with [min–max]. No significance test.LateObservationIndex_158/txs=20000LateObservationIndex_158/txs=100000IngestNewObservations_158TrafficShareScoreMap_158/txs=20000/order=ingestTrafficShareScoreMap_158/txs=100000/order=ingestTrafficShareScoreMap_158/txs=20000/order=reversedTrafficShareScoreMap_158/txs=100000/order=reversedReading.
GetRepeaterUsefulnessScoreMap) is slower on the branch, by +15 % to +60 %. This agrees in direction with the darwin arm64 measurement (ingest order 220 µs → 285 µs at 20K and 2.43 ms → 3.22 ms at 100K, +30–32 %; reversed +58–86 %). Absolute times differ with CPU and cache, and the percentages differ by platform (here the 100K cases are smaller, +15 % and +21 %); I have not isolated why (sort cost on the reversed buckets and cache behaviour are the likely factors, not verified). The direction is the same on both platforms, so the cost is real: it is the price of the defensive distinct count, paid under the read lock on a cached path.IngestNewObservations_158: +9 % here with overlapping ranges, ±0 % on darwin: unchanged within noise. Bytes per op, first run: 776,727 on master, 832,404 on the branch (+7 %); not investigated.Memory
pathhop-entries/txafter the benchmark's late observations: master 137.5 (20k) and 18.1 (100k), growing with every observation; branch 4.99 (3 raw + 2 resolved), constant.pathHopResolved) measured on the heap earlier: 81.6 B/tx (20k), 68.5 B/tx (100k), with 2 relays per transmission. On master each extra observation added 16 B/tx of duplicate pointers, without bound.estimateStoreTxBytes: the record's memory is not included intrackedBytes. A reviewer measured the record on darwin arm64 at 6–12 MB at 100K transmissions and 46–74 MB at 500K. This will be handled in a separate follow-up issue (it also covers the resolvedbyPathHopentries, whichestimateStoreTxBytesdoes not count today).Not verified (Ikke verificeret)
traffic_share_scorestays bounded (around the mean relay-hop count) and stops drifting over at least a day of uptime. It should also confirm that values no longer jump across a restart.Overlap with other open PRs
cmd/server/store.gois also changed by PR API: expose on-wire channelHashHex on channel messages #11 (codex/expose-channel-hash-hex).store.goauto-merges. That branch's existing conflict with master is inopenapi.goand is not caused by this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01VRiEwpZQZpYPxKTJRJAvc5