perf(fill): avoid redundant bounds recomputation
This commit is contained in:
@@ -64,6 +64,160 @@ public class FillExtentsTests
|
||||
Assert.Equal(before, Snapshot(drawing));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DifferentialCases))]
|
||||
public void Fill_Thresholds_MatchPreChangePairFitAndColumnBranches(string shape, double spacing, bool rotated)
|
||||
{
|
||||
var drawing = MakeFixture(shape);
|
||||
var angle = rotated ? System.Math.PI / 6 : 0;
|
||||
var before = Snapshot(drawing);
|
||||
var roomy = new Box(3.1, 5.3, 100, 100);
|
||||
var seed = Invoke(new LegacyFillExtents(roomy, spacing), "BuildPair", drawing, angle);
|
||||
var bbox = PairBounds(seed);
|
||||
// The reference BuildPair is identical to pre-Task-3 production. Task 2's
|
||||
// finite/nonnegative pitch shortcut gives the same column, not different poses.
|
||||
foreach (var axis in new[] { "length", "width", "column" })
|
||||
{
|
||||
foreach (var fits in new[] { false, true })
|
||||
{
|
||||
var delta = fits ? 1e-9 : -1e-9;
|
||||
var length = axis == "length" ? bbox.Length - OpenNest.Math.Tolerance.Epsilon + delta : 100;
|
||||
var width = axis == "width" ? bbox.Width - OpenNest.Math.Tolerance.Epsilon + delta
|
||||
: axis == "column" ? 2 * bbox.Width + spacing - OpenNest.Math.Tolerance.Epsilon + delta : 100;
|
||||
var area = new Box(roomy.X, roomy.Y, length, width);
|
||||
var areaBefore = Bounds(area);
|
||||
var legacy = new LegacyFillExtents(area, spacing);
|
||||
var filler = new FillExtents(area, spacing);
|
||||
var expectedPair = Invoke(legacy, "BuildPair", drawing, angle);
|
||||
var actualPair = Invoke(filler, "BuildPair", drawing, angle);
|
||||
Assert.Equal(axis == "column" || fits, expectedPair != null);
|
||||
Assert.Equal(expectedPair == null, actualPair == null);
|
||||
if (expectedPair != null)
|
||||
{
|
||||
AssertPairEqual(expectedPair, actualPair!);
|
||||
var expectedColumn = (List<Part>)Invoke(legacy, "BuildColumn", expectedPair);
|
||||
var actualColumn = (List<Part>)Invoke(filler, "BuildColumn", actualPair!);
|
||||
if (axis == "column")
|
||||
Assert.Equal(fits ? 4 : 2, expectedColumn.Count);
|
||||
AssertSameLayout(expectedColumn, actualColumn);
|
||||
}
|
||||
var expectedProgress = new List<List<Part>>();
|
||||
var actualProgress = new List<List<Part>>();
|
||||
var expected = legacy.Fill(drawing, angle, reportProgress: (parts, _) => expectedProgress.Add(parts));
|
||||
var actual = filler.Fill(drawing, angle, reportProgress: (parts, _) => actualProgress.Add(parts));
|
||||
AssertSameLayout(expected, actual);
|
||||
Assert.Equal(expectedProgress.Count, actualProgress.Count);
|
||||
for (var i = 0; i < expectedProgress.Count; i++)
|
||||
AssertSameLayout(expectedProgress[i], actualProgress[i]);
|
||||
Assert.Equal(areaBefore, Bounds(area));
|
||||
Assert.Equal(before, Snapshot(drawing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DifferentialCases))]
|
||||
public void Fill_AdjacentDoubleThresholds_PreservePairAndColumnDecisions(string shape, double spacing, bool rotated)
|
||||
{
|
||||
var drawing = MakeFixture(shape);
|
||||
var angle = rotated ? System.Math.PI / 6 : 0;
|
||||
var roomy = new Box(3.1, 5.3, 100, 100);
|
||||
var seed = Invoke(new LegacyFillExtents(roomy, spacing), "BuildPair", drawing, angle);
|
||||
var bbox = PairBounds(seed);
|
||||
foreach (var axis in new[] { "length", "width", "column" })
|
||||
{
|
||||
var threshold = (axis == "length" ? bbox.Length : axis == "width" ? bbox.Width
|
||||
: 2 * bbox.Width + spacing) - OpenNest.Math.Tolerance.Epsilon;
|
||||
var decisions = new HashSet<bool>();
|
||||
foreach (var size in AdjacentDoubles(threshold))
|
||||
{
|
||||
var area = new Box(roomy.X, roomy.Y, axis == "length" ? size : 100,
|
||||
axis == "length" ? 100 : size);
|
||||
var legacy = new LegacyFillExtents(area, spacing);
|
||||
var filler = new FillExtents(area, spacing);
|
||||
var expectedPair = Invoke(legacy, "BuildPair", drawing, angle);
|
||||
var actualPair = Invoke(filler, "BuildPair", drawing, angle);
|
||||
Assert.Equal(expectedPair == null, actualPair == null);
|
||||
if (axis != "column")
|
||||
decisions.Add(expectedPair != null);
|
||||
if (expectedPair == null)
|
||||
continue;
|
||||
AssertPairEqual(expectedPair, actualPair!);
|
||||
var expected = (List<Part>)Invoke(legacy, "BuildColumn", expectedPair);
|
||||
var actual = (List<Part>)Invoke(filler, "BuildColumn", actualPair!);
|
||||
AssertSameLayout(expected, actual);
|
||||
if (axis == "column")
|
||||
decisions.Add(expected.Count == 4);
|
||||
}
|
||||
Assert.Contains(false, decisions);
|
||||
Assert.Contains(true, decisions);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DifferentialCases))]
|
||||
public void TryShiftDirection_AdjacentWidthThresholds_PreserveAcceptance(string shape, double spacing, bool rotated)
|
||||
{
|
||||
var drawing = MakeFixture(shape);
|
||||
var angle = rotated ? System.Math.PI / 6 : 0;
|
||||
var area = new Box(3.1, 5.3, 100, 100);
|
||||
var legacy = new LegacyFillExtents(area, spacing);
|
||||
var filler = new FillExtents(area, spacing);
|
||||
var expectedPair = Invoke(legacy, "BuildPair", drawing, angle);
|
||||
var actualPair = Invoke(filler, "BuildPair", drawing, angle);
|
||||
foreach (var shift in new[] { -0.3, 0.3 })
|
||||
{
|
||||
// Locate adjacent rejected/accepted doubles using only the frozen pre-change
|
||||
// path; do not derive the oracle from the implementation under test.
|
||||
var rejected = 0.0;
|
||||
var accepted = 100.0;
|
||||
for (var i = 0; i < 64; i++)
|
||||
{
|
||||
var middle = (rejected + accepted) / 2;
|
||||
if (Invoke(legacy, "TryShiftDirection", expectedPair, shift, middle) == null)
|
||||
rejected = middle;
|
||||
else
|
||||
accepted = middle;
|
||||
}
|
||||
Assert.Equal(accepted, System.Math.BitIncrement(rejected));
|
||||
foreach (var width in new[] { rejected, accepted })
|
||||
{
|
||||
var expected = Invoke(legacy, "TryShiftDirection", expectedPair, shift, width);
|
||||
var actual = Invoke(filler, "TryShiftDirection", actualPair, shift, width);
|
||||
Assert.Equal(width == accepted, expected != null);
|
||||
Assert.Equal(expected == null, actual == null);
|
||||
if (expected != null)
|
||||
AssertPairEqual(expected, actual!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<double> AdjacentDoubles(double value)
|
||||
{
|
||||
yield return value;
|
||||
var lower = value;
|
||||
var upper = value;
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
lower = System.Math.BitDecrement(lower);
|
||||
upper = System.Math.BitIncrement(upper);
|
||||
yield return lower;
|
||||
yield return upper;
|
||||
}
|
||||
}
|
||||
|
||||
private static Box PairBounds(object pair) => (Box)pair.GetType().GetProperty("Bbox")!.GetValue(pair)!;
|
||||
|
||||
private static void AssertPairEqual(object expected, object actual)
|
||||
{
|
||||
Assert.Equal(Bounds(PairBounds(expected)), Bounds(PairBounds(actual)));
|
||||
var expectedParts = new[] { "Part1", "Part2" }.Select(name =>
|
||||
(Part)expected.GetType().GetProperty(name)!.GetValue(expected)!).ToList();
|
||||
var actualParts = new[] { "Part1", "Part2" }.Select(name =>
|
||||
(Part)actual.GetType().GetProperty(name)!.GetValue(actual)!).ToList();
|
||||
AssertSameLayout(expectedParts, actualParts);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, 5)]
|
||||
[InlineData(15, 10)] // One rectangle fits, the pair does not.
|
||||
@@ -185,6 +339,35 @@ public class FillExtentsTests
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[Theory]
|
||||
[InlineData(0.0)]
|
||||
[InlineData(0.5)]
|
||||
public void Fill_BoundsWork_PerformsFewerUpdates(double spacing)
|
||||
{
|
||||
var area = new Box(3, 5, 45, 27);
|
||||
var drawing = MakeFixture("triangle");
|
||||
PerfCounters.Reset();
|
||||
try
|
||||
{
|
||||
// Task 2 removed boundary preparation, not Part.UpdateBounds calls, so
|
||||
// this frozen baseline has the pre-Task-3 full-fill bounds-update count.
|
||||
var expected = new LegacyFillExtents(area, spacing).Fill(drawing);
|
||||
var before = PerfCounters.PartBoundsUpdates;
|
||||
PerfCounters.Reset();
|
||||
var actual = new FillExtents(area, spacing).Fill(drawing);
|
||||
var after = PerfCounters.PartBoundsUpdates;
|
||||
output.WriteLine($"bounds spacing={spacing}: before={before}, actual={after}, parts={actual.Count}");
|
||||
AssertSameLayout(expected, actual);
|
||||
Assert.Equal(13, before);
|
||||
Assert.True(after < before, $"Expected fewer than {before} part bounds updates, actual {after}.");
|
||||
Assert.Equal(10, after);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PerfCounters.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.0)]
|
||||
[InlineData(0.5)]
|
||||
@@ -284,7 +467,8 @@ public class FillExtentsTests
|
||||
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].Location.X, expected[i].Location.Y),
|
||||
(actual[i].Location.X, actual[i].Location.Y));
|
||||
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));
|
||||
@@ -315,12 +499,12 @@ public class FillExtentsTests
|
||||
{
|
||||
values.Add(code.GetType());
|
||||
if (code is Motion motion)
|
||||
values.Add(motion.EndPoint);
|
||||
values.Add((motion.EndPoint.X, motion.EndPoint.Y));
|
||||
if (code is LinearMove line)
|
||||
values.Add(line.Layer);
|
||||
if (code is ArcMove arc)
|
||||
{
|
||||
values.Add(arc.CenterPoint);
|
||||
values.Add((arc.CenterPoint.X, arc.CenterPoint.Y));
|
||||
values.Add(arc.Rotation);
|
||||
values.Add(arc.Layer);
|
||||
}
|
||||
|
||||
@@ -139,6 +139,66 @@ public class FillPerformanceTests
|
||||
ReportGroupSummary("custom", customSamples, callsPerBatch);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void RotatedPattern_ReportsBoundsConstruction()
|
||||
{
|
||||
Skip.IfNot(Environment.GetEnvironmentVariable("OPENNEST_RUN_FILL_PERF") == "1",
|
||||
"Set OPENNEST_RUN_FILL_PERF=1 to run opt-in fill microbenchmarks.");
|
||||
var drawing = FillExtentsTests.MakeFixture("arc");
|
||||
var group = Enumerable.Range(0, 32).Select(i =>
|
||||
new Part(drawing, new Vector(11.25 + i % 8 * 12, 13.5 + i / 8 * 10))).ToList();
|
||||
var angles = new[] { 0.0, 0.37 };
|
||||
var builds = angles.Select(angle => new Func<List<Part>>(() =>
|
||||
FillHelpers.BuildRotatedPattern(group, angle).Parts)).ToArray();
|
||||
var expected = angles.Select(angle =>
|
||||
OpenNest.Tests.Strategies.FillHelpersTests.PreChangeRotatedPattern(group, angle).Parts).ToArray();
|
||||
var warmupCalls = 1_000;
|
||||
var callsPerBatch = 5_000;
|
||||
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($"rotated-pattern: 32 native-arc parts on an 8-column 12x10 grid from (11.25,13.5); "
|
||||
+ $"angle=0 or 0.37; warmup=2 x {warmupCalls}; measured={repetitions} x {callsPerBatch}; "
|
||||
+ "angle batch order alternates. Real synchronous production construction only; setup, reference, "
|
||||
+ "assertions/output excluded; clone/rotation/aggregate bounds, GC and count consumption included. "
|
||||
+ "Warm drawing/JIT; no forced GC/cache reset. Current-thread allocations, not RSS or a whole-job benchmark.");
|
||||
for (var mode = 0; mode < builds.Length; mode++)
|
||||
FillExtentsTests.AssertSameLayout(expected[mode], builds[mode]());
|
||||
for (var batch = 0; batch < 2; batch++)
|
||||
for (var slot = 0; slot < builds.Length; slot++)
|
||||
MeasureExtents(builds[(slot + batch) % builds.Length], warmupCalls);
|
||||
var samples = angles.Select(_ => new ExtentsSample[repetitions]).ToArray();
|
||||
for (var batch = 0; batch < repetitions; batch++)
|
||||
{
|
||||
for (var slot = 0; slot < builds.Length; slot++)
|
||||
{
|
||||
var mode = (slot + batch) % builds.Length;
|
||||
samples[mode][batch] = MeasureExtents(builds[mode], callsPerBatch);
|
||||
}
|
||||
for (var mode = 0; mode < builds.Length; mode++)
|
||||
{
|
||||
var sample = samples[mode][batch];
|
||||
Assert.Equal((long)callsPerBatch * group.Count, sample.PartCount);
|
||||
FillExtentsTests.AssertSameLayout(expected[mode], sample.LastResult);
|
||||
output.WriteLine(FormattableString.Invariant(
|
||||
$"rotated-pattern angle={angles[mode]} batch={batch + 1}: ms={sample.Milliseconds:F6}; bytes={sample.AllocatedBytes}; parts={sample.PartCount}."));
|
||||
}
|
||||
}
|
||||
for (var mode = 0; mode < builds.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();
|
||||
output.WriteLine(FormattableString.Invariant(
|
||||
$"rotated-pattern angle={angles[mode]}: batch ms min/median/max={times[0]:F6}/{times[repetitions / 2]:F6}/{times[^1]:F6}; us/call min/median/max={times[0] * 1000 / callsPerBatch:F3}/{times[repetitions / 2] * 1000 / callsPerBatch:F3}/{times[^1] * 1000 / callsPerBatch:F3}; batch bytes min/median/max={bytes[0]}/{bytes[repetitions / 2]}/{bytes[^1]}; B/call={(double)bytes[repetitions / 2] / callsPerBatch:F3}."));
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void Extents_ReportsRepeatedColumnRebuilds()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user