Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cmd/server/neighbor_persist.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,15 @@ func resolvePathForObs(pathJSON, observerID string, tx *StoreTx, pm *prefixMap,
contextPKs = append(contextPKs, strings.ToLower(fromNode))
}
resolved := make([]*string, len(hops))
// Reuse a single ctx buffer across hops instead of allocating per hop.
// At most one element (the previous hop's resolved PK) is ever appended;
// the spare capacity is headroom so that append never reallocates.
ctx := make([]string, len(contextPKs), len(contextPKs)+2)
copy(ctx, contextPKs)
ctxLen := len(ctx)
for i, hop := range hops {
ctx := make([]string, len(contextPKs), len(contextPKs)+2)
copy(ctx, contextPKs)
// Reset to base context (trim any previously appended resolved PK)
ctx = ctx[:ctxLen]
if i > 0 && resolved[i-1] != nil {
ctx = append(ctx, *resolved[i-1])
}
Expand Down
29 changes: 26 additions & 3 deletions cmd/server/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,17 @@ func (tx *StoreTx) ParsedDecoded() map[string]interface{} {
// result (hashSizeInfoCache). Acquired independently or
// under mu (in EvictStale).
//
// 7. estMemMu (sync.Mutex) — guards the ReadMemStats result cache
// (estMemVal/estMemAt). Strict leaf: no other lock is
// acquired while holding it.
//
// Nesting that occurs today:
// - IngestNew: mu → cacheMu → channelsCacheMu (1 → 2 → 3, OK)
// - IngestObservations: mu → cacheMu (1 → 2, OK)
// - RunEviction/EvictStale: mu → cacheMu → channelsCacheMu (1 → 2 → 3, OK)
// - RunEviction/EvictStale: mu → hashSizeInfoMu (1 → 6, OK)
// - invalidateCachesFor: cacheMu → channelsCacheMu (2 → 3, OK)
// - Load (startup log lines): mu → estMemMu (1 → 7, OK)
//
// All other locks are acquired independently (no nesting).
// When adding new lock acquisitions, respect this ordering.
Expand Down Expand Up @@ -475,6 +480,13 @@ type PacketStore struct {
trackedBytes int64 // running total of estimated packet store memory
memoryEstimator func() float64 // injectable for tests; nil = use runtime.ReadMemStats (stats only)

// Per-store ReadMemStats cache (5s TTL). Fields (not package-level vars) so
// that test helpers constructing &PacketStore{...} directly get independent
// cache state, avoiding order-dependent test failures.
estMemMu sync.Mutex
estMemVal float64
estMemAt time.Time

// Short-lived cache for the observations aggregate in GetStoreStats (30s TTL).
// Avoids a per-/api/stats full-table scan; values accurate to ~30s which is
// sufficient for dashboard display.
Expand Down Expand Up @@ -4661,13 +4673,24 @@ func estimateStoreObsBytes(obs *StoreObs) int64 {
// estimatedMemoryMB returns current Go heap allocation in MB.
// Kept for stats/debug endpoints only — NOT used in eviction decisions.
// In tests, memoryEstimator can be set to inject a deterministic value.
// Caches the result for 5 seconds because runtime.ReadMemStats() stops the
// world and this is called from stats/debug endpoints that may be polled.
// The cache is per-store (not package-level) so that test helpers constructing
// &PacketStore{...} directly get independent cache state, avoiding
// order-dependent test failures from a shared global cache.
func (s *PacketStore) estimatedMemoryMB() float64 {
if s.memoryEstimator != nil {
return s.memoryEstimator()
}
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
return float64(ms.HeapAlloc) / 1048576.0
s.estMemMu.Lock()
defer s.estMemMu.Unlock()
if time.Since(s.estMemAt) > 5*time.Second {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
s.estMemVal = float64(ms.HeapAlloc) / 1048576.0
s.estMemAt = time.Now()
}
return s.estMemVal
}

// trackedMemoryMB returns the self-accounted packet store memory in MB.
Expand Down
Loading