diff --git a/TreeDB/caching/db.go b/TreeDB/caching/db.go index c6e03932a..73a25091d 100644 --- a/TreeDB/caching/db.go +++ b/TreeDB/caching/db.go @@ -6553,6 +6553,7 @@ func Open(dir string, backend BackendDB, opts Options) (*DB, error) { go db.flushLoop() db.startVlogGenerationLoop() db.startVlogShapeLoop() + registerTreeDBExpvarStatsDB(db) return db, nil } @@ -12602,6 +12603,7 @@ func (db *DB) Close() error { var errs []error hadMemtables := false db.closing.Store(true) + unregisterTreeDBExpvarStatsDB(db) db.stopDomainIngressWorkers() db.waitForRetainedValueLogPrune() diff --git a/TreeDB/caching/expvar_stats.go b/TreeDB/caching/expvar_stats.go new file mode 100644 index 000000000..5724b3e91 --- /dev/null +++ b/TreeDB/caching/expvar_stats.go @@ -0,0 +1,94 @@ +package caching + +import ( + "expvar" + "os" + "strconv" + "strings" + "sync" + "sync/atomic" +) + +const treedbExpvarEnabledEnvKey = "TREEDB_ENABLE_EXPVAR_STATS" + +var ( + treedbExpvarEnabled = parseBoolEnvDefault(treedbExpvarEnabledEnvKey, true) + treedbExpvarPublishOnce sync.Once + treedbExpvarCurrentDB atomic.Pointer[DB] +) + +func parseBoolEnvDefault(key string, def bool) bool { + raw := strings.TrimSpace(os.Getenv(key)) + if raw == "" { + return def + } + switch strings.ToLower(raw) { + case "1", "true", "on", "yes": + return true + case "0", "false", "off", "no": + return false + default: + return def + } +} + +func publishTreeDBExpvarStats() { + treedbExpvarPublishOnce.Do(func() { + expvar.Publish("treedb", expvar.Func(func() any { + return currentTreeDBExpvarStats() + })) + }) +} + +func registerTreeDBExpvarStatsDB(db *DB) { + if db == nil || !treedbExpvarEnabled { + return + } + publishTreeDBExpvarStats() + treedbExpvarCurrentDB.Store(db) +} + +func unregisterTreeDBExpvarStatsDB(db *DB) { + if db == nil || !treedbExpvarEnabled { + return + } + treedbExpvarCurrentDB.CompareAndSwap(db, nil) +} + +func currentTreeDBExpvarStats() map[string]any { + db := treedbExpvarCurrentDB.Load() + if db == nil { + return map[string]any{} + } + return selectTreeDBExpvarStats(db.Stats()) +} + +func selectTreeDBExpvarStats(stats map[string]string) map[string]any { + if len(stats) == 0 { + return map[string]any{} + } + out := make(map[string]any) + for k, v := range stats { + if strings.HasPrefix(k, "treedb.cache.vlog_mmap.") || + strings.HasPrefix(k, "treedb.process.memory.") { + out[k] = coerceStatsValue(v) + } + } + return out +} + +func coerceStatsValue(v string) any { + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + if u, err := strconv.ParseUint(v, 10, 64); err == nil { + return u + } + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + if b, err := strconv.ParseBool(v); err == nil { + return b + } + return v +} diff --git a/TreeDB/caching/expvar_stats_test.go b/TreeDB/caching/expvar_stats_test.go new file mode 100644 index 000000000..a214d76c6 --- /dev/null +++ b/TreeDB/caching/expvar_stats_test.go @@ -0,0 +1,47 @@ +package caching + +import "testing" + +func TestSelectTreeDBExpvarStatsFiltersAndCoerces(t *testing.T) { + stats := map[string]string{ + "treedb.cache.vlog_mmap.active_bytes": "12345", + "treedb.cache.vlog_mmap.read.hit_ratio": "0.625000", + "treedb.cache.vlog_mmap.enabled": "true", + "treedb.process.memory.heap_inuse_bytes": "4096", + "treedb.process.memory.pool_pressure_level": "critical", + "treedb.cache.backpressure_mode": "adaptive", + "treedb.cache.entry_slice.trim_runs_total": "77", + "treedb.process.memory.pool_pressure_high_pct": "85.5", + } + + got := selectTreeDBExpvarStats(stats) + if len(got) != 6 { + t.Fatalf("selectTreeDBExpvarStats len=%d want 6", len(got)) + } + + if v, ok := got["treedb.cache.vlog_mmap.active_bytes"].(int64); !ok || v != 12345 { + t.Fatalf("active_bytes=%T(%v) want int64(12345)", got["treedb.cache.vlog_mmap.active_bytes"], got["treedb.cache.vlog_mmap.active_bytes"]) + } + if v, ok := got["treedb.cache.vlog_mmap.read.hit_ratio"].(float64); !ok || v != 0.625 { + t.Fatalf("hit_ratio=%T(%v) want float64(0.625)", got["treedb.cache.vlog_mmap.read.hit_ratio"], got["treedb.cache.vlog_mmap.read.hit_ratio"]) + } + if v, ok := got["treedb.cache.vlog_mmap.enabled"].(bool); !ok || !v { + t.Fatalf("enabled=%T(%v) want bool(true)", got["treedb.cache.vlog_mmap.enabled"], got["treedb.cache.vlog_mmap.enabled"]) + } + if v, ok := got["treedb.process.memory.heap_inuse_bytes"].(int64); !ok || v != 4096 { + t.Fatalf("heap_inuse_bytes=%T(%v) want int64(4096)", got["treedb.process.memory.heap_inuse_bytes"], got["treedb.process.memory.heap_inuse_bytes"]) + } + if v, ok := got["treedb.process.memory.pool_pressure_level"].(string); !ok || v != "critical" { + t.Fatalf("pool_pressure_level=%T(%v) want string(critical)", got["treedb.process.memory.pool_pressure_level"], got["treedb.process.memory.pool_pressure_level"]) + } + if _, ok := got["treedb.cache.backpressure_mode"]; ok { + t.Fatalf("unexpected backpressure_mode key in expvar selection") + } +} + +func TestSelectTreeDBExpvarStatsEmpty(t *testing.T) { + got := selectTreeDBExpvarStats(nil) + if len(got) != 0 { + t.Fatalf("selectTreeDBExpvarStats(nil) len=%d want 0", len(got)) + } +}