// 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();
}