Add Policy DSL Validator, Schema Exporter, and Simulation Smoke tools
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			- Implemented PolicyDslValidator with command-line options for strict mode and JSON output. - Created PolicySchemaExporter to generate JSON schemas for policy-related models. - Developed PolicySimulationSmoke tool to validate policy simulations against expected outcomes. - Added project files and necessary dependencies for each tool. - Ensured proper error handling and usage instructions across tools.
This commit is contained in:
		| @@ -0,0 +1,376 @@ | ||||
| using System.Globalization; | ||||
| using StellaOps.Bench.LinkNotMerge.Vex.Baseline; | ||||
| using StellaOps.Bench.LinkNotMerge.Vex.Reporting; | ||||
|  | ||||
| namespace StellaOps.Bench.LinkNotMerge.Vex; | ||||
|  | ||||
| internal static class Program | ||||
| { | ||||
|     public static async Task<int> Main(string[] args) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var options = ProgramOptions.Parse(args); | ||||
|             var config = await VexBenchmarkConfig.LoadAsync(options.ConfigPath).ConfigureAwait(false); | ||||
|             var baseline = await BaselineLoader.LoadAsync(options.BaselinePath, CancellationToken.None).ConfigureAwait(false); | ||||
|  | ||||
|             var results = new List<VexScenarioResult>(); | ||||
|             var reports = new List<BenchmarkScenarioReport>(); | ||||
|             var failures = new List<string>(); | ||||
|  | ||||
|             foreach (var scenario in config.Scenarios) | ||||
|             { | ||||
|                 var iterations = scenario.ResolveIterations(config.Iterations); | ||||
|                 var runner = new VexScenarioRunner(scenario); | ||||
|                 var execution = runner.Execute(iterations, CancellationToken.None); | ||||
|  | ||||
|                 var totalStats = DurationStatistics.From(execution.TotalDurationsMs); | ||||
|                 var insertStats = DurationStatistics.From(execution.InsertDurationsMs); | ||||
|                 var correlationStats = DurationStatistics.From(execution.CorrelationDurationsMs); | ||||
|                 var allocationStats = AllocationStatistics.From(execution.AllocatedMb); | ||||
|                 var observationThroughputStats = ThroughputStatistics.From(execution.ObservationThroughputsPerSecond); | ||||
|                 var eventThroughputStats = ThroughputStatistics.From(execution.EventThroughputsPerSecond); | ||||
|  | ||||
|                 var thresholdMs = scenario.ThresholdMs ?? options.ThresholdMs ?? config.ThresholdMs; | ||||
|                 var observationFloor = scenario.MinThroughputPerSecond ?? options.MinThroughputPerSecond ?? config.MinThroughputPerSecond; | ||||
|                 var eventFloor = scenario.MinEventThroughputPerSecond ?? options.MinEventThroughputPerSecond ?? config.MinEventThroughputPerSecond; | ||||
|                 var allocationLimit = scenario.MaxAllocatedMb ?? options.MaxAllocatedMb ?? config.MaxAllocatedMb; | ||||
|  | ||||
|                 var result = new VexScenarioResult( | ||||
|                     scenario.ScenarioId, | ||||
|                     scenario.DisplayLabel, | ||||
|                     iterations, | ||||
|                     execution.ObservationCount, | ||||
|                     execution.AliasGroups, | ||||
|                     execution.StatementCount, | ||||
|                     execution.EventCount, | ||||
|                     totalStats, | ||||
|                     insertStats, | ||||
|                     correlationStats, | ||||
|                     observationThroughputStats, | ||||
|                     eventThroughputStats, | ||||
|                     allocationStats, | ||||
|                     thresholdMs, | ||||
|                     observationFloor, | ||||
|                     eventFloor, | ||||
|                     allocationLimit); | ||||
|  | ||||
|                 results.Add(result); | ||||
|  | ||||
|                 if (thresholdMs is { } threshold && result.TotalStatistics.MaxMs > threshold) | ||||
|                 { | ||||
|                     failures.Add($"{result.Id} exceeded total latency threshold: {result.TotalStatistics.MaxMs:F2} ms > {threshold:F2} ms"); | ||||
|                 } | ||||
|  | ||||
|                 if (observationFloor is { } obsFloor && result.ObservationThroughputStatistics.MinPerSecond < obsFloor) | ||||
|                 { | ||||
|                     failures.Add($"{result.Id} fell below observation throughput floor: {result.ObservationThroughputStatistics.MinPerSecond:N0} obs/s < {obsFloor:N0} obs/s"); | ||||
|                 } | ||||
|  | ||||
|                 if (eventFloor is { } evtFloor && result.EventThroughputStatistics.MinPerSecond < evtFloor) | ||||
|                 { | ||||
|                     failures.Add($"{result.Id} fell below event throughput floor: {result.EventThroughputStatistics.MinPerSecond:N0} events/s < {evtFloor:N0} events/s"); | ||||
|                 } | ||||
|  | ||||
|                 if (allocationLimit is { } limit && result.AllocationStatistics.MaxAllocatedMb > limit) | ||||
|                 { | ||||
|                     failures.Add($"{result.Id} exceeded allocation budget: {result.AllocationStatistics.MaxAllocatedMb:F2} MB > {limit:F2} MB"); | ||||
|                 } | ||||
|  | ||||
|                 baseline.TryGetValue(result.Id, out var baselineEntry); | ||||
|                 var report = new BenchmarkScenarioReport(result, baselineEntry, options.RegressionLimit); | ||||
|                 reports.Add(report); | ||||
|                 failures.AddRange(report.BuildRegressionFailureMessages()); | ||||
|             } | ||||
|  | ||||
|             TablePrinter.Print(results); | ||||
|  | ||||
|             if (!string.IsNullOrWhiteSpace(options.CsvOutPath)) | ||||
|             { | ||||
|                 CsvWriter.Write(options.CsvOutPath!, results); | ||||
|             } | ||||
|  | ||||
|             if (!string.IsNullOrWhiteSpace(options.JsonOutPath)) | ||||
|             { | ||||
|                 var metadata = new BenchmarkJsonMetadata( | ||||
|                     SchemaVersion: "linknotmerge-vex-bench/1.0", | ||||
|                     CapturedAtUtc: (options.CapturedAtUtc ?? DateTimeOffset.UtcNow).ToUniversalTime(), | ||||
|                     Commit: options.Commit, | ||||
|                     Environment: options.Environment); | ||||
|  | ||||
|                 await BenchmarkJsonWriter.WriteAsync(options.JsonOutPath!, metadata, reports, CancellationToken.None).ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|             if (!string.IsNullOrWhiteSpace(options.PrometheusOutPath)) | ||||
|             { | ||||
|                 PrometheusWriter.Write(options.PrometheusOutPath!, reports); | ||||
|             } | ||||
|  | ||||
|             if (failures.Count > 0) | ||||
|             { | ||||
|                 Console.Error.WriteLine(); | ||||
|                 Console.Error.WriteLine("Benchmark failures detected:"); | ||||
|                 foreach (var failure in failures.Distinct()) | ||||
|                 { | ||||
|                     Console.Error.WriteLine($" - {failure}"); | ||||
|                 } | ||||
|  | ||||
|                 return 1; | ||||
|             } | ||||
|  | ||||
|             return 0; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Console.Error.WriteLine($"linknotmerge-vex-bench error: {ex.Message}"); | ||||
|             return 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private sealed record ProgramOptions( | ||||
|         string ConfigPath, | ||||
|         int? Iterations, | ||||
|         double? ThresholdMs, | ||||
|         double? MinThroughputPerSecond, | ||||
|         double? MinEventThroughputPerSecond, | ||||
|         double? MaxAllocatedMb, | ||||
|         string? CsvOutPath, | ||||
|         string? JsonOutPath, | ||||
|         string? PrometheusOutPath, | ||||
|         string BaselinePath, | ||||
|         DateTimeOffset? CapturedAtUtc, | ||||
|         string? Commit, | ||||
|         string? Environment, | ||||
|         double? RegressionLimit) | ||||
|     { | ||||
|         public static ProgramOptions Parse(string[] args) | ||||
|         { | ||||
|             var configPath = DefaultConfigPath(); | ||||
|             var baselinePath = DefaultBaselinePath(); | ||||
|  | ||||
|             int? iterations = null; | ||||
|             double? thresholdMs = null; | ||||
|             double? minThroughput = null; | ||||
|             double? minEventThroughput = null; | ||||
|             double? maxAllocated = null; | ||||
|             string? csvOut = null; | ||||
|             string? jsonOut = null; | ||||
|             string? promOut = null; | ||||
|             DateTimeOffset? capturedAt = null; | ||||
|             string? commit = null; | ||||
|             string? environment = null; | ||||
|             double? regressionLimit = null; | ||||
|  | ||||
|             for (var index = 0; index < args.Length; index++) | ||||
|             { | ||||
|                 var current = args[index]; | ||||
|                 switch (current) | ||||
|                 { | ||||
|                     case "--config": | ||||
|                         EnsureNext(args, index); | ||||
|                         configPath = Path.GetFullPath(args[++index]); | ||||
|                         break; | ||||
|                     case "--iterations": | ||||
|                         EnsureNext(args, index); | ||||
|                         iterations = int.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--threshold-ms": | ||||
|                         EnsureNext(args, index); | ||||
|                         thresholdMs = double.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--min-throughput": | ||||
|                         EnsureNext(args, index); | ||||
|                         minThroughput = double.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--min-event-throughput": | ||||
|                         EnsureNext(args, index); | ||||
|                         minEventThroughput = double.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--max-allocated-mb": | ||||
|                         EnsureNext(args, index); | ||||
|                         maxAllocated = double.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--csv": | ||||
|                         EnsureNext(args, index); | ||||
|                         csvOut = args[++index]; | ||||
|                         break; | ||||
|                     case "--json": | ||||
|                         EnsureNext(args, index); | ||||
|                         jsonOut = args[++index]; | ||||
|                         break; | ||||
|                     case "--prometheus": | ||||
|                         EnsureNext(args, index); | ||||
|                         promOut = args[++index]; | ||||
|                         break; | ||||
|                     case "--baseline": | ||||
|                         EnsureNext(args, index); | ||||
|                         baselinePath = Path.GetFullPath(args[++index]); | ||||
|                         break; | ||||
|                     case "--captured-at": | ||||
|                         EnsureNext(args, index); | ||||
|                         capturedAt = DateTimeOffset.Parse(args[++index], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); | ||||
|                         break; | ||||
|                     case "--commit": | ||||
|                         EnsureNext(args, index); | ||||
|                         commit = args[++index]; | ||||
|                         break; | ||||
|                     case "--environment": | ||||
|                         EnsureNext(args, index); | ||||
|                         environment = args[++index]; | ||||
|                         break; | ||||
|                     case "--regression-limit": | ||||
|                         EnsureNext(args, index); | ||||
|                         regressionLimit = double.Parse(args[++index], CultureInfo.InvariantCulture); | ||||
|                         break; | ||||
|                     case "--help": | ||||
|                     case "-h": | ||||
|                         PrintUsage(); | ||||
|                         System.Environment.Exit(0); | ||||
|                         break; | ||||
|                     default: | ||||
|                         throw new ArgumentException($"Unknown argument '{current}'."); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return new ProgramOptions( | ||||
|                 configPath, | ||||
|                 iterations, | ||||
|                 thresholdMs, | ||||
|                 minThroughput, | ||||
|                 minEventThroughput, | ||||
|                 maxAllocated, | ||||
|                 csvOut, | ||||
|                 jsonOut, | ||||
|                 promOut, | ||||
|                 baselinePath, | ||||
|                 capturedAt, | ||||
|                 commit, | ||||
|                 environment, | ||||
|                 regressionLimit); | ||||
|         } | ||||
|  | ||||
|         private static string DefaultConfigPath() | ||||
|         { | ||||
|             var binaryDir = AppContext.BaseDirectory; | ||||
|             var projectDir = Path.GetFullPath(Path.Combine(binaryDir, "..", "..", "..")); | ||||
|             var benchRoot = Path.GetFullPath(Path.Combine(projectDir, "..")); | ||||
|             return Path.Combine(benchRoot, "config.json"); | ||||
|         } | ||||
|  | ||||
|         private static string DefaultBaselinePath() | ||||
|         { | ||||
|             var binaryDir = AppContext.BaseDirectory; | ||||
|             var projectDir = Path.GetFullPath(Path.Combine(binaryDir, "..", "..", "..")); | ||||
|             var benchRoot = Path.GetFullPath(Path.Combine(projectDir, "..")); | ||||
|             return Path.Combine(benchRoot, "baseline.csv"); | ||||
|         } | ||||
|  | ||||
|         private static void EnsureNext(string[] args, int index) | ||||
|         { | ||||
|             if (index + 1 >= args.Length) | ||||
|             { | ||||
|                 throw new ArgumentException("Missing value for argument."); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static void PrintUsage() | ||||
|         { | ||||
|             Console.WriteLine("Usage: linknotmerge-vex-bench [options]"); | ||||
|             Console.WriteLine(); | ||||
|             Console.WriteLine("Options:"); | ||||
|             Console.WriteLine("  --config <path>                 Path to benchmark configuration JSON."); | ||||
|             Console.WriteLine("  --iterations <count>            Override iteration count."); | ||||
|             Console.WriteLine("  --threshold-ms <value>          Global latency threshold in milliseconds."); | ||||
|             Console.WriteLine("  --min-throughput <value>        Observation throughput floor (observations/second)."); | ||||
|             Console.WriteLine("  --min-event-throughput <value>  Event emission throughput floor (events/second)."); | ||||
|             Console.WriteLine("  --max-allocated-mb <value>      Global allocation ceiling (MB)."); | ||||
|             Console.WriteLine("  --csv <path>                    Write CSV results to path."); | ||||
|             Console.WriteLine("  --json <path>                   Write JSON results to path."); | ||||
|             Console.WriteLine("  --prometheus <path>             Write Prometheus exposition metrics to path."); | ||||
|             Console.WriteLine("  --baseline <path>               Baseline CSV path."); | ||||
|             Console.WriteLine("  --captured-at <iso8601>         Timestamp to embed in JSON metadata."); | ||||
|             Console.WriteLine("  --commit <sha>                  Commit identifier for metadata."); | ||||
|             Console.WriteLine("  --environment <name>            Environment label for metadata."); | ||||
|             Console.WriteLine("  --regression-limit <value>      Regression multiplier (default 1.15)."); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal static class TablePrinter | ||||
| { | ||||
|     public static void Print(IEnumerable<VexScenarioResult> results) | ||||
|     { | ||||
|         Console.WriteLine("Scenario                     |   Observations | Statements |  Events |  Total(ms) | Correl(ms) |  Insert(ms) | Obs k/s | Evnt k/s | Alloc(MB)"); | ||||
|         Console.WriteLine("---------------------------- | ------------- | ---------- | ------- | ---------- | ---------- | ----------- | ------- | -------- | --------"); | ||||
|         foreach (var row in results) | ||||
|         { | ||||
|             Console.WriteLine(string.Join(" | ", new[] | ||||
|             { | ||||
|                 row.IdColumn, | ||||
|                 row.ObservationsColumn, | ||||
|                 row.StatementColumn, | ||||
|                 row.EventColumn, | ||||
|                 row.TotalMeanColumn, | ||||
|                 row.CorrelationMeanColumn, | ||||
|                 row.InsertMeanColumn, | ||||
|                 row.ObservationThroughputColumn, | ||||
|                 row.EventThroughputColumn, | ||||
|                 row.AllocatedColumn, | ||||
|             })); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| internal static class CsvWriter | ||||
| { | ||||
|     public static void Write(string path, IEnumerable<VexScenarioResult> results) | ||||
|     { | ||||
|         ArgumentException.ThrowIfNullOrWhiteSpace(path); | ||||
|         ArgumentNullException.ThrowIfNull(results); | ||||
|  | ||||
|         var resolved = Path.GetFullPath(path); | ||||
|         var directory = Path.GetDirectoryName(resolved); | ||||
|         if (!string.IsNullOrEmpty(directory)) | ||||
|         { | ||||
|             Directory.CreateDirectory(directory); | ||||
|         } | ||||
|  | ||||
|         using var stream = new FileStream(resolved, FileMode.Create, FileAccess.Write, FileShare.None); | ||||
|         using var writer = new StreamWriter(stream); | ||||
|         writer.WriteLine("scenario,iterations,observations,statements,events,mean_total_ms,p95_total_ms,max_total_ms,mean_insert_ms,mean_correlation_ms,mean_observation_throughput_per_sec,min_observation_throughput_per_sec,mean_event_throughput_per_sec,min_event_throughput_per_sec,max_allocated_mb"); | ||||
|  | ||||
|         foreach (var result in results) | ||||
|         { | ||||
|             writer.Write(result.Id); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.Iterations.ToString(CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.ObservationCount.ToString(CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.StatementCount.ToString(CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.EventCount.ToString(CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.TotalStatistics.MeanMs.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.TotalStatistics.P95Ms.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.TotalStatistics.MaxMs.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.InsertStatistics.MeanMs.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.CorrelationStatistics.MeanMs.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.ObservationThroughputStatistics.MeanPerSecond.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.ObservationThroughputStatistics.MinPerSecond.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.EventThroughputStatistics.MeanPerSecond.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.EventThroughputStatistics.MinPerSecond.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.Write(','); | ||||
|             writer.Write(result.AllocationStatistics.MaxAllocatedMb.ToString("F4", CultureInfo.InvariantCulture)); | ||||
|             writer.WriteLine(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user