Files
claude-mnemonic/internal/vector/hybrid/metrics.go
lukaszraczylo 1ae8035470 feat(graph): add observation graph with hybrid vector storage
- [x] Add golangci.yml configuration with fieldalignment linter
- [x] Implement observation graph structure with edge detection
- [x] Add LEANN-inspired hybrid vector storage with hub threshold
- [x] Implement graph-aware search with selective recomputation
- [x] Add auto-tuner for dynamic hub threshold adjustment
- [x] Add graph and vector metrics tracking and reporting
- [x] Extend configuration for graph parameters
- [x] Add graph rebuild background service with periodic updates
- [x] Add HTTP endpoints for graph stats and vector metrics
- [x] Update UI with advanced metrics sidebar panel
- [x] Implement AST-aware code chunking for Go, Python, TypeScript
2026-01-07 18:51:40 +00:00

273 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package hybrid
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// Metrics tracks performance and usage statistics for hybrid vector storage
type Metrics struct {
startTime time.Time
recentLatencies []time.Duration
latenciesMu sync.Mutex
totalQueries atomic.Int64
hubOnlyQueries atomic.Int64
hybridQueries atomic.Int64
onDemandQueries atomic.Int64
graphQueries atomic.Int64
totalLatency atomic.Int64 // Sum in microseconds
hubLatency atomic.Int64
recomputeLatency atomic.Int64
totalDocuments atomic.Int64
hubDocuments atomic.Int64
storedEmbeddings atomic.Int64
recomputedCount atomic.Int64
cacheHits atomic.Int64
cacheMisses atomic.Int64
graphTraversals atomic.Int64
avgTraversalDepth atomic.Int64
}
// NewMetrics creates a new metrics tracker
func NewMetrics() *Metrics {
return &Metrics{
recentLatencies: make([]time.Duration, 0, 1000),
startTime: time.Now(),
}
}
// RecordQuery records a query execution
func (m *Metrics) RecordQuery(queryType string, latency time.Duration, recomputed int) {
m.totalQueries.Add(1)
m.totalLatency.Add(latency.Microseconds())
switch queryType {
case "hub_only":
m.hubOnlyQueries.Add(1)
case "hybrid":
m.hybridQueries.Add(1)
case "on_demand":
m.onDemandQueries.Add(1)
case "graph":
m.graphQueries.Add(1)
}
if recomputed > 0 {
m.recomputedCount.Add(int64(recomputed))
}
// Track recent latencies
m.latenciesMu.Lock()
m.recentLatencies = append(m.recentLatencies, latency)
if len(m.recentLatencies) > 1000 {
m.recentLatencies = m.recentLatencies[len(m.recentLatencies)-1000:]
}
m.latenciesMu.Unlock()
}
// RecordHubLatency records time spent in hub search
func (m *Metrics) RecordHubLatency(latency time.Duration) {
m.hubLatency.Add(latency.Microseconds())
}
// RecordRecomputeLatency records time spent recomputing embeddings
func (m *Metrics) RecordRecomputeLatency(latency time.Duration) {
m.recomputeLatency.Add(latency.Microseconds())
}
// RecordCacheHit records a content cache hit
func (m *Metrics) RecordCacheHit() {
m.cacheHits.Add(1)
}
// RecordCacheMiss records a content cache miss
func (m *Metrics) RecordCacheMiss() {
m.cacheMisses.Add(1)
}
// RecordGraphTraversal records a graph traversal operation
func (m *Metrics) RecordGraphTraversal(depth int) {
m.graphTraversals.Add(1)
m.avgTraversalDepth.Add(int64(depth))
}
// UpdateStorageStats updates current storage statistics
func (m *Metrics) UpdateStorageStats(total, hubs, stored int) {
m.totalDocuments.Store(int64(total))
m.hubDocuments.Store(int64(hubs))
m.storedEmbeddings.Store(int64(stored))
}
// GetSnapshot returns current metrics snapshot
func (m *Metrics) GetSnapshot() MetricsSnapshot {
m.latenciesMu.Lock()
defer m.latenciesMu.Unlock()
totalQueries := m.totalQueries.Load()
snapshot := MetricsSnapshot{
// Query counts
TotalQueries: totalQueries,
HubOnlyQueries: m.hubOnlyQueries.Load(),
HybridQueries: m.hybridQueries.Load(),
OnDemandQueries: m.onDemandQueries.Load(),
GraphQueries: m.graphQueries.Load(),
// Storage
TotalDocuments: int(m.totalDocuments.Load()),
HubDocuments: int(m.hubDocuments.Load()),
StoredEmbeddings: int(m.storedEmbeddings.Load()),
RecomputedTotal: m.recomputedCount.Load(),
// Cache
CacheHits: m.cacheHits.Load(),
CacheMisses: m.cacheMisses.Load(),
// Graph
GraphTraversals: m.graphTraversals.Load(),
// Runtime
Uptime: time.Since(m.startTime),
}
// Calculate latencies
if totalQueries > 0 {
snapshot.AvgLatency = time.Duration(m.totalLatency.Load()/totalQueries) * time.Microsecond
snapshot.AvgHubLatency = time.Duration(m.hubLatency.Load()/totalQueries) * time.Microsecond
}
if m.recomputedCount.Load() > 0 {
snapshot.AvgRecomputeLatency = time.Duration(m.recomputeLatency.Load()/m.recomputedCount.Load()) * time.Microsecond
}
// Calculate percentiles
if len(m.recentLatencies) > 0 {
sorted := make([]time.Duration, len(m.recentLatencies))
copy(sorted, m.recentLatencies)
sortDurations(sorted)
snapshot.P50Latency = percentile(sorted, 0.50)
snapshot.P95Latency = percentile(sorted, 0.95)
snapshot.P99Latency = percentile(sorted, 0.99)
}
// Calculate cache hit rate
totalCacheOps := snapshot.CacheHits + snapshot.CacheMisses
if totalCacheOps > 0 {
snapshot.CacheHitRate = float64(snapshot.CacheHits) / float64(totalCacheOps)
}
// Calculate storage savings
if snapshot.TotalDocuments > 0 {
embeddingSize := 384 * 4 // 384 dims × 4 bytes
fullStorage := snapshot.TotalDocuments * embeddingSize
actualStorage := snapshot.StoredEmbeddings * embeddingSize
if fullStorage > 0 {
snapshot.StorageSavingsPercent = (1.0 - float64(actualStorage)/float64(fullStorage)) * 100
}
}
// Calculate avg traversal depth
if snapshot.GraphTraversals > 0 {
snapshot.AvgTraversalDepth = float64(m.avgTraversalDepth.Load()) / float64(snapshot.GraphTraversals)
}
return snapshot
}
// MetricsSnapshot represents a point-in-time metrics snapshot
type MetricsSnapshot struct {
// Query metrics
TotalQueries int64
HubOnlyQueries int64
HybridQueries int64
OnDemandQueries int64
GraphQueries int64
// Latency metrics
AvgLatency time.Duration
P50Latency time.Duration
P95Latency time.Duration
P99Latency time.Duration
AvgHubLatency time.Duration
AvgRecomputeLatency time.Duration
// Storage metrics
TotalDocuments int
HubDocuments int
StoredEmbeddings int
StorageSavingsPercent float64
RecomputedTotal int64
// Cache metrics
CacheHits int64
CacheMisses int64
CacheHitRate float64
// Graph metrics
GraphTraversals int64
AvgTraversalDepth float64
// Runtime
Uptime time.Duration
}
// sortDurations sorts a slice of durations in ascending order
func sortDurations(durations []time.Duration) {
n := len(durations)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if durations[j] > durations[j+1] {
durations[j], durations[j+1] = durations[j+1], durations[j]
}
}
}
}
// percentile calculates the Nth percentile from a sorted slice
func percentile(sorted []time.Duration, p float64) time.Duration {
if len(sorted) == 0 {
return 0
}
idx := int(float64(len(sorted)) * p)
if idx >= len(sorted) {
idx = len(sorted) - 1
}
return sorted[idx]
}
// String returns a human-readable representation of metrics
func (s MetricsSnapshot) String() string {
return fmt.Sprintf(`Hybrid Vector Storage Metrics:
Queries:
Total: %d (Hub: %d, Hybrid: %d, OnDemand: %d, Graph: %d)
Avg Latency: %v (p50: %v, p95: %v, p99: %v)
Hub Latency: %v, Recompute Latency: %v
Storage:
Documents: %d (Hubs: %d, %.1f%%)
Stored Embeddings: %d
Savings: %.1f%%
Total Recomputed: %d
Cache:
Hits: %d, Misses: %d (Hit Rate: %.1f%%)
Graph:
Traversals: %d (Avg Depth: %.2f)
Runtime: %v`,
s.TotalQueries, s.HubOnlyQueries, s.HybridQueries, s.OnDemandQueries, s.GraphQueries,
s.AvgLatency, s.P50Latency, s.P95Latency, s.P99Latency,
s.AvgHubLatency, s.AvgRecomputeLatency,
s.TotalDocuments, s.HubDocuments, float64(s.HubDocuments)/float64(s.TotalDocuments)*100,
s.StoredEmbeddings,
s.StorageSavingsPercent,
s.RecomputedTotal,
s.CacheHits, s.CacheMisses, s.CacheHitRate*100,
s.GraphTraversals, s.AvgTraversalDepth,
s.Uptime,
)
}