new advisories work and features gaps work
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
|
||||
// Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-OPS-04)
|
||||
// Task: Add ops endpoints for health, bench, cache, and config
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.BinaryIndex.Cache;
|
||||
using StellaOps.BinaryIndex.Disassembly.B2R2;
|
||||
|
||||
namespace StellaOps.BinaryIndex.WebService.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Ops endpoints for BinaryIndex health, benchmarking, cache stats, and configuration.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/ops/binaryindex")]
|
||||
[Produces("application/json")]
|
||||
public sealed class BinaryIndexOpsController : ControllerBase
|
||||
{
|
||||
private readonly B2R2LifterPool? _lifterPool;
|
||||
private readonly FunctionIrCacheService? _cacheService;
|
||||
private readonly IOptions<B2R2LifterPoolOptions> _poolOptions;
|
||||
private readonly IOptions<FunctionIrCacheOptions> _cacheOptions;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<BinaryIndexOpsController> _logger;
|
||||
|
||||
public BinaryIndexOpsController(
|
||||
ILogger<BinaryIndexOpsController> logger,
|
||||
TimeProvider timeProvider,
|
||||
IOptions<B2R2LifterPoolOptions> poolOptions,
|
||||
IOptions<FunctionIrCacheOptions> cacheOptions,
|
||||
B2R2LifterPool? lifterPool = null,
|
||||
FunctionIrCacheService? cacheService = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_poolOptions = poolOptions ?? throw new ArgumentNullException(nameof(poolOptions));
|
||||
_cacheOptions = cacheOptions ?? throw new ArgumentNullException(nameof(cacheOptions));
|
||||
_lifterPool = lifterPool;
|
||||
_cacheService = cacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets BinaryIndex health status including lifter warmness and cache availability.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Health response with component status.</returns>
|
||||
[HttpGet("health")]
|
||||
[ProducesResponseType<BinaryIndexOpsHealthResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status503ServiceUnavailable)]
|
||||
public ActionResult<BinaryIndexOpsHealthResponse> GetHealth(CancellationToken ct)
|
||||
{
|
||||
var lifterStatus = "unavailable";
|
||||
var lifterWarm = false;
|
||||
var lifterPoolStats = ImmutableDictionary<string, int>.Empty;
|
||||
|
||||
if (_lifterPool != null)
|
||||
{
|
||||
var stats = _lifterPool.GetStats();
|
||||
lifterStatus = stats.IsWarm ? "warm" : "cold";
|
||||
lifterWarm = stats.IsWarm;
|
||||
lifterPoolStats = stats.IsaStats
|
||||
.ToImmutableDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value.PooledCount + kv.Value.ActiveCount);
|
||||
}
|
||||
|
||||
var cacheStatus = "unavailable";
|
||||
var cacheEnabled = false;
|
||||
if (_cacheService != null)
|
||||
{
|
||||
var cacheStats = _cacheService.GetStats();
|
||||
cacheStatus = cacheStats.IsEnabled ? "enabled" : "disabled";
|
||||
cacheEnabled = cacheStats.IsEnabled;
|
||||
}
|
||||
|
||||
var response = new BinaryIndexOpsHealthResponse(
|
||||
Status: lifterWarm && cacheEnabled ? "healthy" : "degraded",
|
||||
Timestamp: _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
|
||||
LifterStatus: lifterStatus,
|
||||
LifterWarm: lifterWarm,
|
||||
LifterPoolStats: lifterPoolStats,
|
||||
CacheStatus: cacheStatus,
|
||||
CacheEnabled: cacheEnabled);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a quick benchmark and returns latency metrics.
|
||||
/// </summary>
|
||||
/// <param name="request">Optional bench parameters.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Benchmark response with latency measurements.</returns>
|
||||
[HttpPost("bench/run")]
|
||||
[ProducesResponseType<BinaryIndexBenchResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
|
||||
public ActionResult<BinaryIndexBenchResponse> RunBench(
|
||||
[FromBody] BinaryIndexBenchRequest? request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var iterations = request?.Iterations ?? 10;
|
||||
if (iterations < 1 || iterations > 1000)
|
||||
{
|
||||
return BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid iterations",
|
||||
Detail = "Iterations must be between 1 and 1000",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation("Running BinaryIndex benchmark with {Iterations} iterations", iterations);
|
||||
|
||||
var lifterLatencies = new List<double>();
|
||||
var cacheLatencies = new List<double>();
|
||||
|
||||
// Benchmark lifter acquisition if available
|
||||
if (_lifterPool != null)
|
||||
{
|
||||
var isa = new B2R2.ISA(B2R2.Architecture.Intel, B2R2.WordSize.Bit64);
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var sw = Stopwatch.StartNew();
|
||||
using (var lifter = _lifterPool.Acquire(isa))
|
||||
{
|
||||
// Just acquire and release
|
||||
}
|
||||
sw.Stop();
|
||||
lifterLatencies.Add(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark cache lookup if available
|
||||
if (_cacheService != null)
|
||||
{
|
||||
var dummyKey = new FunctionCacheKey(
|
||||
Isa: "intel-64",
|
||||
B2R2Version: "0.9.1",
|
||||
NormalizationRecipe: "v1",
|
||||
CanonicalIrHash: "0000000000000000000000000000000000000000000000000000000000000000");
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var sw = Stopwatch.StartNew();
|
||||
// Fire and forget the cache lookup
|
||||
_ = _cacheService.TryGetAsync(dummyKey, ct).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
cacheLatencies.Add(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
var lifterStats = ComputeLatencyStats(lifterLatencies);
|
||||
var cacheStats = ComputeLatencyStats(cacheLatencies);
|
||||
|
||||
var response = new BinaryIndexBenchResponse(
|
||||
Timestamp: _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
|
||||
Iterations: iterations,
|
||||
LifterAcquireLatencyMs: lifterStats,
|
||||
CacheLookupLatencyMs: cacheStats);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets function IR cache statistics.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Cache statistics.</returns>
|
||||
[HttpGet("cache")]
|
||||
[ProducesResponseType<BinaryIndexFunctionCacheStats>(StatusCodes.Status200OK)]
|
||||
public ActionResult<BinaryIndexFunctionCacheStats> GetCacheStats(CancellationToken ct)
|
||||
{
|
||||
if (_cacheService == null)
|
||||
{
|
||||
return Ok(new BinaryIndexFunctionCacheStats(
|
||||
Enabled: false,
|
||||
Hits: 0,
|
||||
Misses: 0,
|
||||
Evictions: 0,
|
||||
HitRate: 0.0,
|
||||
KeyPrefix: "",
|
||||
CacheTtlSeconds: 0));
|
||||
}
|
||||
|
||||
var stats = _cacheService.GetStats();
|
||||
|
||||
return Ok(new BinaryIndexFunctionCacheStats(
|
||||
Enabled: stats.IsEnabled,
|
||||
Hits: stats.Hits,
|
||||
Misses: stats.Misses,
|
||||
Evictions: stats.Evictions,
|
||||
HitRate: stats.HitRate,
|
||||
KeyPrefix: stats.KeyPrefix,
|
||||
CacheTtlSeconds: (long)stats.CacheTtl.TotalSeconds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets effective BinaryIndex configuration.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Effective configuration (secrets redacted).</returns>
|
||||
[HttpGet("config")]
|
||||
[ProducesResponseType<BinaryIndexEffectiveConfig>(StatusCodes.Status200OK)]
|
||||
public ActionResult<BinaryIndexEffectiveConfig> GetConfig(CancellationToken ct)
|
||||
{
|
||||
var poolOptions = _poolOptions.Value;
|
||||
var cacheOptions = _cacheOptions.Value;
|
||||
|
||||
return Ok(new BinaryIndexEffectiveConfig(
|
||||
LifterPoolMaxSizePerIsa: poolOptions.MaxPoolSizePerIsa,
|
||||
LifterPoolWarmPreloadEnabled: poolOptions.EnableWarmPreload,
|
||||
LifterPoolWarmPreloadIsas: poolOptions.WarmPreloadIsas,
|
||||
LifterPoolAcquireTimeoutSeconds: (long)poolOptions.AcquireTimeout.TotalSeconds,
|
||||
CacheEnabled: cacheOptions.Enabled,
|
||||
CacheKeyPrefix: cacheOptions.KeyPrefix,
|
||||
CacheTtlSeconds: (long)cacheOptions.CacheTtl.TotalSeconds,
|
||||
CacheMaxTtlSeconds: (long)cacheOptions.MaxTtl.TotalSeconds,
|
||||
B2R2Version: cacheOptions.B2R2Version,
|
||||
NormalizationRecipeVersion: cacheOptions.NormalizationRecipeVersion));
|
||||
}
|
||||
|
||||
private static BinaryIndexLatencyStats ComputeLatencyStats(List<double> latencies)
|
||||
{
|
||||
if (latencies.Count == 0)
|
||||
{
|
||||
return new BinaryIndexLatencyStats(
|
||||
Min: 0,
|
||||
Max: 0,
|
||||
Mean: 0,
|
||||
P50: 0,
|
||||
P95: 0,
|
||||
P99: 0);
|
||||
}
|
||||
|
||||
latencies.Sort();
|
||||
var count = latencies.Count;
|
||||
|
||||
return new BinaryIndexLatencyStats(
|
||||
Min: latencies[0],
|
||||
Max: latencies[^1],
|
||||
Mean: latencies.Average(),
|
||||
P50: latencies[count / 2],
|
||||
P95: latencies[(int)(count * 0.95)],
|
||||
P99: latencies[(int)(count * 0.99)]);
|
||||
}
|
||||
}
|
||||
|
||||
#region Response Models
|
||||
|
||||
/// <summary>
|
||||
/// BinaryIndex health response.
|
||||
/// </summary>
|
||||
public sealed record BinaryIndexOpsHealthResponse(
|
||||
string Status,
|
||||
string Timestamp,
|
||||
string LifterStatus,
|
||||
bool LifterWarm,
|
||||
ImmutableDictionary<string, int> LifterPoolStats,
|
||||
string CacheStatus,
|
||||
bool CacheEnabled);
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark request parameters.
|
||||
/// </summary>
|
||||
public sealed record BinaryIndexBenchRequest(
|
||||
int Iterations = 10);
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark response with latency measurements.
|
||||
/// </summary>
|
||||
public sealed record BinaryIndexBenchResponse(
|
||||
string Timestamp,
|
||||
int Iterations,
|
||||
BinaryIndexLatencyStats LifterAcquireLatencyMs,
|
||||
BinaryIndexLatencyStats CacheLookupLatencyMs);
|
||||
|
||||
/// <summary>
|
||||
/// Latency statistics.
|
||||
/// </summary>
|
||||
public sealed record BinaryIndexLatencyStats(
|
||||
double Min,
|
||||
double Max,
|
||||
double Mean,
|
||||
double P50,
|
||||
double P95,
|
||||
double P99);
|
||||
|
||||
/// <summary>
|
||||
/// Function IR cache statistics.
|
||||
/// </summary>
|
||||
public sealed record BinaryIndexFunctionCacheStats(
|
||||
bool Enabled,
|
||||
long Hits,
|
||||
long Misses,
|
||||
long Evictions,
|
||||
double HitRate,
|
||||
string KeyPrefix,
|
||||
long CacheTtlSeconds);
|
||||
|
||||
/// <summary>
|
||||
/// Effective BinaryIndex configuration.
|
||||
/// </summary>
|
||||
public sealed record BinaryIndexEffectiveConfig(
|
||||
int LifterPoolMaxSizePerIsa,
|
||||
bool LifterPoolWarmPreloadEnabled,
|
||||
ImmutableArray<string> LifterPoolWarmPreloadIsas,
|
||||
long LifterPoolAcquireTimeoutSeconds,
|
||||
bool CacheEnabled,
|
||||
string CacheKeyPrefix,
|
||||
long CacheTtlSeconds,
|
||||
long CacheMaxTtlSeconds,
|
||||
string B2R2Version,
|
||||
string NormalizationRecipeVersion);
|
||||
|
||||
#endregion
|
||||
@@ -22,6 +22,7 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Persistence/StellaOps.BinaryIndex.Persistence.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.VexBridge/StellaOps.BinaryIndex.VexBridge.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.GoldenSet/StellaOps.BinaryIndex.GoldenSet.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Disassembly.B2R2/StellaOps.BinaryIndex.Disassembly.B2R2.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user