// Copyright (c) StellaOps. All rights reserved. // Licensed under BUSL-1.1. See LICENSE in the project root. // Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-CACHE-03) // Task: Function-level cache for canonical IR and semantic fingerprints using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace StellaOps.BinaryIndex.Cache; /// /// Configuration options for the function IR cache. /// public sealed class FunctionIrCacheOptions { /// /// Configuration section name. /// public const string SectionName = "StellaOps:BinaryIndex:FunctionIrCache"; /// /// Valkey key prefix for function IR cache entries. /// public string KeyPrefix { get; init; } = "stellaops:binidx:funccache:"; /// /// TTL for cached function IR entries. /// public TimeSpan CacheTtl { get; init; } = TimeSpan.FromHours(4); /// /// Maximum TTL for any cache entry. /// public TimeSpan MaxTtl { get; init; } = TimeSpan.FromHours(24); /// /// Whether to enable the cache. /// public bool Enabled { get; init; } = true; /// /// B2R2 version string to include in cache keys. /// public string B2R2Version { get; init; } = "0.9.1"; /// /// Normalization recipe version for cache key stability. /// public string NormalizationRecipeVersion { get; init; } = "v1"; } /// /// Cache key components for function IR caching. /// /// ISA identifier (e.g., "intel-64"). /// B2R2 version string. /// Normalization recipe version. /// SHA-256 hash of the canonical IR bytes. public sealed record FunctionCacheKey( string Isa, string B2R2Version, string NormalizationRecipe, string CanonicalIrHash) { /// /// Converts to a deterministic cache key string. /// public string ToKeyString() => string.Format( CultureInfo.InvariantCulture, "{0}:{1}:{2}:{3}", Isa, B2R2Version, NormalizationRecipe, CanonicalIrHash); } /// /// Cached function IR and semantic fingerprint entry. /// /// Original function address. /// Original function name. /// Computed semantic fingerprint. /// Number of IR statements. /// Number of basic blocks. /// When the fingerprint was computed (ISO-8601). /// B2R2 version used. /// Normalization recipe used. public sealed record CachedFunctionFingerprint( ulong FunctionAddress, string FunctionName, string SemanticFingerprint, int IrStatementCount, int BasicBlockCount, string ComputedAtUtc, string B2R2Version, string NormalizationRecipe); /// /// Cache statistics for the function IR cache. /// public sealed record FunctionIrCacheStats( long Hits, long Misses, long Evictions, double HitRate, bool IsEnabled, string KeyPrefix, TimeSpan CacheTtl); /// /// Service for caching function IR and semantic fingerprints. /// Uses Valkey as hot cache with deterministic key generation. /// public sealed class FunctionIrCacheService { private readonly IDistributedCache _cache; private readonly ILogger _logger; private readonly FunctionIrCacheOptions _options; private readonly TimeProvider _timeProvider; // Thread-safe statistics private long _hits; private long _misses; private long _evictions; private static readonly JsonSerializerOptions s_jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; /// /// Creates a new function IR cache service. /// public FunctionIrCacheService( IDistributedCache cache, ILogger logger, IOptions options, TimeProvider timeProvider) { _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _options = options?.Value ?? new FunctionIrCacheOptions(); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } /// /// Gets the current cache statistics. /// public FunctionIrCacheStats GetStats() { var hits = Interlocked.Read(ref _hits); var misses = Interlocked.Read(ref _misses); var total = hits + misses; var hitRate = total > 0 ? (double)hits / total : 0.0; return new FunctionIrCacheStats( Hits: hits, Misses: misses, Evictions: Interlocked.Read(ref _evictions), HitRate: hitRate, IsEnabled: _options.Enabled, KeyPrefix: _options.KeyPrefix, CacheTtl: _options.CacheTtl); } /// /// Tries to get a cached function fingerprint. /// /// The cache key. /// Cancellation token. /// The cached fingerprint if found, null otherwise. public async Task TryGetAsync( FunctionCacheKey key, CancellationToken ct = default) { if (!_options.Enabled) { return null; } var cacheKey = BuildCacheKey(key); try { var bytes = await _cache.GetAsync(cacheKey, ct).ConfigureAwait(false); if (bytes is null || bytes.Length == 0) { Interlocked.Increment(ref _misses); return null; } var result = JsonSerializer.Deserialize(bytes, s_jsonOptions); Interlocked.Increment(ref _hits); _logger.LogTrace( "Cache hit for function {FunctionName} at {Address}", result?.FunctionName, result?.FunctionAddress); return result; } catch (Exception ex) { _logger.LogWarning(ex, "Failed to get cached function fingerprint for key {Key}", cacheKey); Interlocked.Increment(ref _misses); return null; } } /// /// Sets a function fingerprint in the cache. /// /// The cache key. /// The fingerprint to cache. /// Cancellation token. public async Task SetAsync( FunctionCacheKey key, CachedFunctionFingerprint fingerprint, CancellationToken ct = default) { if (!_options.Enabled) { return; } var cacheKey = BuildCacheKey(key); try { var bytes = JsonSerializer.SerializeToUtf8Bytes(fingerprint, s_jsonOptions); var options = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = _options.CacheTtl }; await _cache.SetAsync(cacheKey, bytes, options, ct).ConfigureAwait(false); _logger.LogTrace( "Cached function {FunctionName} fingerprint with key {Key}", fingerprint.FunctionName, cacheKey); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to cache function fingerprint for key {Key}", cacheKey); } } /// /// Removes a cached function fingerprint. /// /// The cache key. /// Cancellation token. public async Task RemoveAsync(FunctionCacheKey key, CancellationToken ct = default) { if (!_options.Enabled) { return; } var cacheKey = BuildCacheKey(key); try { await _cache.RemoveAsync(cacheKey, ct).ConfigureAwait(false); Interlocked.Increment(ref _evictions); _logger.LogTrace("Removed cached function fingerprint for key {Key}", cacheKey); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to remove cached function fingerprint for key {Key}", cacheKey); } } /// /// Computes a canonical IR hash from function bytes. /// /// The canonical IR bytes. /// Hex-encoded SHA-256 hash. public static string ComputeCanonicalIrHash(ReadOnlySpan irBytes) { Span hashBytes = stackalloc byte[32]; SHA256.HashData(irBytes, hashBytes); return Convert.ToHexString(hashBytes).ToLowerInvariant(); } /// /// Creates a cache key for a function. /// /// ISA identifier. /// The canonical IR bytes. /// The cache key. public FunctionCacheKey CreateKey(string isa, ReadOnlySpan canonicalIrBytes) { var hash = ComputeCanonicalIrHash(canonicalIrBytes); return new FunctionCacheKey( Isa: isa, B2R2Version: _options.B2R2Version, NormalizationRecipe: _options.NormalizationRecipeVersion, CanonicalIrHash: hash); } private string BuildCacheKey(FunctionCacheKey key) => _options.KeyPrefix + key.ToKeyString(); }