package storage import ( "bufio" "context" "fmt" "os" "strconv" "strings" "time" "github.com/atlasos/calypso/internal/common/logger" ) // ARCStats represents ZFS ARC (Adaptive Replacement Cache) statistics type ARCStats struct { HitRatio float64 `json:"hit_ratio"` // Percentage of cache hits CacheUsage float64 `json:"cache_usage"` // Percentage of cache used CacheSize int64 `json:"cache_size"` // Current ARC size in bytes CacheMax int64 `json:"cache_max"` // Maximum ARC size in bytes Hits int64 `json:"hits"` // Total cache hits Misses int64 `json:"misses"` // Total cache misses DemandHits int64 `json:"demand_hits"` // Demand data/metadata hits PrefetchHits int64 `json:"prefetch_hits"` // Prefetch hits MRUHits int64 `json:"mru_hits"` // Most Recently Used hits MFUHits int64 `json:"mfu_hits"` // Most Frequently Used hits CollectedAt string `json:"collected_at"` // Timestamp when stats were collected } // ARCService handles ZFS ARC statistics collection type ARCService struct { logger *logger.Logger } // NewARCService creates a new ARC service func NewARCService(log *logger.Logger) *ARCService { return &ARCService{ logger: log, } } // GetARCStats reads and parses ARC statistics from /proc/spl/kstat/zfs/arcstats func (s *ARCService) GetARCStats(ctx context.Context) (*ARCStats, error) { stats := &ARCStats{} // Read ARC stats file file, err := os.Open("/proc/spl/kstat/zfs/arcstats") if err != nil { return nil, fmt.Errorf("failed to open arcstats file: %w", err) } defer file.Close() // Parse the file scanner := bufio.NewScanner(file) arcData := make(map[string]int64) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // Skip empty lines and header lines if line == "" || strings.HasPrefix(line, "name") || strings.HasPrefix(line, "9") { continue } // Parse lines like: "hits 4 311154" fields := strings.Fields(line) if len(fields) >= 3 { key := fields[0] // The value is in the last field (field index 2) if value, err := strconv.ParseInt(fields[len(fields)-1], 10, 64); err == nil { arcData[key] = value } } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read arcstats file: %w", err) } // Extract key metrics stats.Hits = arcData["hits"] stats.Misses = arcData["misses"] stats.DemandHits = arcData["demand_data_hits"] + arcData["demand_metadata_hits"] stats.PrefetchHits = arcData["prefetch_data_hits"] + arcData["prefetch_metadata_hits"] stats.MRUHits = arcData["mru_hits"] stats.MFUHits = arcData["mfu_hits"] // Current ARC size (c) and max size (c_max) stats.CacheSize = arcData["c"] stats.CacheMax = arcData["c_max"] // Calculate hit ratio totalRequests := stats.Hits + stats.Misses if totalRequests > 0 { stats.HitRatio = float64(stats.Hits) / float64(totalRequests) * 100.0 } else { stats.HitRatio = 0.0 } // Calculate cache usage percentage if stats.CacheMax > 0 { stats.CacheUsage = float64(stats.CacheSize) / float64(stats.CacheMax) * 100.0 } else { stats.CacheUsage = 0.0 } // Set collection timestamp stats.CollectedAt = time.Now().Format(time.RFC3339) return stats, nil }