Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

179 lines
5.7 KiB
Go

package mcp
import (
"container/list"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
)
// pprWalkCache is a bounded LRU of seeded random-walk (Personalized
// PageRank) results, keyed by the content-addressed walk key derived
// from sorted seeds + restart + per-package Merkle roots (see
// analysis.AdjacencySnapshot.WalkCacheKey).
//
// It is the incremental-RWR cache: because the key embeds the per-
// package content roots, invalidation is implicit. When a package the
// walk depends on changes, the next analysis pass produces a different
// root → a different key → a miss → recompute; unchanged-package walks
// reproduce the same key and hit, even across a snapshot rebuild or a
// daemon restart of the in-memory graph. Stale entries for changed
// packages become unreachable and age out via LRU eviction.
//
// Cached score maps are treated as read-only by every consumer (the
// rerank pipeline rescales into a fresh map; context_closure only reads
// values), so sharing one map across calls is safe without copying.
type pprWalkCache struct {
mu sync.Mutex
ll *list.List // front = most-recently-used
m map[string]*list.Element
cap int // max distinct walks retained (secondary ceiling)
maxBytes int64 // memory budget across all retained score maps
curBytes int64 // running sum of entry.bytes
topK int // nodes kept per cached walk (0 = unbounded)
enabled bool
hits atomic.Int64
misses atomic.Int64
}
type pprCacheEntry struct {
key string
scores map[string]float64
bytes int64 // estimated retained size, for the cache's byte budget
}
const (
// pprCacheDefaultMaxBytes bounds the total memory the walk cache may
// retain across all cached score maps. An entry-count ceiling alone is
// unsafe: each entry is a per-walk score map whose size scales with the
// graph, so on a large graph 512 entries can reach several GB.
pprCacheDefaultMaxBytes = 256 << 20 // 256 MiB
// pprCacheDefaultTopK caps how many of the highest-scoring nodes each
// cached walk retains. A seeded walk concentrates its mass near the
// seeds, so a few thousand nodes hold all the ranking signal every
// consumer reads; the tail is dropped before caching. 0 disables the
// cap (full dense map).
pprCacheDefaultTopK = 4096
// pprCacheBytesPerScore estimates the retained bytes of one
// map[string]float64 entry: a 16-byte string header + 8-byte value +
// Go map bucket / load-factor overhead. The key's backing bytes are
// shared with the graph and not counted. Deliberately conservative.
pprCacheBytesPerScore = 48
)
// pprEntryBytes estimates the retained size of a cached score map for the
// cache's byte accounting.
func pprEntryBytes(scores map[string]float64) int64 {
return int64(len(scores)) * pprCacheBytesPerScore
}
// newPPRWalkCache constructs the cache from the environment:
// - GORTEX_PPR_CACHE_DISABLE=1 turn the cache off (always recompute)
// - GORTEX_PPR_CACHE_SIZE=<n> max distinct walks retained (default 512)
// - GORTEX_PPR_CACHE_MAX_MB=<n> total memory budget in MiB (default 256)
// - GORTEX_PPR_CACHE_TOPK=<n> nodes kept per walk, 0=unbounded (default 4096)
func newPPRWalkCache() *pprWalkCache {
c := &pprWalkCache{
ll: list.New(),
m: make(map[string]*list.Element),
cap: 512,
maxBytes: pprCacheDefaultMaxBytes,
topK: pprCacheDefaultTopK,
enabled: true,
}
if isTruthyEnv(os.Getenv("GORTEX_PPR_CACHE_DISABLE")) {
c.enabled = false
}
if v := strings.TrimSpace(os.Getenv("GORTEX_PPR_CACHE_SIZE")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
c.cap = n
}
}
if v := strings.TrimSpace(os.Getenv("GORTEX_PPR_CACHE_MAX_MB")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
c.maxBytes = int64(n) << 20
}
}
if v := strings.TrimSpace(os.Getenv("GORTEX_PPR_CACHE_TOPK")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
c.topK = n
}
}
return c
}
// get returns the cached scores for key, promoting it to most-recently-
// used. The second return is false on a miss.
func (c *pprWalkCache) get(key string) (map[string]float64, bool) {
if c == nil || !c.enabled || key == "" {
return nil, false
}
c.mu.Lock()
el, ok := c.m[key]
if ok {
c.ll.MoveToFront(el)
}
c.mu.Unlock()
if !ok {
c.misses.Add(1)
return nil, false
}
c.hits.Add(1)
return el.Value.(*pprCacheEntry).scores, true
}
// put stores scores under key, evicting least-recently-used entries until
// the cache is within both its memory budget and its entry-count ceiling.
func (c *pprWalkCache) put(key string, scores map[string]float64) {
if c == nil || !c.enabled || key == "" || len(scores) == 0 {
return
}
sz := pprEntryBytes(scores)
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.m[key]; ok {
e := el.Value.(*pprCacheEntry)
c.curBytes += sz - e.bytes
e.scores = scores
e.bytes = sz
c.ll.MoveToFront(el)
c.evictLocked()
return
}
el := c.ll.PushFront(&pprCacheEntry{key: key, scores: scores, bytes: sz})
c.m[key] = el
c.curBytes += sz
c.evictLocked()
}
// evictLocked drops least-recently-used entries until the cache satisfies
// both its byte budget and its entry-count ceiling. The caller holds c.mu.
func (c *pprWalkCache) evictLocked() {
for c.ll.Len() > 0 && (c.curBytes > c.maxBytes || c.ll.Len() > c.cap) {
back := c.ll.Back()
if back == nil {
break
}
e := back.Value.(*pprCacheEntry)
c.ll.Remove(back)
delete(c.m, e.key)
c.curBytes -= e.bytes
}
}
// stats returns a snapshot of cache performance for diagnostics.
func (c *pprWalkCache) stats() (hits, misses int64, size, capacity int, enabled bool) {
if c == nil {
return 0, 0, 0, 0, false
}
c.mu.Lock()
size = c.ll.Len()
c.mu.Unlock()
return c.hits.Load(), c.misses.Load(), size, c.cap, c.enabled
}