up
Some checks failed
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 09:37:15 +02:00
parent e00f6365da
commit 6e45066e37
349 changed files with 17160 additions and 1867 deletions

View File

@@ -0,0 +1,268 @@
using System.Collections.Generic;
using System.Linq;
using StellaOps.Scanner.Analyzers.Lang;
namespace StellaOps.Bench.ScannerAnalyzers.Scenarios;
internal static class NodeBenchMetrics
{
private static readonly HashSet<string> Extensions = new(StringComparer.OrdinalIgnoreCase)
{
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
".mts",
".cts"
};
private static readonly string[] IgnoredDirectories =
{
".bin",
".cache",
".store",
"__pycache__"
};
public static IReadOnlyDictionary<string, double> Compute(string rootPath, LanguageAnalyzerResult result)
{
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
ArgumentNullException.ThrowIfNull(result);
var scanRoots = CollectImportScanRoots(rootPath, result);
var packagesScanned = 0;
var filesScanned = 0;
long bytesScanned = 0;
var cappedPackages = 0;
foreach (var scanRoot in scanRoots)
{
var counters = CountImportScan(scanRoot);
packagesScanned++;
filesScanned += counters.FilesScanned;
bytesScanned += counters.BytesScanned;
if (counters.Capped)
{
cappedPackages++;
}
}
return new SortedDictionary<string, double>(StringComparer.Ordinal)
{
["node.importScan.packages"] = packagesScanned,
["node.importScan.filesScanned"] = filesScanned,
["node.importScan.bytesScanned"] = bytesScanned,
["node.importScan.cappedPackages"] = cappedPackages
};
}
public static bool AreEqual(IReadOnlyDictionary<string, double> left, IReadOnlyDictionary<string, double> right)
{
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);
if (ReferenceEquals(left, right))
{
return true;
}
if (left.Count != right.Count)
{
return false;
}
foreach (var (key, value) in left)
{
if (!right.TryGetValue(key, out var other) || other != value)
{
return false;
}
}
return true;
}
private static IReadOnlyList<string> CollectImportScanRoots(string rootPath, LanguageAnalyzerResult result)
{
var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
var roots = new HashSet<string>(comparer);
var fullRoot = Path.GetFullPath(rootPath);
foreach (var record in result.Components)
{
if (!string.Equals(record.AnalyzerId, "node", StringComparison.Ordinal))
{
continue;
}
if (!record.Metadata.TryGetValue("path", out var relativePath) || string.IsNullOrWhiteSpace(relativePath))
{
continue;
}
var isRoot = string.Equals(relativePath, ".", StringComparison.Ordinal);
var isWorkspaceMember = record.Metadata.TryGetValue("workspaceMember", out var workspaceMember)
&& string.Equals(workspaceMember, "true", StringComparison.OrdinalIgnoreCase);
if (!isRoot && !isWorkspaceMember)
{
continue;
}
var absolute = isRoot
? fullRoot
: Path.GetFullPath(Path.Combine(fullRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
if (Directory.Exists(absolute))
{
roots.Add(absolute);
}
}
return roots.OrderBy(static p => p, StringComparer.Ordinal).ToArray();
}
private static ImportScanCounters CountImportScan(string rootPath)
{
const int maxFilesPerPackage = 500;
const long maxBytesPerPackage = 5L * 1024 * 1024;
const long maxFileBytes = 512L * 1024;
const int maxDepth = 20;
var filesScanned = 0;
long bytesScanned = 0;
var capped = false;
foreach (var file in EnumerateSourceFiles(rootPath, maxDepth))
{
if (filesScanned >= maxFilesPerPackage || bytesScanned >= maxBytesPerPackage)
{
capped = true;
break;
}
long length;
try
{
length = new FileInfo(file).Length;
}
catch
{
continue;
}
if (length <= 0 || length > maxFileBytes)
{
continue;
}
if (bytesScanned + length > maxBytesPerPackage)
{
capped = true;
break;
}
bytesScanned += length;
filesScanned++;
}
return new ImportScanCounters(filesScanned, bytesScanned, capped);
}
private static IEnumerable<string> EnumerateSourceFiles(string root, int maxDepth)
{
var pathComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
var stack = new Stack<(string Path, int Depth)>();
stack.Push((root, 0));
while (stack.Count > 0)
{
var (current, depth) = stack.Pop();
IEnumerable<string> files;
try
{
files = Directory.EnumerateFiles(current, "*", SearchOption.TopDirectoryOnly);
}
catch
{
files = Array.Empty<string>();
}
foreach (var file in files.OrderBy(static f => f, pathComparer))
{
var ext = Path.GetExtension(file);
if (!string.IsNullOrWhiteSpace(ext) && Extensions.Contains(ext))
{
yield return file;
}
}
if (depth >= maxDepth)
{
continue;
}
IEnumerable<string> dirs;
try
{
dirs = Directory.EnumerateDirectories(current, "*", SearchOption.TopDirectoryOnly);
}
catch
{
dirs = Array.Empty<string>();
}
var ordered = dirs
.Where(static d => !ShouldSkipImportDirectory(Path.GetFileName(d)))
.OrderBy(static d => d, pathComparer)
.ToArray();
for (var i = ordered.Length - 1; i >= 0; i--)
{
stack.Push((ordered[i], depth + 1));
}
}
}
private static bool ShouldSkipImportDirectory(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
return true;
}
if (string.Equals(name, "node_modules", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(name, ".pnpm", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return ShouldSkipDirectory(name);
}
private static bool ShouldSkipDirectory(string name)
{
if (name.Length == 0)
{
return true;
}
if (name[0] == '.')
{
return !string.Equals(name, ".pnpm", StringComparison.OrdinalIgnoreCase);
}
return IgnoredDirectories.Any(ignored => string.Equals(name, ignored, StringComparison.OrdinalIgnoreCase));
}
private readonly record struct ImportScanCounters(int FilesScanned, long BytesScanned, bool Capped);
}

View File

@@ -43,7 +43,10 @@ internal static class Program
stats.P95Ms,
stats.MaxMs,
iterations,
scenarioThreshold);
scenarioThreshold)
{
Metrics = execution.Metrics
};
results.Add(result);
@@ -101,7 +104,7 @@ internal static class Program
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
Console.Error.WriteLine(ex);
return 1;
}
}

