From 5e9eaf8a4c842f32bec0398bc7bbd9f1dfded9e8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 30 Sep 2026 17:01:11 -0400 Subject: [PATCH] perf(fill): reuse validation for an unchanged row Skip FillGrid's Step 2 overlap check only when Step 1 checked this exact row clean and the perpendicular tiling appended zero parts, so gridResult still holds the same Part objects in the same order and poses. The local rowIsVerified flag is cleared by every Step 1 skip or bbox fallback, and any nonzero append (including partial-copy parts) keeps the eager check. Debug work assertions pin the reduced overlap work (horizontal stripe 8->4 exact calls, 4->2 preparations, 16->8 triangulations; vertical stripe 36->18, 4->2, 38->19) and keep single-seed, Horizontal full-grid, partial-only and PerpOnly work at base; the invalid overlapping-seed control pins both fallback stages plus the retained exact-call total. Three mutants (forced eager check, dropped flag, complete-rows-only) are caught by their designated controls. Serial whole-job layout is byte-identical to the frozen 62b5a8d0 oracle before and after. --- OpenNest.Engine/Fill/FillLinear.cs | 13 +- .../Fill/FillLinearValidationReuseTests.cs | 142 +++++++++++++++++- docs/performance/fill-verification.md | 2 +- 3 files changed, 148 insertions(+), 9 deletions(-) diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index ec4a4c7..8a19366 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -362,6 +362,9 @@ namespace OpenNest.Engine.Fill { var perpAxis = PerpendicularAxis(direction); + // Set only when Step 1's check executed, passed, and no fallback replaced the row. + var rowIsVerified = false; + // Step 1: Tile along primary axis var row = new List(pattern.Parts); row.AddRange(TilePattern(pattern, direction, cache)); @@ -372,6 +375,10 @@ namespace OpenNest.Engine.Fill row = new List(pattern.Parts); row.AddRange(TilePatternBbox(pattern, direction)); } + else if (pattern.Parts.Count > 1) + { + rowIsVerified = true; + } // If primary tiling didn't produce copies, just tile along perpendicular if (row.Count <= pattern.Parts.Count) @@ -396,7 +403,11 @@ namespace OpenNest.Engine.Fill var gridResult = new List(rowPattern.Parts); gridResult.AddRange(TilePattern(rowPattern, perpAxis, cache)); - if (HasOverlappingParts(gridResult, out var a3, out var b3)) + // Only the unchanged row is covered by Step 1's clean verdict: skip Step 2 + // only when the perpendicular tiling appended zero parts, so gridResult + // still contains the same Part objects, in the same order and poses. + if ((!rowIsVerified || gridResult.Count != row.Count) + && HasOverlappingParts(gridResult, out var a3, out var b3)) { LogOverlap("Step2-Perp", perpAxis, rowPattern, gridResult, a3, b3); gridResult = new List(rowPattern.Parts); diff --git a/OpenNest.Tests/Fill/FillLinearValidationReuseTests.cs b/OpenNest.Tests/Fill/FillLinearValidationReuseTests.cs index b33deb4..25d676d 100644 --- a/OpenNest.Tests/Fill/FillLinearValidationReuseTests.cs +++ b/OpenNest.Tests/Fill/FillLinearValidationReuseTests.cs @@ -165,6 +165,112 @@ public class FillLinearValidationReuseTests output.WriteLine($"{direction}: rejected={rejected:R} accepted={accepted:R}"); } +#if DEBUG + // Work-count assertions rely on Debug-only PerfCounters increments; the Release + // properties would read zero, so the whole methods compile only in Debug. + [Theory] + [InlineData("horizontal-stripe", 8, 4, 2, 8)] + [InlineData("vertical-stripe", 19, 18, 2, 19)] + public void UnchangedRow_ReducesStep2OverlapWork(string mode, int count, int exact, int preparations, int triangulations) + { + // One production Fill call between counter resets; the check itself is + // independent validity, taken after the counters are captured. + var pattern = MakePattern("concave", "rotated"); + var direction = mode == "vertical-stripe" ? NestDirection.Vertical : NestDirection.Horizontal; + var area = mode == "vertical-stripe" + ? new Box(0, 0, pattern.BoundingBox.Length, 96) + : new Box(0, 0, 96, pattern.BoundingBox.Width); + long observedExact = -1, observedPreparations = -1, observedTriangulations = -1; + List parts; + PerfCounters.Reset(); + try + { + parts = new FillLinear(area, 0.5).Fill(pattern, direction); + observedExact = PerfCounters.PartIntersects; + observedPreparations = PerfCounters.OverlapPolygonPreparations; + observedTriangulations = PerfCounters.PolygonTriangulations; + // Validity checks run after capturing the counters but inside the measured + // window's try, so the outer finally leaves zero residual counters. + Assert.Equal(count, parts.Count); + AssertValid(parts, area); + Assert.Equal(exact, observedExact); + Assert.Equal(preparations, observedPreparations); + Assert.Equal(triangulations, observedTriangulations); + output.WriteLine($"{mode}: exact={observedExact}; preparations={observedPreparations}; triangulations={observedTriangulations}"); + } + finally + { + PerfCounters.Reset(); + } + } + + [Theory] + [InlineData("single-seed-stripe", 7, 1, 8)] + [InlineData("full-grid", 36, 4, 44)] + [InlineData("partial-only", 36, 4, 38)] + public void GuardedControls_KeepEagerStep2Work(string mode, int exact, int preparations, int triangulations) + { + // Single seed (Step 1 skipped), nonzero perpendicular additions, and partial-only + // additions must all keep the eager Step 2 check: identical work counts to base. + var pattern = MakePattern("concave", mode == "single-seed-stripe" ? "single" : "rotated"); + var vertical = mode == "partial-only"; + var direction = vertical ? NestDirection.Vertical : NestDirection.Horizontal; + var area = mode switch + { + "full-grid" => new Box(0, 0, 96, 48), + "partial-only" => new Box(0, 0, 1.8 * pattern.BoundingBox.Length, 96), + _ => new Box(0, 0, 96, pattern.BoundingBox.Width), + }; + long observedExact = -1, observedPreparations = -1, observedTriangulations = -1; + List parts; + PerfCounters.Reset(); + try + { + parts = new FillLinear(area, 0.5).Fill(pattern, direction); + observedExact = PerfCounters.PartIntersects; + observedPreparations = PerfCounters.OverlapPolygonPreparations; + observedTriangulations = PerfCounters.PolygonTriangulations; + Assert.Equal(exact, observedExact); + Assert.Equal(preparations, observedPreparations); + Assert.Equal(triangulations, observedTriangulations); + output.WriteLine($"{mode}: exact={observedExact}; preparations={observedPreparations}; triangulations={observedTriangulations}"); + } + finally + { + PerfCounters.Reset(); + } + } + + [Theory] + [InlineData(NestDirection.Horizontal, 9)] + [InlineData(NestDirection.Vertical, 3)] + public void PerpOnly_KeepsPerpOnlyCheckWork(NestDirection direction, int exact) + { + // The untouched Step1-PerpOnly path keeps checking; PartIntersects counts every + // exact checker call the path makes (measured on the clean dbcf7de base). + var pattern = MakePattern("concave", "rotated"); + var area = direction == NestDirection.Horizontal + ? new Box(0, 0, pattern.BoundingBox.Length, 48) + : new Box(0, 0, 48, pattern.BoundingBox.Width); + long observed = -1; + List parts; + PerfCounters.Reset(); + try + { + parts = new FillLinear(area, 0.5).Fill(pattern, direction); + observed = PerfCounters.PartIntersects; + Assert.Equal(exact, observed); + Assert.NotEmpty(parts); + AssertValid(parts, area); + output.WriteLine($"PerpOnly {direction}: exact={observed}"); + } + finally + { + PerfCounters.Reset(); + } + } +#endif + [Fact] public void InvalidOverlappingSeeds_PreserveBothFallbacks_EvenWithoutPerpendicularAdditions() { @@ -184,14 +290,27 @@ public class FillLinearValidationReuseTests Trace.Listeners.Add(listener); try { -#endif var expected = frozen.Fill(pattern, NestDirection.Horizontal); - var actual = filler.Fill(pattern, NestDirection.Horizontal); - Assert.Equal(6, expected.Count); - AssertLayout(expected, actual, pattern.Parts.Select(p => p.Program).ToArray()); - Assert.True(FillHelpers.HasOverlappingParts(actual)); - Assert.Equal(before, PatternSnapshot(pattern)); -#if DEBUG + long exactWithGuard; + PerfCounters.Reset(); + try + { + var actual = filler.Fill(pattern, NestDirection.Horizontal); + exactWithGuard = PerfCounters.PartIntersects; + Assert.Equal(6, expected.Count); + AssertLayout(expected, actual, pattern.Parts.Select(p => p.Program).ToArray()); + Assert.True(FillHelpers.HasOverlappingParts(actual)); + Assert.Equal(before, PatternSnapshot(pattern)); + // The Step 1 fallback clears rowIsVerified, so the Step 2 check must + // still execute despite zero perpendicular additions: its exact + // short-circuit call is retained. Layout equality alone would miss a + // wrongly skipped check whose fallback happens to reproduce the same row. + Assert.Equal(2, exactWithGuard); + } + finally + { + PerfCounters.Reset(); + } foreach (var label in new[] { "invalid-frozen", "invalid-production" }) { var records = listener.Records.Where(r => r.Label == label).ToArray(); @@ -208,6 +327,15 @@ public class FillLinearValidationReuseTests { Trace.Listeners.Remove(listener); } +#else + { + var expected = frozen.Fill(pattern, NestDirection.Horizontal); + var actual = filler.Fill(pattern, NestDirection.Horizontal); + Assert.Equal(6, expected.Count); + AssertLayout(expected, actual, pattern.Parts.Select(p => p.Program).ToArray()); + Assert.True(FillHelpers.HasOverlappingParts(actual)); + Assert.Equal(before, PatternSnapshot(pattern)); + } #endif } diff --git a/docs/performance/fill-verification.md b/docs/performance/fill-verification.md index 680a8d3..c9dfa14 100644 --- a/docs/performance/fill-verification.md +++ b/docs/performance/fill-verification.md @@ -28,7 +28,7 @@ OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Rel `PreStep3FillLinear.cs` freezes production `FillLinear` at **1b23ad79f25d77fdd745bfea029de9dff2a91f6d**, separately from the older `LegacyFillLinear`. Reverse only the documented type/constructor rename, namespace/visibility/import additions, nullable directive and import formatting; retain a zero-diff comparison against `git show :OpenNest.Engine/Fill/FillLinear.cs` and SHA-256 hashes. Do not refactor the oracle or use it for before timings. `FillLinearValidationReuseTests` compares ordered IEEE-754 poses/bounds, CNC values/program sharing, drawing references and immutable inputs through both public `Fill` overloads, including concurrent calls on one filler. Valid fixtures assert closed positive-area material and no overlap. The separate invalid overlapping-seed case intentionally remains invalid; in Debug its listener confirms Step1 and Step2 fallback pair `(0,1)` with six parts at each step (zero perpendicular additions). It makes no claim that bbox fallback repairs the seeds. -Keep future validation-work counter assertions Debug-only in `FillCacheCollection`, reset counters in `finally`, and retain the invalid fallback and single-seed/partial-only controls. This baseline does not assert a future skipped check or introduce production counters. Whole-job exact preservation needs a separately repeated serial oracle (fresh process, `DOTNET_PROCESSOR_COUNT=1`, verified one-worker thread-pool cap, solve on that worker); ordinary parallel layouts can differ on the same tree, and the serial gate does not replace concurrent differential tests. +Keep validation-work counter assertions Debug-only in `FillCacheCollection`, reset counters in `finally`, and retain the invalid fallback and single-seed/partial-only controls. Since the S1 guard shipped (re-delivered 2026-09-30 on `dbcf7de`), `UnchangedRow_ReducesStep2OverlapWork` asserts the reduced Step 2 work (horizontal stripe 8→4 exact / 4→2 preparations / 16→8 triangulations; vertical stripe 36→18 / 4→2 / 38→19) and `GuardedControls_KeepEagerStep2Work` plus `PerpOnly_KeepsPerpOnlyCheckWork` assert single-seed (7/1/8), Horizontal full-grid (36/4/44, matching the 2026-09-27 table), partial-only (36/4/38) and PerpOnly exact-call (H=9/V=3) work stay at base on the current tree. The work-count methods compile entirely within DEBUG — Release counter properties read zero, and empty passing shells would misreport coverage. The invalid fallback control additionally pins production's exact-call total at 2 so a wrongly skipped Step 2 check cannot hide behind a layout match. Whole-job exact preservation needs a separately repeated serial oracle (fresh process, `DOTNET_PROCESSOR_COUNT=1`, verified one-worker thread-pool cap, solve on that worker); ordinary parallel layouts can differ on the same tree, and the serial gate does not replace concurrent differential tests. Debug behavior/skipped-work checks: