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
+13 -2
View File
@@ -3,6 +3,7 @@ package cache
import (
"io"
"s1d3sw1ped/steamcache2/steamcache/metrics"
"s1d3sw1ped/steamcache2/vfs"
"s1d3sw1ped/steamcache2/vfs/vfserror"
"sync/atomic"
@@ -10,8 +11,9 @@ import (
// TieredCache implements a lock-free two-tier cache for better concurrency
type TieredCache struct {
fast *atomic.Value // Memory cache (fast) - atomic.Value for lock-free access
slow *atomic.Value // Disk cache (slow) - atomic.Value for lock-free access
fast *atomic.Value // Memory cache (fast) - atomic.Value for lock-free access
slow *atomic.Value // Disk cache (slow) - atomic.Value for lock-free access
metrics *metrics.Metrics
}
// New creates a new tiered cache
@@ -22,6 +24,11 @@ func New() *TieredCache {
}
}
// SetMetrics allows wiring the top-level metrics collector (called from SteamCache).
func (tc *TieredCache) SetMetrics(m *metrics.Metrics) {
tc.metrics = m
}
// SetFast sets the fast (memory) tier atomically
func (tc *TieredCache) SetFast(vfs vfs.VFS) {
tc.fast.Store(vfs)
@@ -221,6 +228,10 @@ func (tc *TieredCache) promoteToFast(key string, reader io.ReadCloser) {
// Failure (e.g. mem pressure, concurrent evict) is non-fatal and does not affect correctness of slow tier.
_, _ = writer.Write(content)
_ = writer.Close()
if tc.metrics != nil {
tc.metrics.IncrementPromotions()
}
}
}
}
+51 -2
View File
@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"s1d3sw1ped/steamcache2/steamcache/logger"
"s1d3sw1ped/steamcache2/steamcache/metrics"
"s1d3sw1ped/steamcache2/vfs"
"s1d3sw1ped/steamcache2/vfs/locks"
"s1d3sw1ped/steamcache2/vfs/lru"
@@ -43,6 +44,7 @@ type DiskFS struct {
// initCloseOnce ensures initDone closed exactly once even on panic in bg populator (panic safety for Issue 1).
initCloseOnce sync.Once
startupEvict func(vfs.VFS, uint) uint // passed to New (via gc.GetGCAlgorithm); invoked as last step of bg init if over cap (no post-ctor race)
metrics *metrics.Metrics
}
// shardPath converts a Steam cache key to a sharded directory path to reduce inode pressure
@@ -134,6 +136,12 @@ func New(root string, capacity int64, evict func(vfs.VFS, uint) uint) (*DiskFS,
return d, nil
}
// SetMetrics allows the owner (SteamCache) to inject the metrics collector
// so that per-tier hit and eviction counters can be recorded.
func (d *DiskFS) SetMetrics(met *metrics.Metrics) {
d.metrics = met
}
// calculateSizeAndPopulateIndex runs in background from New to avoid blocking startup or O(N) RAM for large caches (millions of Steam files).
// It streams batch inserts (bounded by maxEvictBatch) to keep lock times short and eliminate giant temporary slice.
// Startup over-capacity eviction (if needed) runs as the very last step (using the evict func passed to New, selected via gc.GetGCAlgorithm).
@@ -489,8 +497,14 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
return nil, err
}
// Use memory mapping for large files (>1MB) to improve performance
const mmapThreshold = 1024 * 1024 // 1MB
// Use memory mapping for large files to improve performance.
// We use 8 MiB as the threshold because:
// - Most Steam chunks are ~1 MiB (see current disk cache analysis).
// - mmap has non-trivial fixed overhead (page tables, TLB, faults).
// - For files < ~4-8 MiB the overhead often outweighs the zero-copy benefit
// on mostly sequential access patterns.
// - Larger files benefit more from kernel readahead + zero-copy.
const mmapThreshold = 8 * 1024 * 1024 // 8 MiB
if fi.Size > mmapThreshold {
// Close the regular file handle
_ = file.Close() // best-effort; mmap path takes over or falls back
@@ -505,9 +519,21 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
if err != nil {
_ = mmapFile.Close() // best-effort close before fallback open
// Fallback to regular file reading (intentional 3rd open of same path after mmap failure; pre-existing pattern, no leak)
if d.metrics != nil {
d.metrics.IncrementDiskCacheHits()
}
return os.Open(path)
}
// Hint to the kernel (on supported platforms) that we will access
// this mapping sequentially. This enables better readahead.
if err := madviseSequential(mapped); err != nil {
logger.Logger.Debug().
Err(err).
Str("key", key).
Msg("madvise(MADV_SEQUENTIAL) failed on mmap'd chunk")
}
return &mmapReadCloser{
data: mapped,
file: mmapFile,
@@ -515,6 +541,9 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
}, nil
}
if d.metrics != nil {
d.metrics.IncrementDiskCacheHits()
}
return file, nil
}
@@ -677,6 +706,10 @@ func (d *DiskFS) EvictLRU(bytesNeeded uint) uint {
}
}
d.mu.Unlock()
if d.metrics != nil && evicted > 0 {
d.metrics.IncrementEvictions()
}
return evicted
}
@@ -726,6 +759,10 @@ func (d *DiskFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
}
}
d.mu.Unlock()
if d.metrics != nil && evicted > 0 {
d.metrics.IncrementEvictions()
}
return evicted
}
@@ -773,6 +810,10 @@ func (d *DiskFS) EvictFIFO(bytesNeeded uint) uint {
}
}
d.mu.Unlock()
if d.metrics != nil && evicted > 0 {
d.metrics.IncrementEvictions()
}
return evicted
}
@@ -825,6 +866,10 @@ func (d *DiskFS) EvictLFU(bytesNeeded uint) uint {
}
}
d.mu.Unlock()
if d.metrics != nil && evicted > 0 {
d.metrics.IncrementEvictions()
}
return evicted
}
@@ -878,5 +923,9 @@ func (d *DiskFS) EvictHybrid(bytesNeeded uint) uint {
}
}
d.mu.Unlock()
if d.metrics != nil && evicted > 0 {
d.metrics.IncrementEvictions()
}
return evicted
}
+13
View File
@@ -0,0 +1,13 @@
//go:build !windows
package disk
import (
"golang.org/x/sys/unix"
)
// madviseSequential gives the OS a hint that the memory region will be
// accessed sequentially. This is a no-op or best-effort on some platforms.
func madviseSequential(b []byte) error {
return unix.Madvise(b, unix.MADV_SEQUENTIAL)
}
+11
View File
@@ -0,0 +1,11 @@
//go:build windows
package disk
// madviseSequential is a no-op on Windows.
// Windows file mappings don't have a direct equivalent to MADV_SEQUENTIAL
// in the same way. Sequential access hints are better done via
// FILE_FLAG_SEQUENTIAL_SCAN at file open time (future improvement possible).
func madviseSequential(b []byte) error {
return nil
}
+32
View File
@@ -5,6 +5,7 @@ import (
"bytes"
"fmt"
"io"
"s1d3sw1ped/steamcache2/steamcache/metrics"
"s1d3sw1ped/steamcache2/vfs"
"s1d3sw1ped/steamcache2/vfs/locks"
"s1d3sw1ped/steamcache2/vfs/lru"
@@ -33,6 +34,7 @@ type MemoryFS struct {
keyLocks []sync.Map // Sharded lock pools for better concurrency
LRU *lru.LRUList[*types.FileInfo]
timeUpdater *types.BatchedTimeUpdate // Batched time updates for better performance
metrics *metrics.Metrics
}
// New creates a new MemoryFS
@@ -55,6 +57,12 @@ func New(capacity int64) (*MemoryFS, error) {
}, nil
}
// SetMetrics allows the owner (SteamCache) to inject the metrics collector
// so that per-tier hit and eviction counters can be recorded.
func (m *MemoryFS) SetMetrics(met *metrics.Metrics) {
m.metrics = met
}
// Name returns the name of this VFS
func (m *MemoryFS) Name() string {
return "MemoryFS"
@@ -209,6 +217,10 @@ func (m *MemoryFS) Open(key string) (io.ReadCloser, error) {
// Use zero-copy approach - return reader that reads directly from buffer
m.mu.Unlock()
if m.metrics != nil {
m.metrics.IncrementMemoryCacheHits()
}
return &memoryReadCloser{
buffer: buffer,
offset: 0,
@@ -344,6 +356,10 @@ func (m *MemoryFS) EvictLRU(bytesNeeded uint) uint {
}
}
m.mu.Unlock()
if m.metrics != nil && evicted > 0 {
m.metrics.IncrementEvictions()
}
return evicted
}
@@ -395,6 +411,10 @@ func (m *MemoryFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
}
}
m.mu.Unlock()
if m.metrics != nil && evicted > 0 {
m.metrics.IncrementEvictions()
}
return evicted
}
@@ -442,6 +462,10 @@ func (m *MemoryFS) EvictFIFO(bytesNeeded uint) uint {
}
}
m.mu.Unlock()
if m.metrics != nil && evicted > 0 {
m.metrics.IncrementEvictions()
}
return evicted
}
@@ -494,6 +518,10 @@ func (m *MemoryFS) EvictLFU(bytesNeeded uint) uint {
}
}
m.mu.Unlock()
if m.metrics != nil && evicted > 0 {
m.metrics.IncrementEvictions()
}
return evicted
}
@@ -548,5 +576,9 @@ func (m *MemoryFS) EvictHybrid(bytesNeeded uint) uint {
}
}
m.mu.Unlock()
if m.metrics != nil && evicted > 0 {
m.metrics.IncrementEvictions()
}
return evicted
}