From 2c4edb7e8004f1c9d663163cf6c65eac76bf42c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 18:34:38 +0000 Subject: [PATCH 1/2] Pick benchmark cells from the live NOAA catalog by rule Two of the three hard-coded cells (US5WA50M, US3WA01M) are no longer in NOAA's catalog, so the first Benchmarks run measured only US4MD81M. NOAA withdraws and renames cells as it re-schemes its coverage, so choose cells by rule instead: for each usage band (harbour, approach, coastal), the active cell with update files at the 90th percentile of download size. Explicit cell names still override the rule. The current US4MD81M (edition 36) is about 320 KB, against the 3.5 MB edition the synthetic Approach profile was calibrated on. Say so in the profile docs: the larger profiles stand for the bigger cells an application may still meet, not a typical current cell. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ --- scripts/DownloadBenchmarkCells.cs | 49 ++++++++++++++++--- .../PerformanceCells.cs | 2 +- tests/Shared/SyntheticCellProfile.cs | 9 ++-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/scripts/DownloadBenchmarkCells.cs b/scripts/DownloadBenchmarkCells.cs index 1b2442a..803f7bb 100644 --- a/scripts/DownloadBenchmarkCells.cs +++ b/scripts/DownloadBenchmarkCells.cs @@ -7,11 +7,17 @@ using EncDotNet.Noaa; // ============================================================================ -// DownloadBenchmarkCells.cs — Downloads a fixed set of real NOAA ENC cells for +// DownloadBenchmarkCells.cs — Downloads a few real NOAA ENC cells for // the benchmarks in tests/EncDotNet.Benchmarks. // // Usage: dotnet run scripts/DownloadBenchmarkCells.cs -- [cell...] // +// Without cell names, the script picks one cell per usage band (harbour, approach +// and coastal) from the live NOAA catalog: among active cells with update files, +// the one at the 90th percentile of download size, so the set is substantial but +// typical. Picking by rule rather than by name survives NOAA withdrawing or +// renaming cells, which it does as it re-schemes its coverage. +// // Each cell is extracted to // with its update files. Point // ENCDOTNET_ENC_ROOT at to include them in a benchmark run. The // script writes /cells.txt recording each cell's edition, update @@ -24,9 +30,10 @@ // Never commit the downloaded cells (see CONTRIBUTING.md). // ============================================================================ -// A harbour, an approach and a coastal cell, all with update files. Together they -// cover the range of record counts and edge lengths in typical NOAA cells. -string[] defaultCells = ["US5WA50M", "US4MD81M", "US3WA01M"]; +// Navigational purpose (usage band), the third character of the cell file name +// (S-57 Appendix B.1, 5.6.4). +(char Band, string Purpose)[] bands = [('5', "harbour"), ('4', "approach"), ('3', "coastal")]; +const double sizePercentile = 0.9; if (args.Length == 0) { @@ -35,13 +42,43 @@ } var outputDirectory = Path.GetFullPath(args[0]); -var cellNames = args.Length > 1 ? args[1..] : defaultCells; Directory.CreateDirectory(outputDirectory); using var catalogClient = new EncProductCatalogClient(); var catalog = await catalogClient.GetNoaaCatalogAsync(); var cellsByName = catalog.Cells.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase); +string[] cellNames; +if (args.Length > 1) +{ + cellNames = args[1..]; +} +else +{ + var picked = new List(); + foreach (var (band, purpose) in bands) + { + var candidates = catalog.Cells + .Where(c => c.Name.Length == 8 && c.Name[2] == band) + .Where(c => c.UpdateNumber > 0 && c.ZipfileSize > 0) + .Where(c => !c.Status.Contains("cancel", StringComparison.OrdinalIgnoreCase)) + .OrderBy(c => c.ZipfileSize) + .ThenBy(c => c.Name, StringComparer.Ordinal) + .ToList(); + if (candidates.Count == 0) + { + Console.Error.WriteLine($"warning: no active {purpose} cell with updates in the NOAA catalog."); + continue; + } + + var cell = candidates[(int)((candidates.Count - 1) * sizePercentile)]; + Console.WriteLine($"Picked {cell.Name} as the {purpose} cell ({candidates.Count} candidates, zip {cell.ZipfileSize:N0} bytes)."); + picked.Add(cell.Name); + } + + cellNames = [.. picked]; +} + using var httpClient = new HttpClient(); var manifest = new List(); var missing = 0; @@ -96,4 +133,4 @@ } File.WriteAllLines(Path.Combine(outputDirectory, "cells.txt"), manifest); -return missing == cellNames.Length ? 1 : 0; +return cellNames.Length == 0 || missing == cellNames.Length ? 1 : 0; diff --git a/tests/EncDotNet.PerformanceTests/PerformanceCells.cs b/tests/EncDotNet.PerformanceTests/PerformanceCells.cs index 37cab38..500dc4c 100644 --- a/tests/EncDotNet.PerformanceTests/PerformanceCells.cs +++ b/tests/EncDotNet.PerformanceTests/PerformanceCells.cs @@ -13,7 +13,7 @@ internal static class PerformanceCells private static readonly Lazy LazyHarbourX4 = new(() => new Inputs(SyntheticCellProfile.Harbour.Scale(4))); /// - /// Gets the harbour-scale cell, shaped like NOAA US5WA50M. + /// Gets the harbour-scale cell, shaped like an earlier edition of NOAA US5WA50M. /// public static Inputs Harbour => LazyHarbour.Value; diff --git a/tests/Shared/SyntheticCellProfile.cs b/tests/Shared/SyntheticCellProfile.cs index f3f435e..770bcaf 100644 --- a/tests/Shared/SyntheticCellProfile.cs +++ b/tests/Shared/SyntheticCellProfile.cs @@ -6,7 +6,10 @@ namespace EncDotNet.Testing; /// /// The built-in profiles are calibrated against real NOAA cells: record counts, the /// distribution of edge lengths, attributes per feature and spatial pointers per feature -/// follow the measured averages of the cell named in each profile's remarks. +/// follow the measured averages of the cell named in each profile's summary. Those were +/// earlier editions; NOAA has since re-schemed its coverage into smaller cells (edition 36 +/// of US4MD81M is about 320 KB), so the larger profiles now stand for the bigger cells an +/// application may still meet rather than for a typical current one. /// /// The profile name, used in the generated data set name and in benchmark output. /// The seed for the generator; the same profile always yields the same bytes. @@ -35,7 +38,7 @@ public sealed record SyntheticCellProfile( int AreaFeatures) { /// - /// A harbour-scale cell shaped like NOAA US5WA50M (about 400 KB). + /// A harbour-scale cell shaped like an earlier edition of NOAA US5WA50M (about 400 KB). /// public static SyntheticCellProfile Harbour { get; } = new( Name: "Harbour", @@ -52,7 +55,7 @@ public sealed record SyntheticCellProfile( AreaFeatures: 211); /// - /// An approach-scale cell shaped like NOAA US4MD81M (about 3.5 MB). + /// An approach-scale cell shaped like an earlier edition of NOAA US4MD81M (about 3.5 MB). /// public static SyntheticCellProfile Approach { get; } = new( Name: "Approach", From 5a4518009e1f06c859c9a1d7e0da28bf0cd83b53 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 19:26:47 +0000 Subject: [PATCH 2/2] Shorten benchmark iterations to halve the comparison's run time Each case took 10-15 s under BenchmarkDotNet's default 500 ms iterations, whatever the operation's cost, so the Benchmarks job took 26 minutes. Locally, 15 iterations of 200 ms halve a pass (420 s to 214 s) with the median's standard error unchanged (2.06% to 1.85%). Running in-process saved only 7%, so it stays out-of-process. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ --- .github/workflows/benchmarks.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 5bfc8a5..8a69f9d 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -122,7 +122,10 @@ jobs: BASE_BUILT: ${{ steps.build_base.outcome == 'success' }} run: | DLL=tests/EncDotNet.Benchmarks/bin/Release/net10.0/EncDotNet.Benchmarks.dll - ARGS=(--filter '*' --exporters json --warmupCount 3 --iterationCount 10 --launchCount 1) + # BenchmarkDotNet's default 500 ms iterations make each case take 10-15 s however + # fast the operation is. Shorter iterations, with more of them, halve the run time + # while keeping the median as precise (standard error about 2%). + ARGS=(--filter '*' --exporters json --warmupCount 3 --iterationCount 15 --iterationTime 200 --launchCount 1) if [ "$SCOPE" = "core" ]; then ARGS+=(--anyCategories Core) fi