View File

@@ -53,6 +53,7 @@ internal static class BenchmarkJsonWriter
report.Result.P95Ms,
report.Result.MaxMs,
report.Result.ThresholdMs,
report.Result.Metrics,
baseline is null
? null
: new BenchmarkJsonScenarioBaseline(
@@ -84,6 +85,7 @@ internal static class BenchmarkJsonWriter
double P95Ms,
double MaxMs,
double ThresholdMs,
IReadOnlyDictionary<string, double>? Metrics,
BenchmarkJsonScenarioBaseline? Baseline,
BenchmarkJsonScenarioRegression Regression);

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
namespace StellaOps.Bench.ScannerAnalyzers.Reporting;
@@ -35,7 +36,13 @@ internal sealed class BenchmarkScenarioReport
}
var percentage = (MaxRegressionRatio.Value - 1d) * 100d;
return $"{Result.Id} exceeded regression budget: max {Result.MaxMs:F2} ms vs baseline {Baseline!.MaxMs:F2} ms (+{percentage:F1}%)";
return string.Format(
CultureInfo.InvariantCulture,
"{0} exceeded regression budget: max {1:F2} ms vs baseline {2:F2} ms (+{3:F1}%)",
Result.Id,
Result.MaxMs,
Baseline!.MaxMs,
percentage);
}
private static double? CalculateRatio(double current, double? baseline)

View File

@@ -1,4 +1,5 @@
using System.Globalization;
using System.Linq;
using System.Text;
namespace StellaOps.Bench.ScannerAnalyzers.Reporting;
@@ -20,6 +21,10 @@ internal static class PrometheusWriter
var builder = new StringBuilder();
builder.AppendLine("# HELP scanner_analyzer_bench_duration_ms Analyzer benchmark duration metrics in milliseconds.");
builder.AppendLine("# TYPE scanner_analyzer_bench_duration_ms gauge");
builder.AppendLine("# HELP scanner_analyzer_bench_sample_count Analyzer benchmark sample counts (component/file counts).");
builder.AppendLine("# TYPE scanner_analyzer_bench_sample_count gauge");
builder.AppendLine("# HELP scanner_analyzer_bench_metric Additional analyzer benchmark metrics.");
builder.AppendLine("# TYPE scanner_analyzer_bench_metric gauge");
foreach (var report in reports)
{
@@ -28,6 +33,7 @@ internal static class PrometheusWriter
AppendMetric(builder, "scanner_analyzer_bench_p95_ms", scenarioLabel, report.Result.P95Ms);
AppendMetric(builder, "scanner_analyzer_bench_max_ms", scenarioLabel, report.Result.MaxMs);
AppendMetric(builder, "scanner_analyzer_bench_threshold_ms", scenarioLabel, report.Result.ThresholdMs);
AppendMetric(builder, "scanner_analyzer_bench_sample_count", scenarioLabel, report.Result.SampleCount);
if (report.Baseline is { } baseline)
{
@@ -41,6 +47,19 @@ internal static class PrometheusWriter
AppendMetric(builder, "scanner_analyzer_bench_regression_limit", scenarioLabel, report.RegressionLimit);
AppendMetric(builder, "scanner_analyzer_bench_regression_breached", scenarioLabel, report.RegressionBreached ? 1 : 0);
}
if (report.Result.Metrics is { Count: > 0 } metrics)
{
foreach (var metric in metrics.OrderBy(static item => item.Key, StringComparer.Ordinal))
{
builder.Append("scanner_analyzer_bench_metric{scenario=\"");
builder.Append(scenarioLabel);
builder.Append("\",name=\"");
builder.Append(Escape(metric.Key));
builder.Append("\"} ");
builder.AppendLine(metric.Value.ToString("G17", CultureInfo.InvariantCulture));
}
}
}
File.WriteAllText(resolved, builder.ToString(), Encoding.UTF8);

