Update metrics tracking and enhance cache eviction strategies
Release Tag / release (push) Successful in 34s

- Added metrics for bytes saved from cache to improve performance insights.
- Updated cache eviction strategies in MemoryFS and DiskFS to include metrics tracking for hits and evictions.
- Enhanced README.md with updated garbage collection algorithm descriptions and recommendations for cache usage.
- Introduced new madviseSequential functionality for improved memory access hints on Unix systems.
- Adjusted validation configuration in examples to better reflect realistic usage scenarios.
This commit is contained in:
2026-05-28 10:31:23 -05:00
parent 3fd72705fc
commit b7e3a0da86
11 changed files with 184 additions and 41 deletions
+7 -5
View File
@@ -110,6 +110,7 @@ func (sc *SteamCache) handleCacheHit(w http.ResponseWriter, r *http.Request, cac
sc.metrics.IncrementCacheHits()
sc.metrics.AddResponseTime(time.Since(tstart))
sc.metrics.AddBytesServed(int64(len(cachedData)))
sc.metrics.AddBytesSaved(int64(len(cachedData)))
sc.metrics.IncrementServiceRequests(service.Name)
logger.Logger.Debug().
@@ -197,6 +198,7 @@ func (sc *SteamCache) waitForCoalesced(w http.ResponseWriter, r *http.Request, c
sc.metrics.IncrementCacheCoalesced()
sc.metrics.AddResponseTime(time.Since(tstart))
sc.metrics.AddBytesServed(int64(len(responseData)))
sc.metrics.AddBytesSaved(int64(len(responseData)))
sc.metrics.IncrementServiceRequests(service.Name)
logger.Logger.Info().
@@ -233,9 +235,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
defer sc.requestSemaphore.Release(1)
// Track total requests
sc.metrics.IncrementTotalRequests()
// Apply per-client rate limiting
clientLimiter := sc.getOrCreateClientLimiter(clientIP)
@@ -305,6 +304,11 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Str("client_ip", clientIP).
Msg("Generated cache key")
// Only count real cacheable service traffic toward total_requests / hit_rate.
// Special endpoints (/, /metrics, /lancache-heartbeat) and unsupported services
// are intentionally excluded so that idle monitoring doesn't dilute the hit rate.
sc.metrics.IncrementTotalRequests()
if sc.handleCacheHit(w, r, cachePath, cacheKey, urlPath, service, clientIP, tstart) {
return
}
@@ -588,8 +592,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sc.metrics.IncrementServiceError("cache_write")
_ = sc.vfs.Delete(cachePath) // best-effort removal of partial corrupt cache entry on write failure; non-fatal. Deferred cacheWriter.Close() (from earlier in block) runs after this on error unwind path (harmless per DiskFS design)
} else {
// Track successful cache write
sc.metrics.AddBytesCached(int64(len(cacheData)))
logger.Logger.Debug().
Str("key", cacheKey).
Str("url", urlPath).
+17 -11
View File
@@ -22,7 +22,9 @@ type Metrics struct {
// Performance metrics
TotalResponseTime int64 // in nanoseconds
TotalBytesServed int64
TotalBytesCached int64
TotalBytesSaved int64 // bytes served from cache instead of being re-downloaded from upstream
// Cache metrics
MemoryCacheSize int64
@@ -98,9 +100,10 @@ func (m *Metrics) AddBytesServed(bytes int64) {
atomic.AddInt64(&m.TotalBytesServed, bytes)
}
// AddBytesCached adds bytes cached to the total
func (m *Metrics) AddBytesCached(bytes int64) {
atomic.AddInt64(&m.TotalBytesCached, bytes)
// AddBytesSaved records bytes that were served from the cache instead of being
// fetched again from the upstream (the main value metric for a cache).
func (m *Metrics) AddBytesSaved(bytes int64) {
atomic.AddInt64(&m.TotalBytesSaved, bytes)
}
// SetMemoryCacheSize sets the current memory cache size
@@ -191,8 +194,8 @@ func (m *Metrics) GetStats() *Stats {
RateLimited: atomic.LoadInt64(&m.RateLimited),
HitRate: hitRate,
AvgResponseTime: avgResponseTime,
TotalBytesServed: atomic.LoadInt64(&m.TotalBytesServed),
TotalBytesCached: atomic.LoadInt64(&m.TotalBytesCached),
TotalBytesServed: atomic.LoadInt64(&m.TotalBytesServed),
TotalBytesSaved: atomic.LoadInt64(&m.TotalBytesSaved),
MemoryCacheSize: atomic.LoadInt64(&m.MemoryCacheSize),
DiskCacheSize: atomic.LoadInt64(&m.DiskCacheSize),
MemoryCacheHits: atomic.LoadInt64(&m.MemoryCacheHits),
@@ -218,7 +221,7 @@ func (m *Metrics) Reset() {
atomic.StoreInt64(&m.RateLimited, 0)
atomic.StoreInt64(&m.TotalResponseTime, 0)
atomic.StoreInt64(&m.TotalBytesServed, 0)
atomic.StoreInt64(&m.TotalBytesCached, 0)
atomic.StoreInt64(&m.TotalBytesSaved, 0)
atomic.StoreInt64(&m.MemoryCacheHits, 0)
atomic.StoreInt64(&m.DiskCacheHits, 0)
atomic.StoreInt64(&m.Promotions, 0)
@@ -247,9 +250,11 @@ type Stats struct {
RateLimited int64
HitRate float64
AvgResponseTime time.Duration
TotalBytesServed int64
TotalBytesCached int64
MemoryCacheSize int64
TotalBytesServed int64
TotalBytesSaved int64
MemoryCacheSize int64
DiskCacheSize int64
MemoryCacheHits int64
DiskCacheHits int64
@@ -291,7 +296,8 @@ func WriteText(w http.ResponseWriter, stats *Stats) {
_, _ = fmt.Fprintf(w, "hit_rate %.4f\n", stats.HitRate)
_, _ = fmt.Fprintf(w, "avg_response_time_ms %.2f\n", float64(stats.AvgResponseTime.Nanoseconds())/1e6)
_, _ = fmt.Fprintf(w, "total_bytes_served %d\n", stats.TotalBytesServed)
_, _ = fmt.Fprintf(w, "total_bytes_cached %d\n", stats.TotalBytesCached)
_, _ = fmt.Fprintf(w, "total_bytes_saved %d\n", stats.TotalBytesSaved)
_, _ = fmt.Fprintf(w, "memory_cache_size %d\n", stats.MemoryCacheSize)
_, _ = fmt.Fprintf(w, "disk_cache_size %d\n", stats.DiskCacheSize)
_, _ = fmt.Fprintf(w, "uptime_seconds %.2f\n", stats.Uptime.Seconds())
+15
View File
@@ -179,6 +179,17 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
metrics: metrics.NewMetrics(),
}
// Wire metrics into TieredCache so promotions are counted.
c.SetMetrics(sc.metrics)
// Wire metrics into the concrete storage tiers (MemoryFS / DiskFS) so per-tier hits and evictions are counted.
if m != nil {
m.SetMetrics(sc.metrics)
}
if d != nil {
d.SetMetrics(sc.metrics)
}
// Wire the request processor (constructor injection of interfaces for the owned wrappers + vfs + client + scalars).
// Done after all fields including wrappers are set; before attach goroutines (no impact on lifecycle paths).
sc.processor = newRequestProcessor(sc)
@@ -209,12 +220,16 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
sc.wg.Add(1)
go func() {
defer sc.wg.Done()
t0 := time.Now()
_ = d.Size()
select {
case <-sc.shutdownCh:
return
default:
c.SetSlow(dgc)
logger.Logger.Info().
Dur("attach_delay", time.Since(t0)).
Msg("Disk slow tier attached (mixed mode); prior traffic was memory-only")
}
}()
}