using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.Bench.LinkNotMerge; internal sealed record BenchmarkConfig( double? ThresholdMs, double? MinThroughputPerSecond, double? MinInsertThroughputPerSecond, double? MaxAllocatedMb, int? Iterations, IReadOnlyList Scenarios) { public static async Task LoadAsync(string path) { ArgumentException.ThrowIfNullOrWhiteSpace(path); var resolved = Path.GetFullPath(path); if (!File.Exists(resolved)) { throw new FileNotFoundException($"Benchmark configuration '{resolved}' was not found.", resolved); } await using var stream = File.OpenRead(resolved); var model = await JsonSerializer.DeserializeAsync( stream, new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }).ConfigureAwait(false); if (model is null) { throw new InvalidOperationException($"Benchmark configuration '{resolved}' could not be parsed."); } if (model.Scenarios.Count == 0) { throw new InvalidOperationException($"Benchmark configuration '{resolved}' does not contain any scenarios."); } foreach (var scenario in model.Scenarios) { scenario.Validate(); } return new BenchmarkConfig( model.ThresholdMs, model.MinThroughputPerSecond, model.MinInsertThroughputPerSecond, model.MaxAllocatedMb, model.Iterations, model.Scenarios); } private sealed class BenchmarkConfigModel { [JsonPropertyName("thresholdMs")] public double? ThresholdMs { get; init; } [JsonPropertyName("minThroughputPerSecond")] public double? MinThroughputPerSecond { get; init; } [JsonPropertyName("minInsertThroughputPerSecond")] public double? MinInsertThroughputPerSecond { get; init; } [JsonPropertyName("maxAllocatedMb")] public double? MaxAllocatedMb { get; init; } [JsonPropertyName("iterations")] public int? Iterations { get; init; } [JsonPropertyName("scenarios")] public List Scenarios { get; init; } = new(); } } internal sealed class LinkNotMergeScenarioConfig { private const int DefaultObservationCount = 5_000; private const int DefaultAliasGroups = 500; private const int DefaultPurlsPerObservation = 4; private const int DefaultCpesPerObservation = 2; private const int DefaultReferencesPerObservation = 3; private const int DefaultTenants = 4; private const int DefaultBatchSize = 500; private const int DefaultSeed = 42_022; [JsonPropertyName("id")] public string? Id { get; init; } [JsonPropertyName("label")] public string? Label { get; init; } [JsonPropertyName("observations")] public int? Observations { get; init; } [JsonPropertyName("aliasGroups")] public int? AliasGroups { get; init; } [JsonPropertyName("purlsPerObservation")] public int? PurlsPerObservation { get; init; } [JsonPropertyName("cpesPerObservation")] public int? CpesPerObservation { get; init; } [JsonPropertyName("referencesPerObservation")] public int? ReferencesPerObservation { get; init; } [JsonPropertyName("tenants")] public int? Tenants { get; init; } [JsonPropertyName("batchSize")] public int? BatchSize { get; init; } [JsonPropertyName("seed")] public int? Seed { get; init; } [JsonPropertyName("iterations")] public int? Iterations { get; init; } [JsonPropertyName("thresholdMs")] public double? ThresholdMs { get; init; } [JsonPropertyName("minThroughputPerSecond")] public double? MinThroughputPerSecond { get; init; } [JsonPropertyName("minInsertThroughputPerSecond")] public double? MinInsertThroughputPerSecond { get; init; } [JsonPropertyName("maxAllocatedMb")] public double? MaxAllocatedMb { get; init; } public string ScenarioId => string.IsNullOrWhiteSpace(Id) ? "linknotmerge" : Id!.Trim(); public string DisplayLabel => string.IsNullOrWhiteSpace(Label) ? ScenarioId : Label!.Trim(); public int ResolveObservationCount() => Observations.HasValue && Observations.Value > 0 ? Observations.Value : DefaultObservationCount; public int ResolveAliasGroups() => AliasGroups.HasValue && AliasGroups.Value > 0 ? AliasGroups.Value : DefaultAliasGroups; public int ResolvePurlsPerObservation() => PurlsPerObservation.HasValue && PurlsPerObservation.Value > 0 ? PurlsPerObservation.Value : DefaultPurlsPerObservation; public int ResolveCpesPerObservation() => CpesPerObservation.HasValue && CpesPerObservation.Value >= 0 ? CpesPerObservation.Value : DefaultCpesPerObservation; public int ResolveReferencesPerObservation() => ReferencesPerObservation.HasValue && ReferencesPerObservation.Value >= 0 ? ReferencesPerObservation.Value : DefaultReferencesPerObservation; public int ResolveTenantCount() => Tenants.HasValue && Tenants.Value > 0 ? Tenants.Value : DefaultTenants; public int ResolveBatchSize() => BatchSize.HasValue && BatchSize.Value > 0 ? BatchSize.Value : DefaultBatchSize; public int ResolveSeed() => Seed.HasValue && Seed.Value > 0 ? Seed.Value : DefaultSeed; public int ResolveIterations(int? defaultIterations) { var iterations = Iterations ?? defaultIterations ?? 3; if (iterations <= 0) { throw new InvalidOperationException($"Scenario '{ScenarioId}' requires iterations > 0."); } return iterations; } public void Validate() { if (ResolveObservationCount() <= 0) { throw new InvalidOperationException($"Scenario '{ScenarioId}' requires observations > 0."); } if (ResolveAliasGroups() <= 0) { throw new InvalidOperationException($"Scenario '{ScenarioId}' requires aliasGroups > 0."); } if (ResolvePurlsPerObservation() <= 0) { throw new InvalidOperationException($"Scenario '{ScenarioId}' requires purlsPerObservation > 0."); } if (ResolveTenantCount() <= 0) { throw new InvalidOperationException($"Scenario '{ScenarioId}' requires tenants > 0."); } if (ResolveBatchSize() > ResolveObservationCount()) { throw new InvalidOperationException($"Scenario '{ScenarioId}' batchSize cannot exceed observations."); } } }