View File

@@ -12,6 +12,8 @@ internal sealed record ScenarioResult(
int Iterations,
double ThresholdMs)
{
public IReadOnlyDictionary<string, double>? Metrics { get; init; }
public string IdColumn => Id.Length <= 28 ? Id.PadRight(28) : Id[..28];
public string SampleCountColumn => SampleCount.ToString(CultureInfo.InvariantCulture).PadLeft(5);

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Bun;
using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Java;
using StellaOps.Scanner.Analyzers.Lang.Node;
@@ -17,7 +18,7 @@ internal interface IScenarioRunner
Task<ScenarioExecutionResult> ExecuteAsync(string rootPath, int iterations, CancellationToken cancellationToken);
}
internal sealed record ScenarioExecutionResult(double[] Durations, int SampleCount);
internal sealed record ScenarioExecutionResult(double[] Durations, int SampleCount, IReadOnlyDictionary<string, double>? Metrics = null);
internal static class ScenarioRunnerFactory
{
@@ -40,6 +41,7 @@ internal static class ScenarioRunnerFactory
internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
{
private readonly IReadOnlyList<Func<ILanguageAnalyzer>> _analyzerFactories;
private readonly bool _includesNodeAnalyzer;
public LanguageAnalyzerScenarioRunner(IEnumerable<string> analyzerIds)
{
@@ -48,11 +50,15 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
throw new ArgumentNullException(nameof(analyzerIds));
}
_analyzerFactories = analyzerIds
var normalizedIds = analyzerIds
.Where(static id => !string.IsNullOrWhiteSpace(id))
.Select(CreateFactory)
.Select(static id => id.Trim().ToLowerInvariant())
.ToArray();
_includesNodeAnalyzer = normalizedIds.Contains("node", StringComparer.Ordinal);
_analyzerFactories = normalizedIds.Select(CreateFactory).ToArray();
if (_analyzerFactories.Count == 0)
{
throw new InvalidOperationException("At least one analyzer id must be provided.");
@@ -70,6 +76,7 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
var engine = new LanguageAnalyzerEngine(analyzers);
var durations = new double[iterations];
var componentCount = -1;
IReadOnlyDictionary<string, double>? metrics = null;
for (var i = 0; i < iterations; i++)
{
@@ -91,6 +98,19 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
{
throw new InvalidOperationException($"Analyzer output count changed between iterations ({componentCount} vs {currentCount}).");
}
if (_includesNodeAnalyzer)
{
var currentMetrics = NodeBenchMetrics.Compute(rootPath, result);
if (metrics is null)
{
metrics = currentMetrics;
}
else if (!NodeBenchMetrics.AreEqual(metrics, currentMetrics))
{
throw new InvalidOperationException($"Analyzer metrics changed between iterations for '{rootPath}'.");
}
}
}
if (componentCount < 0)
@@ -98,7 +118,7 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
componentCount = 0;
}
return new ScenarioExecutionResult(durations, componentCount);
return new ScenarioExecutionResult(durations, componentCount, metrics);
}
private static Func<ILanguageAnalyzer> CreateFactory(string analyzerId)
@@ -106,6 +126,7 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
var id = analyzerId.Trim().ToLowerInvariant();
return id switch
{
"bun" => static () => new BunLanguageAnalyzer(),
"java" => static () => new JavaLanguageAnalyzer(),
"go" => static () => new GoLanguageAnalyzer(),
"node" => static () => new NodeLanguageAnalyzer(),

View File

@@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/StellaOps.Scanner.Analyzers.Lang.Bun.csproj" />
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj" />
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
@@ -21,4 +22,4 @@
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Bench.ScannerAnalyzers.Tests" />
</ItemGroup>
</Project>
</Project>