Update metrics tracking and enhance cache eviction strategies
Release Tag / release (push) Successful in 34s
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:
Vendored
+13
-2
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user