mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-30 00:17:59 +00:00
Rebase of #1881 by @SaarMesh-Bot onto current master. Their three commits are preserved, two of them cherry-picked with authorship intact; the sweep itself had to be regenerated. Opened as a new PR rather than force-pushing their branch. Closes #1881 once merged. Addresses parts 1 and 3 of #1859; part 2 landed as #1937. ## Why regenerated rather than merged The sweep in #1881 was cut on 2026-09-02 07:13 and roughly forty PRs landed after it, so it went `CONFLICTING/DIRTY`. Re-running `gofmt` on current master is cheaper and less error-prone than resolving 72 conflicts that are all whitespace. The drift it fixes also grew in the meantime: 66 files now, against 72 then, but spread differently. ## The three commits 1. **`style(#1859)`** — `gofmt -w` across the 14 modules. 66 files. 2. **`test(#1859)`** — @SaarMesh-Bot's fix for the one `go vet` copylocks finding, `cmd/ingestor/coverage_boost_test.go`: the range variable copied a `Config` embedding `sync.Once`. Cherry-picked unchanged. 3. **`ci(#1859)`** — @SaarMesh-Bot's CI step that fails on gofmt drift or vet findings, plus `.git-blame-ignore-revs`. Cherry-picked with one change, noted in the commit message: the ignore file pointed at `04bc80ee`, the sweep commit on their branch, which does not exist on this base and would make `git blame --ignore-revs-file` error. Repointed at `d3a02599`, the sweep here. ## Verification The claim "formatting only" is checked twice rather than asserted: - Every changed file is byte-identical to `gofmt(previous content)`. 0 of 66 deviate. - With line comments and all whitespace stripped, 0 of 66 files differ, so no code outside comments changed. 14 of the 66 also show doc-comment reflow. Since Go 1.19 `gofmt` re-indents indented comment blocks to tabs and inserts a blank comment line before them; the behavior matrix above `resolveHopWithContext` in `cmd/ingestor/path_resolver.go` is a clear example. That is gofmt's own output, not an edit, but it is worth naming because it makes the diff look larger than "whitespace" suggests. The gate was run locally exactly as the workflow runs it: `gofmt` clean, and `go vet` clean in all 14 modules, including `cmd/ingestor` which is what commit 2 fixes. Suites: `cmd/server` ok (80.7s), `internal/packetpath` ok (2.3s), `cmd/ingestor` passes except `TestWriteStatsAtomic_SymlinkAtDestIsReplaced`, which fails identically on bare master with "A required privilege is not held by the client" (Windows symlink privilege on my host, not code). ## Sequencing This should go last in the queue. The sweep touches 66 files, so merging it before the remaining open Go PRs gives each of them a conflict about nothing but formatting. After it lands the gate is active, and any PR with drift fails CI until it runs `gofmt -w`. Excluded from the sweep: the misnamed `Dockerfile.go`, which is a Dockerfile that gofmt cannot parse (the workflow excludes it too), and `docs/DEPLOYMENT.md`, which a case-insensitive filesystem surfaces as a spurious modification against `docs/deployment.md` and is unrelated. --------- Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
180 lines
5.0 KiB
Go
180 lines
5.0 KiB
Go
package main
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// GetRepeaterUsefulnessScore returns a 0..1 score representing what
|
||
// fraction of non-advert traffic in the store passes through this
|
||
// repeater as a relay hop. Issue #672 (Traffic axis only — bridge,
|
||
// coverage, and redundancy axes are deferred to follow-up work).
|
||
//
|
||
// Numerator: count of non-advert StoreTx entries indexed under
|
||
//
|
||
// pubkey in byPathHop.
|
||
//
|
||
// Denominator: total non-advert StoreTx entries in the store
|
||
//
|
||
// (sum of byPayloadType for all keys != payloadTypeAdvert).
|
||
//
|
||
// Returns 0 when there is no non-advert traffic, the pubkey is empty,
|
||
// or the repeater never appears as a relay hop. Scores are clamped to
|
||
// [0,1] for defensive bounds.
|
||
//
|
||
// Cost: O(N) over byPayloadType keys (typically <20) plus the per-hop
|
||
// slice for pubkey. Cheap relative to the per-request enrichment loop
|
||
// in handleNodes; if it ever shows up in profiles, denominator can be
|
||
// memoized off store invalidation.
|
||
func (s *PacketStore) GetRepeaterUsefulnessScore(pubkey string) float64 {
|
||
if pubkey == "" {
|
||
return 0
|
||
}
|
||
key := strings.ToLower(pubkey)
|
||
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
// Denominator: total non-advert packets.
|
||
totalNonAdvert := 0
|
||
for pt, list := range s.byPayloadType {
|
||
if pt == payloadTypeAdvert {
|
||
continue
|
||
}
|
||
totalNonAdvert += len(list)
|
||
}
|
||
if totalNonAdvert == 0 {
|
||
return 0
|
||
}
|
||
|
||
// Numerator: this repeater's non-advert hop appearances.
|
||
relayed := 0
|
||
for _, tx := range s.byPathHop[key] {
|
||
if tx == nil {
|
||
continue
|
||
}
|
||
if tx.PayloadType != nil && *tx.PayloadType == payloadTypeAdvert {
|
||
continue
|
||
}
|
||
relayed++
|
||
}
|
||
|
||
score := float64(relayed) / float64(totalNonAdvert)
|
||
if score < 0 {
|
||
return 0
|
||
}
|
||
if score > 1 {
|
||
return 1
|
||
}
|
||
return score
|
||
}
|
||
|
||
// RepeaterNodeStats bundles relay-activity and usefulness data for a single node.
|
||
type RepeaterNodeStats struct {
|
||
Info RepeaterRelayInfo
|
||
Score float64
|
||
}
|
||
|
||
// GetRepeaterNodeStatsBatch computes relay info and usefulness scores for all given
|
||
// pubkeys in a single read-lock pass, sharing the non-advert denominator across all
|
||
// nodes. All StoreTx fields are read under the lock and copied into relayEntry
|
||
// snapshots before the lock is released; no StoreTx pointers escape the lock.
|
||
// Replaces the per-node loop in handleNodes that called GetRepeaterRelayInfo +
|
||
// GetRepeaterUsefulnessScore N times (O(N × byPayloadType) → O(byPayloadType + N)).
|
||
func (s *PacketStore) GetRepeaterNodeStatsBatch(pubkeys []string, windowHours float64) map[string]RepeaterNodeStats {
|
||
result := make(map[string]RepeaterNodeStats, len(pubkeys))
|
||
if len(pubkeys) == 0 {
|
||
return result
|
||
}
|
||
|
||
type nodeSnap struct {
|
||
entries []relayEntry
|
||
relayed int // non-advert count in full-key list only (for usefulness score)
|
||
}
|
||
|
||
s.mu.RLock()
|
||
|
||
totalNonAdvert := 0
|
||
for pt, list := range s.byPayloadType {
|
||
if pt != payloadTypeAdvert {
|
||
totalNonAdvert += len(list)
|
||
}
|
||
}
|
||
|
||
snaps := make(map[string]nodeSnap, len(pubkeys))
|
||
for _, pk := range pubkeys {
|
||
key := strings.ToLower(pk)
|
||
entries := s.collectRelayEntriesLocked(key)
|
||
relayed := 0
|
||
for _, tx := range s.byPathHop[key] {
|
||
if tx != nil && (tx.PayloadType == nil || *tx.PayloadType != payloadTypeAdvert) {
|
||
relayed++
|
||
}
|
||
}
|
||
snaps[pk] = nodeSnap{entries: entries, relayed: relayed}
|
||
}
|
||
|
||
s.mu.RUnlock()
|
||
|
||
for _, pk := range pubkeys {
|
||
snap := snaps[pk]
|
||
info := computeRelayInfoFromEntries(snap.entries, windowHours)
|
||
|
||
var score float64
|
||
if totalNonAdvert > 0 && snap.relayed > 0 {
|
||
score = float64(snap.relayed) / float64(totalNonAdvert)
|
||
if score > 1 {
|
||
score = 1
|
||
}
|
||
}
|
||
|
||
result[pk] = RepeaterNodeStats{Info: info, Score: score}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// GetRepeaterNodeStatsBatchCached wraps GetRepeaterNodeStatsBatch with a 5min
|
||
// TTL cache keyed on (pubkeys, windowHours). handleNodes calls this for every
|
||
// map/live/node request; without caching the full batch over ~1900 repeaters
|
||
// takes 20-30s on large datasets.
|
||
// 300s TTL: cold compute (~25s) runs at most once per 5min (~8% duty cycle)
|
||
// vs the previous 30s TTL (~82% duty cycle).
|
||
func (s *PacketStore) GetRepeaterNodeStatsBatchCached(pubkeys []string, windowHours float64) map[string]RepeaterNodeStats {
|
||
sig := pubkeySig(pubkeys)
|
||
|
||
s.relayStatsCacheMu.Lock()
|
||
if s.relayStatsCache != nil &&
|
||
s.relayStatsCacheSig == sig &&
|
||
s.relayStatsCacheWindow == windowHours &&
|
||
time.Since(s.relayStatsCacheAt) < 300*time.Second {
|
||
cached := s.relayStatsCache
|
||
s.relayStatsCacheMu.Unlock()
|
||
return cached
|
||
}
|
||
s.relayStatsCacheMu.Unlock()
|
||
|
||
result := s.GetRepeaterNodeStatsBatch(pubkeys, windowHours)
|
||
|
||
s.relayStatsCacheMu.Lock()
|
||
s.relayStatsCache = result
|
||
s.relayStatsCacheAt = time.Now()
|
||
s.relayStatsCacheWindow = windowHours
|
||
s.relayStatsCacheSig = sig
|
||
s.relayStatsCacheMu.Unlock()
|
||
|
||
return result
|
||
}
|
||
|
||
// pubkeySig returns a stable, order-independent string key for a pubkey set.
|
||
func pubkeySig(pubkeys []string) string {
|
||
if len(pubkeys) == 0 {
|
||
return ""
|
||
}
|
||
sorted := make([]string, len(pubkeys))
|
||
copy(sorted, pubkeys)
|
||
sort.Strings(sorted)
|
||
return strings.Join(sorted, ",")
|
||
}
|