perf(fill): remove discarded extents pitch geometry

This commit is contained in:
aj
2026-09-25 19:52:19 -04:00
parent cec7396da6
commit 6efa6b1117
8 changed files with 961 additions and 20 deletions
+6
View File
@@ -13,11 +13,13 @@ namespace OpenNest
private static long offsetPerimeterEntities; private static long offsetPerimeterEntities;
private static long partIntersects; private static long partIntersects;
private static long fillScoreComputations; private static long fillScoreComputations;
private static long partBoundaryPreparations;
public static long FindBestFits => Interlocked.Read(ref findBestFits); public static long FindBestFits => Interlocked.Read(ref findBestFits);
public static long OffsetPerimeterEntities => Interlocked.Read(ref offsetPerimeterEntities); public static long OffsetPerimeterEntities => Interlocked.Read(ref offsetPerimeterEntities);
public static long PartIntersects => Interlocked.Read(ref partIntersects); public static long PartIntersects => Interlocked.Read(ref partIntersects);
public static long FillScoreComputations => Interlocked.Read(ref fillScoreComputations); public static long FillScoreComputations => Interlocked.Read(ref fillScoreComputations);
public static long PartBoundaryPreparations => Interlocked.Read(ref partBoundaryPreparations);
[Conditional("DEBUG")] [Conditional("DEBUG")]
public static void CountFindBestFits() => Interlocked.Increment(ref findBestFits); public static void CountFindBestFits() => Interlocked.Increment(ref findBestFits);
@@ -32,12 +34,16 @@ namespace OpenNest
[Conditional("DEBUG")] [Conditional("DEBUG")]
public static void CountFillScoreComputation() => Interlocked.Increment(ref fillScoreComputations); public static void CountFillScoreComputation() => Interlocked.Increment(ref fillScoreComputations);
[Conditional("DEBUG")]
public static void CountPartBoundaryPreparation() => Interlocked.Increment(ref partBoundaryPreparations);
public static void Reset() public static void Reset()
{ {
Interlocked.Exchange(ref findBestFits, 0); Interlocked.Exchange(ref findBestFits, 0);
Interlocked.Exchange(ref offsetPerimeterEntities, 0); Interlocked.Exchange(ref offsetPerimeterEntities, 0);
Interlocked.Exchange(ref partIntersects, 0); Interlocked.Exchange(ref partIntersects, 0);
Interlocked.Exchange(ref fillScoreComputations, 0); Interlocked.Exchange(ref fillScoreComputations, 0);
Interlocked.Exchange(ref partBoundaryPreparations, 0);
} }
} }
} }
+23 -20
View File
@@ -117,29 +117,32 @@ namespace OpenNest.Engine.Fill
{ {
var column = new List<Part> { (Part)pair.Part1.Clone(), (Part)pair.Part2.Clone() }; var column = new List<Part> { (Part)pair.Part1.Clone(), (Part)pair.Part2.Clone() };
// Find geometry-aware copy distance for the pair vertically.
var boundary1 = new PartBoundary(pair.Part1, halfSpacing);
var boundary2 = new PartBoundary(pair.Part2, halfSpacing);
// Compute vertical copy distance using bounding boxes as starting point,
// then slide down to find true geometry distance.
var pairHeight = pair.Bbox.Width; var pairHeight = pair.Bbox.Width;
var testOffset = new Vector(0, pairHeight); var copyDistance = pairHeight + partSpacing;
// Create test parts for slide distance measurement. // For finite valid geometry and nonnegative spacing, the legacy helper returns
var testPart1 = pair.Part1.CloneAtOffset(testOffset); // pairHeight + partSpacing on negative/no-hit slides. Otherwise minSlide >= 0,
var testPart2 = pair.Part2.CloneAtOffset(testOffset); // so Max(pairHeight - minSlide, pairHeight + partSpacing) is the same pitch.
// Public callers do not validate spacing; retain legacy work/behavior outside
// that domain rather than changing its results or exceptions.
if (!double.IsFinite(partSpacing) || partSpacing < 0 || !double.IsFinite(pairHeight))
{
var boundary1 = new PartBoundary(pair.Part1, halfSpacing);
var boundary2 = new PartBoundary(pair.Part2, halfSpacing);
var testOffset = new Vector(0, pairHeight);
var testPart1 = pair.Part1.CloneAtOffset(testOffset);
var testPart2 = pair.Part2.CloneAtOffset(testOffset);
// Find minimum distance from test pair sliding down toward original pair. copyDistance = FindVerticalCopyDistance(
var copyDistance = FindVerticalCopyDistance( pair.Part1,
pair.Part1, pair.Part2,
pair.Part2, testPart1,
testPart1, testPart2,
testPart2, boundary1,
boundary1, boundary2,
boundary2, pairHeight
pairHeight );
); }
if (copyDistance <= 0) if (copyDistance <= 0)
return column; return column;
+1
View File
@@ -23,6 +23,7 @@ namespace OpenNest.Engine.Fill
public PartBoundary(Part part, double spacing) public PartBoundary(Part part, double spacing)
{ {
PerfCounters.CountPartBoundaryPreparation();
var entities = ConvertProgram var entities = ConvertProgram
.ToGeometry(part.Program) .ToGeometry(part.Program)
.Where(e => e.Layer == SpecialLayers.Cut) .Where(e => e.Layer == SpecialLayers.Cut)
+325
View File
@@ -1,11 +1,336 @@
using System.Reflection;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Engine.Strategies;
using OpenNest.Tests.BestFit;
using Xunit.Abstractions;
using OpenNest.Engine.Fill; using OpenNest.Engine.Fill;
using OpenNest.Geometry; using OpenNest.Geometry;
namespace OpenNest.Tests.Fill; namespace OpenNest.Tests.Fill;
[Collection(nameof(FillCacheCollection))]
public class FillExtentsTests public class FillExtentsTests
{ {
private readonly ITestOutputHelper output;
public FillExtentsTests(ITestOutputHelper output) => this.output = output;
public static IEnumerable<object[]> DifferentialCases()
{
foreach (var shape in new[] { "rectangle", "triangle", "concave", "arc" })
foreach (var spacing in new[] { 0.0, 0.5 })
foreach (var rotated in new[] { false, true })
yield return new object[] { shape, spacing, rotated };
}
[Theory]
[MemberData(nameof(DifferentialCases))]
public void Fill_MatchesFrozenLegacy_OrderedLayoutAndInputOwnership(string shape, double spacing, bool rotated)
{
var drawing = MakeFixture(shape);
if (rotated)
drawing.Program.Rotate(System.Math.PI / 2);
var before = Snapshot(drawing);
var workArea = new Box(3, 5, 45, 27);
var areaBefore = Bounds(workArea);
var angle = rotated ? System.Math.PI / 2 : 0;
var expectedProgress = new List<(List<Part> Parts, string Message)>();
var actualProgress = new List<(List<Part> Parts, string Message)>();
var expected = new LegacyFillExtents(workArea, spacing).Fill(drawing, angle,
reportProgress: (parts, message) => expectedProgress.Add((parts, message)));
var actual = new FillExtents(workArea, spacing).Fill(drawing, angle,
reportProgress: (parts, message) => actualProgress.Add((parts, message)));
AssertSameLayout(expected, actual);
Assert.Equal(expectedProgress.Count, actualProgress.Count);
for (var i = 0; i < expectedProgress.Count; i++)
{
Assert.Equal(expectedProgress[i].Message, actualProgress[i].Message);
AssertSameLayout(expectedProgress[i].Parts, actualProgress[i].Parts);
}
AssertValidLayout(actual, workArea);
Assert.Equal(before, Snapshot(drawing));
Assert.Equal(areaBefore, Bounds(workArea));
Assert.All(actual, part => Assert.NotSame(drawing.Program, part.Program));
// Offset clones intentionally share programs until rotation takes ownership.
for (var i = 0; i < actual.Count; i++)
for (var j = 0; j < actual.Count; j++)
Assert.Equal(ReferenceEquals(expected[i].Program, expected[j].Program),
ReferenceEquals(actual[i].Program, actual[j].Program));
var siblingPrograms = actual.Skip(1).Select(part => ProgramValues(part.Program)).ToArray();
actual[0].Rotate(0.125);
for (var i = 1; i < actual.Count; i++)
Assert.Equal(siblingPrograms[i - 1], ProgramValues(actual[i].Program));
Assert.Equal(before, Snapshot(drawing));
}
[Theory]
[InlineData(5, 5)]
[InlineData(15, 10)] // One rectangle fits, the pair does not.
public void Fill_NoFit_MatchesFrozenLegacy(double length, double width)
{
var drawing = MakeRect(10, 8);
var before = Snapshot(drawing);
var area = new Box(3, 5, length, width);
var expected = new LegacyFillExtents(area, 0.5).Fill(drawing);
var actual = new FillExtents(area, 0.5).Fill(drawing);
Assert.Empty(expected);
AssertSameLayout(expected, actual);
Assert.Equal(before, Snapshot(drawing));
}
[Fact]
public void Fill_PreCancelled_MatchesFrozenLegacyRatherThanThrowing()
{
var drawing = MakeFixture("triangle");
var before = Snapshot(drawing);
var area = new Box(3, 5, 45, 27);
var token = new CancellationToken(true);
var expected = new LegacyFillExtents(area, 0.5).Fill(drawing, token: token);
var actual = new FillExtents(area, 0.5).Fill(drawing, token: token);
Assert.NotEmpty(expected);
AssertSameLayout(expected, actual);
AssertValidLayout(actual, area);
Assert.Equal(before, Snapshot(drawing));
}
[Theory]
[InlineData("rectangle", 0.0, false)]
[InlineData("triangle", 0.5, false)]
[InlineData("concave", 0.5, false)]
[InlineData("arc", 0.5, false)]
[InlineData("triangle", 0.0, true)]
[InlineData("triangle", 0.5, true)]
public void ColumnAdjustment_UsesAdjustedOrOverlapFallbackWithinBounds(string shape, double spacing, bool fallback)
{
var drawing = MakeFixture(shape);
var area = new Box(3, 5, 45, 27);
var legacy = new LegacyFillExtents(area, spacing);
var angle = fallback ? System.Math.PI / 6 : 0;
var pair = Invoke(legacy, "BuildPair", drawing, angle);
var initial = (List<Part>)Invoke(legacy, "BuildColumn", pair);
var adjusted = (List<Part>)Invoke(legacy, "AdjustColumn", pair, initial, CancellationToken.None);
var overlap = FillHelpers.HasOverlappingParts(adjusted);
Assert.NotSame(initial, adjusted);
Assert.Equal(fallback, overlap);
output.WriteLine($"{shape}, spacing={spacing}: initial={initial.Count}, adjusted={adjusted.Count}, "
+ $"same={ReferenceEquals(initial, adjusted)}, overlap={overlap}");
var progress = new List<List<Part>>();
var actual = new FillExtents(area, spacing).Fill(drawing, angle,
reportProgress: (parts, _) => progress.Add(parts));
AssertSameLayout(overlap ? initial : adjusted, progress[1]);
AssertValidLayout(progress[1], area);
AssertValidLayout(actual, area);
}
[Theory]
[InlineData("rectangle")]
[InlineData("arc")]
public void Fill_KnownLegacyTightWidthOverrun_RemainsDifferentialOnly(string shape)
{
// Baseline already extends ~1e-5 beyond this exactly tiled width at zero spacing.
// Record that limitation separately; this optimization does not fix geometry.
var area = new Box(3, 5, 40, 27);
var drawing = MakeFixture(shape);
var expected = new LegacyFillExtents(area, 0).Fill(drawing);
var actual = new FillExtents(area, 0).Fill(drawing);
AssertSameLayout(expected, actual);
Assert.Contains(expected, part => part.Right > area.Right + 1e-6);
Assert.InRange(expected.Max(part => part.Right) - area.Right, 9e-6, 11e-6);
}
[Theory]
[InlineData(-0.25)]
[InlineData(-0.5)]
public void Fill_NegativeSpacing_PreservesLegacyResultsAndInputs(double spacing)
{
var area = new Box(3, 5, 45, 27);
var drawing = MakeFixture("rectangle");
var before = Snapshot(drawing);
var expected = new LegacyFillExtents(area, spacing).Fill(drawing);
var actual = new FillExtents(area, spacing).Fill(drawing);
AssertSameLayout(expected, actual);
Assert.Equal(before, Snapshot(drawing));
}
[Theory]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
public void Fill_NonfiniteSpacing_NoFitStillReturnsEmptyWithoutValidation(double spacing)
{
var area = new Box(3, 5, 1, 1);
var drawing = MakeFixture("rectangle");
Assert.Empty(new LegacyFillExtents(area, spacing).Fill(drawing));
Assert.Empty(new FillExtents(area, spacing).Fill(drawing));
}
[Theory]
[InlineData(-0.5)]
[InlineData(-10.0)]
[InlineData(double.NegativeInfinity)]
public void BuildColumn_UnsupportedNegativeSpacing_RetainsLegacyPitch(double spacing)
{
var area = new Box(3, 5, 45, 27);
var drawing = MakeFixture("rectangle");
// Prepare finite valid pairs independently of unsupported spacing. This isolates
// the private column calculation without risking legacy nonfinite public tiling.
var legacyPair = Invoke(new LegacyFillExtents(area, 0), "BuildPair", drawing, 0.0);
var pair = Invoke(new FillExtents(area, 0), "BuildPair", drawing, 0.0);
var expected = (List<Part>)Invoke(new LegacyFillExtents(area, spacing), "BuildColumn", legacyPair);
var actual = (List<Part>)Invoke(new FillExtents(area, spacing), "BuildColumn", pair);
Assert.Equal(6, expected.Count);
AssertSameLayout(expected, actual);
Assert.Equal(8.0, actual[2].Bottom - actual[0].Bottom, 10);
}
#if DEBUG
[Theory]
[InlineData(0.0)]
[InlineData(0.5)]
public void BuildColumn_RepeatedCalls_DoNotPrepareBoundaries(double spacing)
{
var area = new Box(3, 5, 45, 27);
var drawing = MakeFixture("triangle");
var legacy = new LegacyFillExtents(area, spacing);
var filler = new FillExtents(area, spacing);
PerfCounters.Reset();
try
{
var legacyPair = Invoke(legacy, "BuildPair", drawing, 0.0);
PerfCounters.Reset();
var pair = Invoke(filler, "BuildPair", drawing, 0.0);
Assert.Equal(2, PerfCounters.PartBoundaryPreparations); // BuildPair must retain geometry.
PerfCounters.Reset();
var expected = new List<List<Part>>();
for (var i = 0; i < 4; i++)
expected.Add((List<Part>)Invoke(legacy, "BuildColumn", legacyPair));
Assert.Equal(8, PerfCounters.PartBoundaryPreparations);
PerfCounters.Reset();
for (var i = 0; i < 4; i++)
AssertSameLayout(expected[i], (List<Part>)Invoke(filler, "BuildColumn", pair));
Assert.Equal(0, PerfCounters.PartBoundaryPreparations);
}
finally
{
PerfCounters.Reset();
}
}
[Theory]
[InlineData(0.0)]
[InlineData(0.5)]
public void Fill_RebuildsColumns_OnlyBuildPairPreparesBoundaries(double spacing)
{
var area = new Box(3, 5, 45, 27);
var drawing = MakeFixture("triangle");
PerfCounters.Reset();
try
{
var expected = new LegacyFillExtents(area, spacing).Fill(drawing);
var legacyPreparations = PerfCounters.PartBoundaryPreparations;
// Only BuildPair and BuildColumn construct PartBoundary in this pipeline.
// More than four proves AdjustColumn called BuildColumn again.
Assert.True(legacyPreparations > 4);
PerfCounters.Reset();
var actual = new FillExtents(area, spacing).Fill(drawing);
output.WriteLine($"spacing={spacing}: legacy boundaries={legacyPreparations}, "
+ $"BuildColumn calls={(legacyPreparations - 2) / 2}, actual boundaries={PerfCounters.PartBoundaryPreparations}, parts={actual.Count}");
AssertSameLayout(expected, actual);
AssertValidLayout(actual, area);
Assert.Equal(2, PerfCounters.PartBoundaryPreparations);
}
finally
{
PerfCounters.Reset();
}
}
#endif
internal static Drawing MakeFixture(string shape)
{
if (shape == "rectangle")
return MakeRect(10, 8);
if (shape == "triangle")
return MakeRightTriangle(10, 8);
var program = new Program();
program.Codes.Add(new RapidMove(new Vector(0, 0)));
if (shape == "concave")
{
foreach (var point in new[] { new Vector(10, 0), new Vector(10, 3),
new Vector(4, 3), new Vector(4, 8), new Vector(0, 8), new Vector(0, 0) })
program.Codes.Add(new LinearMove(point));
}
else
{
// Native CCW quarter arcs, not a polygonized or self-crossing approximation.
program.Codes.Add(new LinearMove(new Vector(9, 0)));
program.Codes.Add(new ArcMove(new Vector(10, 1), new Vector(9, 1), RotationType.CCW));
program.Codes.Add(new LinearMove(new Vector(10, 7)));
program.Codes.Add(new ArcMove(new Vector(9, 8), new Vector(9, 7), RotationType.CCW));
program.Codes.Add(new LinearMove(new Vector(0, 8)));
program.Codes.Add(new LinearMove(new Vector(0, 0)));
}
return new Drawing(shape, program);
}
internal static object Invoke(object target, string method, params object[] args) =>
target.GetType().GetMethod(method, BindingFlags.NonPublic | BindingFlags.Instance)!
.Invoke(target, args)!;
internal static void AssertSameLayout(List<Part> expected, List<Part> actual)
{
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
{
Assert.Same(expected[i].BaseDrawing, actual[i].BaseDrawing);
Assert.Equal(expected[i].Location, actual[i].Location);
Assert.Equal(expected[i].Rotation, actual[i].Rotation);
Assert.Equal(Bounds(expected[i].BoundingBox), Bounds(actual[i].BoundingBox));
Assert.Equal(ProgramValues(expected[i].Program), ProgramValues(actual[i].Program));
}
}
internal static void AssertValidLayout(List<Part> parts, Box area)
{
Assert.NotEmpty(parts);
foreach (var part in parts)
{
Assert.True(part.BaseDrawing.Area > 0);
Assert.True(part.Left >= area.Left - 1e-6 && part.Right <= area.Right + 1e-6
&& part.Bottom >= area.Bottom - 1e-6 && part.Top <= area.Top + 1e-6);
Assert.All(new[] { part.Left, part.Right, part.Bottom, part.Top, part.Rotation },
value => Assert.True(double.IsFinite(value)));
}
Assert.False(FillHelpers.HasOverlappingParts(parts));
}
private static (double X, double Y, double Length, double Width) Bounds(Box box) =>
(box.X, box.Y, box.Length, box.Width);
private static object[] ProgramValues(Program program)
{
var values = new List<object> { program.Mode, program.Rotation, Bounds(program.BoundingBox()) };
foreach (var code in program.Codes)
{
values.Add(code.GetType());
if (code is Motion motion)
values.Add(motion.EndPoint);
if (code is LinearMove line)
values.Add(line.Layer);
if (code is ArcMove arc)
{
values.Add(arc.CenterPoint);
values.Add(arc.Rotation);
values.Add(arc.Layer);
}
}
return values.ToArray();
}
private static object[] Snapshot(Drawing drawing) => new object[] { drawing, drawing.Area, drawing.Program }
.Concat(drawing.Program.Codes.Cast<object>()).Concat(ProgramValues(drawing.Program)).ToArray();
private static Drawing MakeRightTriangle(double w, double h) private static Drawing MakeRightTriangle(double w, double h)
{ {
var pgm = new Program(); var pgm = new Program();
@@ -139,6 +139,101 @@ public class FillPerformanceTests
ReportGroupSummary("custom", customSamples, callsPerBatch); ReportGroupSummary("custom", customSamples, callsPerBatch);
} }
[SkippableFact]
public void Extents_ReportsRepeatedColumnRebuilds()
{
Skip.IfNot(Environment.GetEnvironmentVariable("OPENNEST_RUN_FILL_PERF") == "1",
"Set OPENNEST_RUN_FILL_PERF=1 to run opt-in fill microbenchmarks.");
var area = new Box(3, 5, 45, 27);
var drawing = FillExtentsTests.MakeFixture("triangle");
var spacings = new[] { 0.0, 0.5 };
var fills = spacings.Select(spacing =>
{
var filler = new FillExtents(area, spacing);
return new Func<List<Part>>(() => filler.Fill(drawing));
}).ToArray();
var expected = spacings.Select(spacing => new LegacyFillExtents(area, spacing).Fill(drawing)).ToArray();
for (var i = 0; i < fills.Length; i++)
{
Assert.Equal(24, expected[i].Count);
FillExtentsTests.AssertSameLayout(expected[i], fills[i]());
FillExtentsTests.AssertValidLayout(expected[i], area);
}
var warmupCalls = 50;
var callsPerBatch = 200;
var repetitions = 7;
#if DEBUG
output.WriteLine("Configuration=Debug (diagnostic only; use Release for measurements).");
#else
output.WriteLine("Configuration=Release.");
#endif
output.WriteLine($"Runtime={RuntimeInformation.FrameworkDescription}; OS={RuntimeInformation.OSDescription}; "
+ $"architecture={RuntimeInformation.ProcessArchitecture}; processors={Environment.ProcessorCount}; "
+ $"Stopwatch.Frequency={Stopwatch.Frequency} ticks/s.");
output.WriteLine("extents: synthetic closed right triangle (0,0)-(10,0)-(0,8)-(0,0); "
+ "area=(3,5,45,27); angle=0; spacing=0 or 0.5; 24 parts/fill. "
+ "The matching Debug work test proves 2 BuildColumn calls/fill (initial + adjustment). "
+ "Real synchronous production Fill, no reflection/reference inside timing. "
+ "Setup, assertions and output excluded; geometry, tiling, adjustment, overlap fallback, GC, "
+ "delegate/loop/count consumption included. Allocations=GC.GetAllocatedBytesForCurrentThread "
+ "around synchronous calls, not RSS. Warm source drawing/JIT, no forced GC or cache reset. "
+ "Not a timing gate, isolated BuildColumn latency, or whole-job benchmark.");
output.WriteLine($"extents: warmup=2 batches x {warmupCalls} calls per spacing; "
+ $"measured={repetitions} batches x {callsPerBatch} calls per spacing; "
+ "spacing order alternates in warmup and measurement.");
for (var batch = 0; batch < 2; batch++)
for (var slot = 0; slot < fills.Length; slot++)
MeasureExtents(fills[(slot + batch) % fills.Length], warmupCalls);
var samples = spacings.Select(_ => new ExtentsSample[repetitions]).ToArray();
for (var batch = 0; batch < repetitions; batch++)
{
for (var slot = 0; slot < fills.Length; slot++)
{
var mode = (slot + batch) % fills.Length;
samples[mode][batch] = MeasureExtents(fills[mode], callsPerBatch);
}
for (var mode = 0; mode < fills.Length; mode++)
{
var sample = samples[mode][batch];
Assert.Equal((long)callsPerBatch * expected[mode].Count, sample.PartCount);
FillExtentsTests.AssertSameLayout(expected[mode], sample.LastResult);
FillExtentsTests.AssertValidLayout(sample.LastResult, area);
output.WriteLine(FormattableString.Invariant(
$"extents spacing={spacings[mode]} batch={batch + 1}: ms={sample.Milliseconds:F6}; bytes={sample.AllocatedBytes}; parts={sample.PartCount}."));
}
}
for (var mode = 0; mode < fills.Length; mode++)
{
var times = samples[mode].Select(s => s.Milliseconds).OrderBy(t => t).ToArray();
var bytes = samples[mode].Select(s => s.AllocatedBytes).OrderBy(b => b).ToArray();
var median = repetitions / 2;
output.WriteLine(FormattableString.Invariant(
$"extents spacing={spacings[mode]}: batch ms min/median/max={times[0]:F6}/{times[median]:F6}/{times[^1]:F6}; us/call min/median/max={times[0] * 1000 / callsPerBatch:F3}/{times[median] * 1000 / callsPerBatch:F3}/{times[^1] * 1000 / callsPerBatch:F3}; batch bytes min/median/max={bytes[0]}/{bytes[median]}/{bytes[^1]}; B/call min/median/max={(double)bytes[0] / callsPerBatch:F3}/{(double)bytes[median] / callsPerBatch:F3}/{(double)bytes[^1] / callsPerBatch:F3}."));
}
}
private static ExtentsSample MeasureExtents(Func<List<Part>> fill, int calls)
{
var partCount = 0L;
var last = new List<Part>();
var allocatedBefore = GC.GetAllocatedBytesForCurrentThread();
var start = Stopwatch.GetTimestamp();
for (var i = 0; i < calls; i++)
{
last = fill();
partCount += last.Count;
}
var elapsed = Stopwatch.GetTimestamp() - start;
var allocated = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore;
return new ExtentsSample(elapsed * 1000.0 / Stopwatch.Frequency, allocated, partCount, last);
}
private readonly record struct ExtentsSample(double Milliseconds, long AllocatedBytes,
long PartCount, List<Part> LastResult);
private static GroupPatternSample MeasureGroupPattern(Func<List<Part>> fill, int calls) private static GroupPatternSample MeasureGroupPattern(Func<List<Part>> fill, int calls)
{ {
var partCount = 0L; var partCount = 0L;
+376
View File
@@ -0,0 +1,376 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.Fill
{
internal class LegacyFillExtents
{
private const int MaxIterations = 10;
private readonly Box workArea;
private readonly double partSpacing;
private readonly double halfSpacing;
public LegacyFillExtents(Box workArea, double partSpacing)
{
this.workArea = workArea;
this.partSpacing = partSpacing;
halfSpacing = partSpacing / 2;
}
public List<Part> Fill(
Drawing drawing,
double rotationAngle = 0,
CancellationToken token = default,
Action<List<Part>, string> reportProgress = null
)
{
var pair = BuildPair(drawing, rotationAngle);
if (pair == null)
return new List<Part>();
var column = BuildColumn(pair.Value);
if (column.Count == 0)
return new List<Part>();
reportProgress?.Invoke(column, $"Extents: initial column {column.Count} parts");
var adjusted = AdjustColumn(pair.Value, column, token);
// The iterative pair adjustment can shift parts enough to cause
// genuine overlap. Fall back to the unadjusted column when this happens.
if (HasOverlappingParts(adjusted))
{
Debug.WriteLine("[FillExtents] Adjusted column has overlaps, using unadjusted");
adjusted = column;
}
reportProgress?.Invoke(adjusted, $"Extents: column {adjusted.Count} parts");
var result = RepeatColumns(adjusted, token);
reportProgress?.Invoke(result, $"Extents: {result.Count} parts total");
return result;
}
// --- Step 1: Pair Construction ---
private PartPair? BuildPair(Drawing drawing, double rotationAngle)
{
var part1 = Part.CreateAtOrigin(drawing, rotationAngle);
var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI);
// Check that each part fits in the work area individually.
if (
part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon
|| part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon
)
return null;
// Slide part2 toward part1 from the right using geometry-aware distance.
var boundary1 = new PartBoundary(part1, halfSpacing);
var boundary2 = new PartBoundary(part2, halfSpacing);
// Position part2 to the right of part1 at bounding box width distance.
var startOffset = part1.BoundingBox.Length + part2.BoundingBox.Length + partSpacing;
part2.Offset(startOffset, 0);
part2.UpdateBounds();
// Slide part2 left toward part1.
var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left);
var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right);
var dist = SpatialQuery.DirectionalDistance(
movingLines,
stationaryLines,
PushDirection.Left
);
if (dist < double.MaxValue && dist > 0)
{
part2.Offset(-dist, 0);
part2.UpdateBounds();
}
var pair = AnchorToWorkArea(part1, part2);
if (pair == null)
return null;
// Verify pair fits in work area.
if (
pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon
|| pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon
)
return null;
return pair;
}
// --- Step 2: Build Column (tile vertically) ---
private List<Part> BuildColumn(PartPair pair)
{
var column = new List<Part> { (Part)pair.Part1.Clone(), (Part)pair.Part2.Clone() };
// Find geometry-aware copy distance for the pair vertically.
var boundary1 = new PartBoundary(pair.Part1, halfSpacing);
var boundary2 = new PartBoundary(pair.Part2, halfSpacing);
// Compute vertical copy distance using bounding boxes as starting point,
// then slide down to find true geometry distance.
var pairHeight = pair.Bbox.Width;
var testOffset = new Vector(0, pairHeight);
// Create test parts for slide distance measurement.
var testPart1 = pair.Part1.CloneAtOffset(testOffset);
var testPart2 = pair.Part2.CloneAtOffset(testOffset);
// Find minimum distance from test pair sliding down toward original pair.
var copyDistance = FindVerticalCopyDistance(
pair.Part1,
pair.Part2,
testPart1,
testPart2,
boundary1,
boundary2,
pairHeight
);
if (copyDistance <= 0)
return column;
var count = 1;
while (true)
{
var nextBottom = pair.Bbox.Bottom + copyDistance * count;
if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon)
break;
var offset = new Vector(0, copyDistance * count);
column.Add(pair.Part1.CloneAtOffset(offset));
column.Add(pair.Part2.CloneAtOffset(offset));
count++;
}
return column;
}
private double FindVerticalCopyDistance(
Part origPart1,
Part origPart2,
Part testPart1,
Part testPart2,
PartBoundary boundary1,
PartBoundary boundary2,
double pairHeight
)
{
// Check all 4 combinations: test parts sliding down toward original parts.
var slidePairs = new[]
{
(
moving: boundary1,
movingLoc: testPart1.Location,
stationary: boundary1,
stationaryLoc: origPart1.Location
),
(
moving: boundary1,
movingLoc: testPart1.Location,
stationary: boundary2,
stationaryLoc: origPart2.Location
),
(
moving: boundary2,
movingLoc: testPart2.Location,
stationary: boundary1,
stationaryLoc: origPart1.Location
),
(
moving: boundary2,
movingLoc: testPart2.Location,
stationary: boundary2,
stationaryLoc: origPart2.Location
),
};
var minSlide = double.MaxValue;
foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs)
{
var d = SlideDistance(
moving,
movingLoc,
stationary,
stationaryLoc,
PushDirection.Down
);
if (d < minSlide)
minSlide = d;
}
if (minSlide >= double.MaxValue || minSlide < 0)
return pairHeight + partSpacing;
// Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide,
// clamped so it never goes below pairHeight + partSpacing to prevent
// bounding-box overlap from spurious slide values.
var copyDist = pairHeight - minSlide;
return System.Math.Max(copyDist, pairHeight + partSpacing);
}
private static double SlideDistance(
PartBoundary movingBoundary,
Vector movingLocation,
PartBoundary stationaryBoundary,
Vector stationaryLocation,
PushDirection direction
)
{
var opposite = SpatialQuery.OppositeDirection(direction);
var movingEdges = movingBoundary.GetEdges(direction);
var stationaryEdges = stationaryBoundary.GetEdges(opposite);
return SpatialQuery.DirectionalDistance(
movingEdges,
movingLocation,
stationaryEdges,
stationaryLocation,
direction
);
}
// --- Step 3: Iterative Adjustment ---
private List<Part> AdjustColumn(PartPair pair, List<Part> column, CancellationToken token)
{
var originalPairWidth = pair.Bbox.Length;
for (var iteration = 0; iteration < MaxIterations; iteration++)
{
if (token.IsCancellationRequested)
break;
// Measure current gap.
var topEdge = double.MinValue;
foreach (var p in column)
if (p.BoundingBox.Top > topEdge)
topEdge = p.BoundingBox.Top;
var gap = workArea.Top - topEdge;
if (gap <= Tolerance.Epsilon)
break;
var pairCount = column.Count / 2;
if (pairCount <= 0)
break;
var adjustment = gap / pairCount;
if (adjustment <= Tolerance.Epsilon)
break;
// Try adjusting the pair and rebuilding the column.
var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth);
if (adjusted == null)
break;
var newColumn = BuildColumn(adjusted.Value);
if (newColumn.Count == 0)
break;
column = newColumn;
pair = adjusted.Value;
}
return column;
}
private PartPair? TryAdjustPair(PartPair pair, double adjustment, double originalPairWidth)
{
// Try shifting part2 up first.
var result = TryShiftDirection(pair, adjustment, originalPairWidth);
if (result != null)
return result;
// Up made the pair wider — try down instead.
return TryShiftDirection(pair, -adjustment, originalPairWidth);
}
private PartPair? TryShiftDirection(
PartPair pair,
double verticalShift,
double originalPairWidth
)
{
// Clone parts so we don't mutate the originals.
var p1 = (Part)pair.Part1.Clone();
var p2 = (Part)pair.Part2.Clone();
// Separate: shift part2 right so bounding boxes don't touch.
p2.Offset(partSpacing, 0);
p2.UpdateBounds();
// Apply the vertical shift.
p2.Offset(0, verticalShift);
p2.UpdateBounds();
// Compact part2 left toward part1.
var moving = new List<Part> { p2 };
var obstacles = new List<Part> { p1 };
Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left);
// Check if the pair got wider.
var newBbox = PairBbox(p1, p2);
if (newBbox.Length > originalPairWidth + Tolerance.Epsilon)
return null;
return AnchorToWorkArea(p1, p2);
}
// --- Step 4: Horizontal Repetition ---
private List<Part> RepeatColumns(List<Part> column, CancellationToken token)
{
if (column.Count == 0)
return column;
var pattern = new Pattern();
pattern.Parts.AddRange(column);
pattern.UpdateBounds();
var linear = new FillLinear(workArea, partSpacing);
return linear.Fill(pattern, NestDirection.Horizontal);
}
// --- Helpers ---
private PartPair? AnchorToWorkArea(Part part1, Part part2)
{
var bbox = PairBbox(part1, part2);
var anchor = new Vector(workArea.X - bbox.Left, workArea.Y - bbox.Bottom);
part1.Offset(anchor);
part2.Offset(anchor);
part1.UpdateBounds();
part2.UpdateBounds();
bbox = PairBbox(part1, part2);
return new PartPair(part1, part2, bbox);
}
private static Box PairBbox(Part part1, Part part2) =>
((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
private static bool HasOverlappingParts(List<Part> parts) =>
FillHelpers.HasOverlappingParts(parts);
private readonly record struct PartPair(Part Part1, Part Part2, Box Bbox);
}
}
+2
View File
@@ -45,6 +45,8 @@ The comparer microbenchmark uses deterministic, valid nonoverlapping rectangles,
The group-pattern measurement also compares default scoring with a custom comparer on a valid two-part group. It includes the real fill and scheduling work and omits allocation totals because fills can use worker threads. Run only that case with `--filter "FullyQualifiedName~GroupPattern_ReportsDefaultAndCustomComparer"`; helper behavior and Debug score-work checks use `--filter "FullyQualifiedName~FillHelpersTests"`. Workload details and measured limitations are in [the fill performance report](docs/performance/fill-performance.md). The group-pattern measurement also compares default scoring with a custom comparer on a valid two-part group. It includes the real fill and scheduling work and omits allocation totals because fills can use worker threads. Run only that case with `--filter "FullyQualifiedName~GroupPattern_ReportsDefaultAndCustomComparer"`; helper behavior and Debug score-work checks use `--filter "FullyQualifiedName~FillHelpersTests"`. Workload details and measured limitations are in [the fill performance report](docs/performance/fill-performance.md).
The extents measurement exercises repeated column rebuilding with a closed triangle at zero and positive spacing. It times the synchronous production fill and reports current-thread allocations; the frozen legacy implementation is used only for correctness checks outside timing. Run only that case with `--filter "FullyQualifiedName~Extents_ReportsRepeatedColumnRebuilds"`. Extents behavior, overlap-fallback, and Debug boundary-preparation checks use `--filter "FullyQualifiedName~FillExtentsTests|FullyQualifiedName~StrategyOverlapTests"`. Finite nonnegative spacing uses the equivalent bounding-box pitch without discarded vertical boundary preparation; negative/nonfinite spacing retains the legacy calculation rather than adding validation.
### Quick start ### Quick start
1. File > New Nest 1. File > New Nest
+133
View File
@@ -230,3 +230,136 @@ dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
``` ```
To reproduce before/after independently, use separate disposable worktrees at the baseline revision and the delivered revision, copy the identical measured harness/tests into both, and verify their hashes before rebuilding and running. Do not substitute a test-only approximation for the before production path or mix measurements from changed harnesses. To reproduce before/after independently, use separate disposable worktrees at the baseline revision and the delivered revision, copy the identical measured harness/tests into both, and verify their hashes before rebuilding and running. Do not substitute a test-only approximation for the before production path or mix measurements from changed harnesses.
## Task 2 — eliminate discarded extents pitch geometry — 2026-09-25
### Scope, proof, and compatibility
`FillExtents.BuildColumn` now uses `pair.Bbox.Width + partSpacing` directly for finite pair height and finite, nonnegative spacing. The source comment records the equivalence: the old helper returns that pitch for negative/no-hit slide distances; otherwise `minSlide >= 0`, so `Max(pairHeight - minSlide, pairHeight + partSpacing)` selects the same pitch. Boundary creation, two temporary clones, and vertical slide queries were discarded work in that domain.
The caller audit found that `NestJobValidator` rejects negative/nonfinite spacing, but the public constructor, `Plate.PartSpacing`, `PlateFillService`, and `ExtentsFillStrategy` do not establish that invariant for all interactive/public callers. The legacy calculation and its helpers therefore remain behind a negative/nonfinite-spacing or nonfinite-height guard. No validation or exception policy was added. Pair construction still prepares both boundaries. Tiling, adjustment, overlap fallbacks, progress, cancellation, bounds recomputation, and horizontal repetition are untouched. Task 3 and ML work are not included.
### Source and measurement provenance
- Resumed base: `cec7396da6d62386ef4817a6eb0e1b573ddd58de` on `master`, with unfinished Task 2 edits. The final implementation is delivered with this report; resolve its commit with `git log -1 --format=%H -- OpenNest.Engine/Fill/FillExtents.cs`.
- Original `FillExtents.cs` SHA-256: `e4110022f142ec4f954a00b8569dea5d451dbd5216756b0b6bf6a9595f2a83d0`.
- Optimized `FillExtents.cs` SHA-256: `efaacfca83a6d94bb7fdc65ba4da4be09c97c14c9c4e161491d48d64a69d1be3`.
- Identical final before/after harness `FillPerformanceTests.cs` SHA-256: `bb2adf31fa3348d1b84c239b01cdffa50fffc92193af3d1a4122461a93181cda`.
- Identical behavior/work tests `FillExtentsTests.cs` SHA-256: `b43508e30ab9683a123c4e00a0478e92cc1467fcf1afc5c6778f8fb2d548f02b`.
- Frozen test reference `LegacyFillExtents.cs` SHA-256: `b50b3b64014446d9688facc0b711b59cf126b64037fdf667603ce3d85859a324`. Reversing only the type/constructor rename and public-to-internal visibility change reproduced the baseline file byte-for-byte. The full reference is retained because the tests compare intermediate progress and private column-adjustment/fallback behavior as well as final layouts.
- Before tests/measurements temporarily restored the actual original production file, with restoration of the optimized file in `finally`. Source/project manifests matched across measurements except for `FillExtents.cs`; diagnostics and all tests were identical. No reference implementation or reflection is inside timing.
- Host: `hermes`, Ubuntu 24.04.5 LTS x64, Linux 6.8.0-142-generic, KVM guest with four vCPUs presented as AMD Ryzen 9 5900X. SDK 10.0.112. All measured tests emitted .NET 8.0.31, Release, and a 1,000,000,000 Hz stopwatch. Shared VM, no CPU pinning or host isolation.
- The resumed exploratory logs were not used for final numerical estimates: the full measurement pair was rerun after test formatting, then repeated in reverse process order. All final raw rows are preserved below; no best-run selection or mixing of harness versions.
### Behavior and deterministic work evidence
35 new Release behavior cases cover a 16-case shape/spacing/rotation matrix (rectangle, right triangle, concave L, native quarter-arc profile; spacing 0 and 0.5), nonzero work-area origins, no-fit parts/pairs, pre-cancellation, adjusted columns, overlap fallback at a 30-degree triangle angle, negative spacing, nonfinite no-fit calls, and isolated unsupported negative-spacing column calculations. Exact ordered comparisons include drawing reference identity, location, rotation, bounds, program geometry, and progress messages/layouts. Caller program/code identities and values, cached area, work-area values, and clone program-sharing/rotation ownership are checked. Main supported fixtures are finite, positive-area, in-bounds, and nonoverlapping; exact legacy equivalence preserves their spacing behavior rather than introducing a new spacing algorithm.
Two dedicated differential cases explicitly retain a pre-existing limitation: at zero spacing in a `(3,5,40,27)` area, rectangle/native-arc fills overrun the right edge by approximately `1e-5`. Those cases are not counted as valid-layout evidence and are not silently repaired by this optimization. The normal validity matrix and benchmark instead use `(3,5,45,27)`. Nonfinite spacing is tested only through bounded no-fit or isolated negative-infinity column paths, not unrestricted legacy tiling that could fail to terminate.
`PerfCounters.PartBoundaryPreparations` follows the existing Debug-only conditional increment pattern. Four new work assertions run in the existing nonparallel `FillCacheCollection`, resetting counters in `finally`:
| Case (each at spacing 0 and 0.5) | Required | Original production | Optimized production |
| --- | ---: | ---: | ---: |
| Four repeated `BuildColumn` calls | 0 preparations | 8 (genuine RED) | 0 |
| Full triangle fill, initial plus rebuilt column | 2 preparations | 6 (genuine RED) | 2 |
The two remaining full-fill preparations are the necessary `BuildPair` work, also asserted independently. Every full-fill work case returns the same 24-part layout. Six column-adjustment cases establish both accepted adjustment and fallback: four retain nonoverlapping adjusted columns, and two reject overlapping adjusted columns in favor of the unchanged initial column. The selected column and final layout remain in bounds. Release counter calls compile away; zero Release counters are not evidence of skipping work.
### Workload and results
The opt-in benchmark uses a closed right triangle `(0,0)-(10,0)-(0,8)-(0,0)`, work area `(3,5,45,27)`, rotation 0, and spacing 0 or 0.5. Both modes return 24 parts and rebuild their column once. Each spacing gets two 50-call warmups and seven 200-call measured batches per process. Spacing order alternates per batch. The first pair runs original then optimized; the repeat runs optimized then original. These are separate rebuilt test processes, not an interleaved in-process implementation comparison.
Timing includes real synchronous production fill, geometry preparation, tiling, adjustment, overlap checking/fallback, GC, delegates/loops, and count consumption. Setup, reference fills, assertions, and output are outside timing. Every batch consumes 4,800 parts and checks its last layout against the frozen reference. Source drawing data/JIT paths are warm; no forced collection, cache reset, file fixture, or network dependency is involved.
Microseconds per fill, minimum / median / maximum over seven batches:
| Process pair | Spacing | Before µs/fill | After µs/fill | Median change |
| --- | ---: | --- | --- | ---: |
| Forward | 0 | 2205.240 / 2281.382 / 2332.728 | 2158.270 / 2186.747 / 2207.422 | -4.15% |
| Forward | 0.5 | 2244.825 / 2310.086 / 2330.661 | 2155.409 / 2190.618 / 2215.091 | -5.17% |
| Reverse | 0 | 2165.221 / 2229.736 / 2403.664 | 2165.604 / 2210.005 / 2316.437 | -0.88% |
| Reverse | 0.5 | 2224.011 / 2302.593 / 2330.944 | 2187.911 / 2203.634 / 2266.720 | -4.30% |
The measured medians decrease in both process orders, but the ranges overlap (especially zero spacing) and the gain varies with run order. These shared-VM samples support a modest local improvement, not a stable percentage guarantee or whole-job speedup. No reproducible material regression was observed and there is no elapsed-time test gate.
Synchronous current-thread allocations are identical in every measured batch for a given implementation/spacing:
| Spacing | Before bytes/batch | After bytes/batch | Before bytes/fill | After bytes/fill | Saved bytes/fill | Reduction |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 0 | 174939200 | 167924800 | 874696 | 839624 | 35072 | 4.01% |
| 0.5 | 226099200 | 205708800 | 1130496 | 1028544 | 101952 | 9.02% |
These are `GC.GetAllocatedBytesForCurrentThread` managed-allocation differences around synchronous fills, not retained memory, RSS, or parallel-job totals. The deterministic result is removal of discarded boundary preparation; the allocation savings reproduce in both process orders.
### Tests and serial reviews
| Run | Passed | Skipped | Failed |
| --- | ---: | ---: | ---: |
| Original targeted Release | 43 | 0 | 0 |
| Original targeted Debug (intentional RED) | 43 | 0 | 4 |
| Optimized targeted Release | 43 | 0 | 0 |
| Optimized targeted Debug | 47 | 0 | 0 |
| Each of four final opt-in Release measurement runs | 1 | 0 | 0 |
| Extents opt-in gate unset | 0 | 1 | 0 |
| Extents opt-in gate `0` | 0 | 1 | 0 |
| Full main Release | 1206 | 15 | 0 |
| Full main Debug | 1219 | 15 | 0 |
| Full engine Release | 300 | 0 | 0 |
The targeted filter is `FillExtentsTests|StrategyOverlapTests` with `FullyQualifiedName~` on each term. Full-suite TRX outcomes were parsed rather than inferred from console totals: main Release has 1,221 total results, Debug 1,234, engine 300. All skips are the 12 optional CHR-font fixtures and three opt-in performance tests. The adapter confirms .NET 8.0.31 in all full runs. Rebuilding the engine emits the existing `CirclePacking/Item.cs` CS0108/CS0114 warnings; rebuilding the test project also emits its existing nullable/xUnit warnings. The frozen `LegacyFillExtents.cs:30` adds one benign CS8625 warning because the unchanged legacy `reportProgress = null` default is copied into the nullable-enabled test project. It is retained verbatim for reference fidelity, not presented as a pre-existing test warning. No new production warnings or unexpected test failures were found.
Per the user's no-subagents instruction, spec review and subsequent quality/integration review were performed serially by the same agent, not independently. Spec review checked the algebraic guard, caller audit, exact frozen reference, behavior/ownership/cancellation/overlap coverage, genuine red/green work assertions, and Task 2-only scope. Quality review checked the production/test diff, Debug counter isolation and Release compilation convention, absence of new secrets or unsafe I/O, runtime/harness provenance, and all final raw TRX rows against console output. No blocking findings remained after the test-only whitespace correction. Changed-file `dotnet format whitespace --verify-no-changes` and `git diff --check` passed.
README documents the extents benchmark and work-test commands. The previously denied `CLAUDE.md` workflow sync remains blocked pending explicit approval; it was not retried. No representative corpus, Windows UI runtime test, or actual ONNX inference was run. This slice does not complete Tasks 3–5 or authorize gated follow-ups.
### Raw measured batches
Milliseconds per 200 actual production fills. Each row contains both implementations from one process pair; every batch produced 4,800 parts. Allocation totals for every row are the constant spacing-specific values above.
| Process pair | Spacing | Batch | Before ms | After ms |
| --- | ---: | ---: | ---: | ---: |
| forward | 0 | 1 | 463.229998 | 439.473112 |
| forward | 0 | 2 | 466.545605 | 437.349468 |
| forward | 0 | 3 | 450.826218 | 441.484339 |
| forward | 0 | 4 | 456.276359 | 438.751402 |
| forward | 0 | 5 | 462.975196 | 434.451956 |
| forward | 0 | 6 | 445.596554 | 433.705799 |
| forward | 0 | 7 | 441.048033 | 431.653954 |
| forward | 0.5 | 1 | 465.387515 | 443.018255 |
| forward | 0.5 | 2 | 462.017170 | 441.358437 |
| forward | 0.5 | 3 | 466.132129 | 438.198245 |
| forward | 0.5 | 4 | 450.824107 | 431.081841 |
| forward | 0.5 | 5 | 462.570224 | 438.123662 |
| forward | 0.5 | 6 | 454.096721 | 433.895019 |
| forward | 0.5 | 7 | 448.964925 | 437.877448 |
| reverse | 0 | 1 | 480.732828 | 463.287364 |
| reverse | 0 | 2 | 445.947233 | 434.966923 |
| reverse | 0 | 3 | 450.506707 | 442.001094 |
| reverse | 0 | 4 | 433.764890 | 446.086663 |
| reverse | 0 | 5 | 437.597684 | 448.988001 |
| reverse | 0 | 6 | 433.044245 | 433.120823 |
| reverse | 0 | 7 | 448.325975 | 439.151450 |
| reverse | 0.5 | 1 | 460.518547 | 447.369861 |
| reverse | 0.5 | 2 | 461.616765 | 437.593637 |
| reverse | 0.5 | 3 | 466.188871 | 453.049694 |
| reverse | 0.5 | 4 | 463.178063 | 453.343928 |
| reverse | 0.5 | 5 | 453.526082 | 440.726846 |
| reverse | 0.5 | 6 | 444.802234 | 440.614301 |
| reverse | 0.5 | 7 | 448.043300 | 437.582121 |
### Reproduction and evidence retention
```bash
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~Extents_ReportsRepeatedColumnRebuilds' \
--logger 'console;verbosity=detailed'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~FillExtentsTests|FullyQualifiedName~StrategyOverlapTests'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
--filter 'FullyQualifiedName~FillExtentsTests|FullyQualifiedName~StrategyOverlapTests'
```
For an independent before/after comparison, keep the delivered tests/diagnostics identical in two disposable trees and use the baseline `FillExtents.cs` only in the before tree. Rebuild each configuration and verify the source/harness hashes. Do not change production spacing policy to reproduce invalid-domain cases or substitute the frozen test type for the measured production method.
Final logs, TRX files, source restoration snapshot, manifests, and parsed rows were held in `/home/aj/.hermes/cache/scratch/opennest-task2-resume-20260925/` through review. The report preserves all final timing rows, allocation totals, source hashes, runtime and test summaries; temporary evidence and the superseded resumed exploration directory are removed before commit. Next planned hardening is the separately reviewable redundant-bounds slice with threshold-fit and canonical-frame safety, not a geometry repair bundled into this change.