47 KiB
Fill performance measurements
Documentation follow-up — 2026-09-25: the user explicitly approved the previously blocked instruction update, then chose AGENTS.md as the shared source of truth for multiple coding agents. CLAUDE.md imports it with @AGENTS.md for compatibility. The shared instructions document the delivered comparer/group/extents workflow, Debug work counters, measurement limitations, and safeguards already reflected in README. The instruction-document sync blocker is resolved; references to it in the historical slice records below describe their delivery-time status. No production code or measured results changed.
Count-first comparer slice — 2026-09-25
This historical Task 1 section covers only the count-first DefaultFillComparer change. Geometry, custom-comparer score elimination, ML work, and whole-job optimization had not been implemented or measured in that slice. Task 1b is reported separately below.
Source and environment
Final staged review corrected two delegate declarations to the repository's var style. The complete original/optimized measurement pair was then rerun with that identical final harness; all timing tables below are from those final runs, not mixed with earlier samples. The original production comparer was temporarily restored only for the before measurement and reinstated in finally.
- Baseline:
1b862dc1a896df502935b900094c15c3300999c5onmaster. - Optimized source: the count-first implementation delivered with this report; use
git log -1 --format=%H -- OpenNest.Engine/Fill/DefaultFillComparer.csto resolve its implementation commit. - The original comparer was measured before adding the fast path. Tests and the Debug-only diagnostic were already present; the diagnostic call is compiled out in Release.
- Original comparer SHA-256:
8d436b88831d3187b319d52084131773f998c94ff4a227262cf15e4d27013b8f. - Optimized comparer SHA-256:
2444d30ddc92fa662f5ad50ac2cca0a7c50da3aee15f0fc932d58cf64023333d. - Identical before/after measurement harness SHA-256:
6a3b911748e31027bab6922d9d54f89b6fd7257884e1eecc5461db032089c7bf(OpenNest.Tests/Fill/FillPerformanceTests.cs). - Machine:
hermes, KVM guest, 4 vCPUs presented as AMD Ryzen 9 5900X; Ubuntu 24.04.5 LTS x64; Linux 6.8.0-142-generic. No CPU pinning. - Build SDK: 10.0.112. Measured runtime, emitted by the test: .NET 8.0.31; Release; stopwatch frequency 1,000,000,000 ticks/second.
Workload and timing scope
All inputs are fixture-independent, finite, nonoverlapping 2×1 rectangles inside a (0,0,256,256) work area, arranged in 64 columns. The larger layout has 2,048 parts at pitch 4; the smaller layout has 2,047 at pitch 3 (higher density despite lower count). The equal-count control compares 2,048 parts at pitch 3 against the larger layout.
Each implementation gets two warmup batches of 50,000 calls per case, then seven measured batches. Unequal-count batches contain 500,000 calls; equal-count batches contain 10,000. Argument order alternates each call, with half forward and half reverse. Actual/reference batch order alternates between repetitions. The reference is the prior scoring expression, FillScore.Compute(a, area) > FillScore.Compute(b, area), not a duplicate comparer class.
Inputs, cached drawing areas/bounds, and JIT paths are warm. There is no fill-cache operation or cache reset in this workload. Construction, layout validation, correctness assertions, and output are outside timing. Delegate/loop/result-consumption overhead and JIT optimization are included. Returned true counts are checked after each batch.
Allocated bytes are measured around synchronous work with GC.GetAllocatedBytesForCurrentThread, not process-wide memory or RSS. Every batch of both implementations, before and after, reported zero allocated managed bytes on the measured thread.
Results
Nanoseconds per call, minimum / median / maximum over seven batches:
| Case | Implementation | Before | After |
|---|---|---|---|
| unequal-counts | Comparer | 11901.864 / 11979.646 / 12194.228 | 1.800 / 1.878 / 3.221 |
| unequal-counts | Reference expression | 11654.151 / 12085.460 / 12269.212 | 12093.576 / 12247.735 / 12419.104 |
| equal-count-control | Comparer | 11617.457 / 12455.571 / 12814.497 | 11874.469 / 12372.208 / 12666.589 |
| equal-count-control | Reference expression | 11576.379 / 12344.544 / 12983.099 | 11736.897 / 12565.047 / 12864.353 |
The unequal-count median batch decreased from 5,989.823190 ms to 0.939182 ms. Their ratio is approximately 6,378× for this warm, repeated local-operation workload only. The optimized path is close to loop/JIT overhead; its nanosecond estimate is not an isolated method-latency guarantee. The unchanged reference varied slightly between processes, illustrating host/runtime variability. Do not extrapolate the ratio to nesting jobs.
Equal-count medians remained in the same range as the reference; these runs show no material regression, not evidence of an equal-count optimization. No elapsed-time thresholds are enforced in CI.
Behavioral and work-removal evidence
- Characterization covers null/empty guard order, lower-count rejection, count winning against density, both density directions for equal counts, and exact ties retaining the current result.
- A deterministic matrix of 33 valid layouts checks all 1,089 ordered pairs against the reference expression, including empty, self, translated, and reordered cases. It snapshots caller part/drawing/program identities, positions, rotations, bounds, and motion endpoints.
- A Debug-only
PerfCounters.FillScoreComputationsdiagnostic follows the existing conditional counter pattern. Concrete lists and nonvirtual getters do not offer a practical direct scan spy without broader production changes. - Before the fast path, both unequal-count work assertions failed with expected 0, actual 2 score computations; the other three work cases passed. Afterward, all five passed: unequal/empty cases compute no scores, while nonempty equal-count cases compute both scores. Counter tests run in the existing nonparallel
FillCacheCollectionand reset counters infinally. - Debug instrumentation does not establish Release behavior by reading zero counters; Release evidence is the compiled-out call convention, source fast path, equivalent outputs, and timings above.
Test results
| Run | Passed | Skipped | Failed |
|---|---|---|---|
| Original full main suite, Release | 1,140 | 12 | 0 |
| Original full engine suite, Release | 300 | 0 | 0 |
| Final full main suite, Release | 1,154 | 13 | 0 |
| Final full main suite, Debug | 1,159 | 13 | 0 |
| Final full engine suite, Release | 300 | 0 | 0 |
The 12 original skips are optional ChrFontTests: ChrFontPath not configured in test-config.json or file not found. The additional final skip is the opt-in performance test. The five extra Debug cases are the score-work tests. The performance test passed with the opt-in value 1, and was separately verified to skip when unset and when set to 0.
The implementer's broader comparer/score selection passed 28 Release tests before and after, and 33 Debug tests afterward (it also selected NestProgressTests.BestDensity_MatchesFillScoreFormula). Parent re-verification using the plan's narrower FillComparerTests|FillScoreTests filter passed 27 Release tests, and the five Debug work tests passed separately. Existing unrelated compiler warnings remain; no failures are hidden as baseline bugs. Source formatting and git diff --check passed.
Reproduction
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'Category=FillPerformance' --logger 'console;verbosity=detailed'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
--filter 'FullyQualifiedName~DefaultFillComparerWorkTests'
Normal suite runs skip the measurement. The installed Xunit.SkippableFact 1.4.13 names its inverse skip predicate Skip.IfNot, not Skip.Unless; no package change was needed. README documents both shell and PowerShell invocation.
The corresponding CLAUDE.md workflow edit was denied by the protected-file permission gate. That file was left unchanged; this documentation sync remains blocked pending explicit approval. No retry or alternate write was attempted.
No Windows UI runtime tests, actual ONNX inference, representative production corpus, or whole-job benchmark was run for this slice. These results do not complete the broader initial optimization batch.
Raw measured batches
Times in milliseconds; allocations were 0 bytes for every actual/reference batch below. Preserved from matching raw TRX and console outputs before removing temporary test artifacts.
| State | Case | Batch | Comparer ms | Reference ms |
|---|---|---|---|---|
| pre | unequal-counts | 1 | 6097.114079 | 6093.454749 |
| pre | unequal-counts | 2 | 5950.931964 | 6031.795978 |
| pre | unequal-counts | 3 | 6010.884370 | 5827.075456 |
| pre | unequal-counts | 4 | 5989.823190 | 6019.619401 |
| pre | unequal-counts | 5 | 5953.063588 | 6134.606110 |
| pre | unequal-counts | 6 | 6051.466272 | 6042.729937 |
| pre | unequal-counts | 7 | 5973.554301 | 6083.705108 |
| pre | equal-count-control | 1 | 126.868005 | 129.830987 |
| pre | equal-count-control | 2 | 124.555712 | 128.105601 |
| pre | equal-count-control | 3 | 125.615121 | 124.509854 |
| pre | equal-count-control | 4 | 128.144975 | 120.288678 |
| pre | equal-count-control | 5 | 121.787519 | 123.445436 |
| pre | equal-count-control | 6 | 116.562527 | 115.763791 |
| pre | equal-count-control | 7 | 116.174565 | 118.153899 |
| post | unequal-counts | 1 | 0.931488 | 6123.867689 |
| post | unequal-counts | 2 | 0.933221 | 6046.787840 |
| post | unequal-counts | 3 | 0.939182 | 6209.551894 |
| post | unequal-counts | 4 | 1.610389 | 6120.239321 |
| post | unequal-counts | 5 | 1.210914 | 6145.847134 |
| post | unequal-counts | 6 | 0.945585 | 6160.595776 |
| post | unequal-counts | 7 | 0.899788 | 6097.613482 |
| post | equal-count-control | 1 | 122.282550 | 121.856075 |
| post | equal-count-control | 2 | 124.576228 | 123.526717 |
| post | equal-count-control | 3 | 123.722076 | 125.650465 |
| post | equal-count-control | 4 | 126.342491 | 128.643534 |
| post | equal-count-control | 5 | 126.665891 | 125.683547 |
| post | equal-count-control | 6 | 118.822360 | 126.622891 |
| post | equal-count-control | 7 | 118.744694 | 117.368968 |
Task 1b — skip unused group-pattern scores — 2026-09-25
Scope and provenance
Only the two candidate additions in FillHelpers.FillPattern changed: horizontal and vertical results compute FillScore when comparer == null, otherwise store default. The ConcurrentBag, angle scheduling, fill calls, result iteration, strict default-score comparison, and custom-comparer calls are unchanged. No geometry, bounds, scoring formula, ML, or Tasks 2+ optimization is included.
- Before source:
masterHEADb318950a543d2a6c4fd0b305e5e3ae2bbee3c6d9, already containing Task 1.FillHelpers.cswas confirmed unchanged against HEAD before measurement. The actual unchanged production method was measured, not a reference approximation. - Before
OpenNest.Engine/Strategies/FillHelpers.csSHA-256:3f01f15b673d33626e49e06f4a381dc7331b33ec0b8850acf56dff4aa71e9198. - After
OpenNest.Engine/Strategies/FillHelpers.csSHA-256:a1c8a1bc155dbe62439f8345a3a54e06147a5404a6a46835a30b66d52cceccfa. - Identical before/after
OpenNest.Tests/Fill/FillPerformanceTests.csSHA-256:a5594148a2b11e37f5d47925c96774855eb622befb54b6736e98112ae8662873. This extends the historical Task 1 harness; it does not replace its measurements above. - Identical red/green
OpenNest.Tests/Strategies/FillHelpersTests.csSHA-256:35fee6661bd23091708041b0dc7e3b0517ad859bbf05e3ead95082fe7025fed9. - Tracked C# source/project manifests before and after differ only in
FillHelpers.cs; the new, untracked test file is covered separately by the hash above. There was no temporary production restoration or alternate implementation during these measurements. - Host
hermes: Ubuntu 24.04.5 LTS x64, Linux 6.8.0-142-generic, KVM guest, four vCPUs presented as AMD Ryzen 9 5900X. No CPU pinning or host isolation. SDK 10.0.112; test-emitted runtime .NET 8.0.31, Release, stopwatch frequency 1,000,000,000 ticks/second.
Workload and method
The opt-in GroupPattern_ReportsDefaultAndCustomComparer test uses two finite, nonoverlapping synthetic rectangles: 2×1 at (11,13) and 1×2 at (13.5,14.5). Work area is (3,5,5,9), part spacing 0.25, and the only candidate angle is 0 radians. Horizontal fill returns eight parts; vertical returns seven. The default path selects horizontal. A custom comparer deliberately prefers fewer parts, selecting vertical against FillScore.
Each mode gets two warmup batches of 2,000 calls and seven measured batches of 20,000 calls. Default/custom order alternates in both warmup and measured repetitions. Before and after run in separate test processes using the same harness and inputs, with the two-line production change between them. Both run the real Parallel.ForEach/fill/bag/selection path. One angle avoids cross-worker bag tie-order ambiguity; it does not represent multi-angle parallel scaling.
Setup, correctness/layout assertions, and output are outside timing. Fill geometry, candidate construction, scheduling, garbage collection, scoring/selection, delegate/loop overhead, and result-count consumption are included. Each batch consumes every result count (160,000 default or 140,000 custom parts); its last result is checked for exact drawing identity, part order, locations, and rotations against direct horizontal/vertical fills. Preflight and batch checks establish finite, positive-area, in-bounds, nonoverlapping layouts. Behavioral tests separately check input non-mutation. No cache reset or forced collection is inserted between batches; JIT paths and source drawing data are warm, but each call still constructs its candidates.
Allocation measurement is omitted for this parallel workload. The current-thread allocation probe in the older synchronous comparer benchmark above would not measure all fill workers and must not be interpreted as total group-fill allocations.
Results and limitations
Minimum / median / maximum over seven batches, calculated from raw TRX rows with Python statistics.median:
| Mode | Before batch ms | After batch ms | Before µs/call | After µs/call |
|---|---|---|---|---|
| Default scoring | 12349.821045 / 12588.985384 / 12798.229355 | 12373.077829 / 12613.732653 / 12696.177150 | 617.491 / 629.449 / 639.911 | 618.654 / 630.687 / 634.809 |
| Custom fewer-parts comparer | 12430.689835 / 12733.580886 / 12944.521123 | 12537.190933 / 12624.207441 / 12905.258236 | 621.534 / 636.679 / 647.226 | 626.860 / 631.210 / 645.263 |
The custom-comparer median decreased 0.86%, while the default control increased 0.20%. Before/after ranges overlap. These sequential process measurements on a shared VM do not establish a reliable fill-time speedup. The benchmark includes geometry and scheduling, not isolated score latency, and its small candidate lists limit the amount of removed work. The concrete result is removal of two unused score computations per successful single-angle custom-comparer call, established by Debug diagnostics below. A custom comparer may still compute any scores it requires.
This is a local group-fill microbenchmark, not a whole-job speedup estimate, production-corpus result, multi-angle throughput test, Windows UI runtime test, or ONNX inference test. There are no timing gates. The full main/engine-suite counts in the historical section above belong to Task 1; the independent Task 1b full-suite results are recorded below.
Behavior and real red/green evidence
FillHelpersTests adds 17 behavior cases in both configurations and four Debug-only work cases:
- Valid staggered rectangle groups exercise higher-count wins in both directions and equal-count density wins in both directions. The deliberately reversed-score comparer must select the opposite layout, even against count or density.
- Recording comparers assert exactly one call, horizontal candidate versus vertical current, the original work-area object, and the exact selected list reference. One angle keeps both writes on a single bag queue. Tie cases verify default selection retains the first bag result and custom comparison still executes, with both accept and reject outcomes.
- Layout comparisons preserve count, drawing reference identity, order, positions, rotations, and bounds. Input snapshots cover part/drawing/program/code identities, poses, bounds values, drawing area, and motion endpoints; a nonzero rotation case checks cloning without changing caller programs. Input angle lists and work areas are also checked where applicable.
- Empty angle lists, empty groups, and valid groups too large for the work area return null, make no comparer calls, and compute no scores. No invalid or null parts are used.
- Existing process-wide
PerfCounters.FillScoreComputationsis reused, with the existing nonparallelFillCacheCollectionand reset infinally. No new production diagnostic or testing seam was added.
Before changing production, all characterization passed. Three new work assertions genuinely failed:
| Debug case | Expected | Before actual | After actual |
|---|---|---|---|
| Non-scoring custom comparer, area 5×9 | 0 | 2 | 0 |
| Non-scoring custom comparer, area 10×4 | 0 | 2 | 0 |
| Custom comparer explicitly computes two scores | 2 | 4 | 2 |
| No comparer, default-score control | 2 | 2 | 2 |
The scoring-comparer control proves the helper does not bypass the supplied comparer or its score work. Release counter increments compile away; zero Release counters are not used as evidence of work removal.
| Run | Passed | Skipped | Failed |
|---|---|---|---|
| Before targeted Release | 47 | 0 | 0 |
| Before targeted Debug (intentional RED) | 48 | 0 | 3 |
| After targeted Release | 47 | 0 | 0 |
| After targeted Debug (GREEN) | 51 | 0 | 0 |
Existing DefaultFillComparerWorkTests, Debug, after |
5 | 0 | 0 |
| Group-pattern measurement, Release, before | 1 | 0 | 0 |
| Group-pattern measurement, Release, after | 1 | 0 | 0 |
| Group-pattern gate, environment unset | 0 | 1 | 0 |
Group-pattern gate, environment 0 |
0 | 1 | 0 |
| Parent full main suite, Release | 1,171 | 14 | 0 |
| Parent full main suite, Debug | 1,180 | 14 | 0 |
| Parent full engine suite, Release | 300 | 0 | 0 |
The targeted filter is FillHelpersTests|FillPipelineTests|FillComparerTests|FillScoreTests using FullyQualifiedName~ for each term. The five older Debug comparer-work cases have a different class-name suffix, so they were run separately. Existing compiler warnings remain outside the changed code; no warnings were emitted for the new test/harness or changed helper. git diff --check passed. The parent independently reconciled all 14 raw timing rows against TRX and console output, verified source snapshots/hashes and the sole production-source difference, and reran the full suites. The 14 skips are the 12 existing optional CHR-font tests plus both opt-in benchmarks. README now documents the group-pattern filter, parallel-allocation limitation, and helper checks. The previously denied CLAUDE.md workflow sync remains blocked pending explicit approval; no retry or alternate write was attempted.
Raw measured batches
Times in milliseconds for 20,000 actual production calls per mode per batch. Odd batches run default first; even batches run custom first.
| State | Batch | Default ms | Custom ms |
|---|---|---|---|
| before | 1 | 12798.229355 | 12733.580886 |
| before | 2 | 12446.484280 | 12838.273225 |
| before | 3 | 12349.821045 | 12543.308696 |
| before | 4 | 12638.532022 | 12944.521123 |
| before | 5 | 12588.985384 | 12547.909410 |
| before | 6 | 12475.834989 | 12430.689835 |
| before | 7 | 12603.474398 | 12883.844997 |
| after | 1 | 12613.732653 | 12795.834020 |
| after | 2 | 12659.052180 | 12624.207441 |
| after | 3 | 12696.177150 | 12537.190933 |
| after | 4 | 12476.409124 | 12611.963585 |
| after | 5 | 12683.978429 | 12905.258236 |
| after | 6 | 12373.077829 | 12553.051166 |
| after | 7 | 12462.758852 | 12731.211633 |
Evidence files and reproduction
Raw test/measurement logs, TRX, source snapshots/manifests, and exploratory artifacts were retained under /home/aj/.hermes/cache/scratch/opennest-fill-1b-20260925/ through independent spec and subsequent quality/integration review. Both reviews passed with no findings and reconciled the source hashes, genuine red/green failures, raw timings, and full-suite outcomes. The parent verified the same evidence and formatting checks. Temporary artifacts were then removed before commit; all measured batch rows, source hashes, environment details, and test summaries needed for this report are preserved above.
Run the current group-pattern benchmark alone (the category filter also runs the historical comparer benchmark):
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~GroupPattern_ReportsDefaultAndCustomComparer' \
--logger 'console;verbosity=detailed'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~FillHelpersTests|FullyQualifiedName~FillPipelineTests|FullyQualifiedName~FillComparerTests|FullyQualifiedName~FillScoreTests'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
--filter 'FullyQualifiedName~FillHelpersTests|FullyQualifiedName~FillPipelineTests|FullyQualifiedName~FillComparerTests|FullyQualifiedName~FillScoreTests'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
--filter 'FullyQualifiedName~DefaultFillComparerWorkTests'
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:
cec7396da6d62386ef4817a6eb0e1b573ddd58deonmaster, with unfinished Task 2 edits. The final implementation is delivered with this report; resolve its commit withgit log -1 --format=%H -- OpenNest.Engine/Fill/FillExtents.cs. - Original
FillExtents.csSHA-256:e4110022f142ec4f954a00b8569dea5d451dbd5216756b0b6bf6a9595f2a83d0. - Optimized
FillExtents.csSHA-256:efaacfca83a6d94bb7fdc65ba4da4be09c97c14c9c4e161491d48d64a69d1be3. - Identical final before/after harness
FillPerformanceTests.csSHA-256:bb2adf31fa3348d1b84c239b01cdffa50fffc92193af3d1a4122461a93181cda. - Identical behavior/work tests
FillExtentsTests.csSHA-256:b43508e30ab9683a123c4e00a0478e92cc1467fcf1afc5c6778f8fb2d548f02b. - Frozen test reference
LegacyFillExtents.csSHA-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 forFillExtents.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
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.
Task 3 — remove provably redundant bounds walks — 2026-09-25
Scope and per-site decisions
Part.Offset translates the cached BoundingBox arithmetically and Part.Clone copies it exactly, so several UpdateBounds() calls in the extents/pattern paths recompute a box that just changed by pure translation. Recomputation is mathematically equivalent but not bitwise identical to accumulated translates, and the extents pipeline compares boxes at Tolerance.Epsilon fit thresholds. Each candidate removal therefore had to keep every characterized layout, box, and threshold branch bitwise identical (the differential harness compares (X, Y, Length, Width) and location tuples with exact double equality, not tolerance — Vector.Equals itself is tolerance-based, so location/coordinate snapshots were switched to scalar tuples to expose ulp deltas).
| Site | Decision | Evidence |
|---|---|---|
BuildPair after Offset(startOffset, 0) |
Removed | All characterization green; slide uses locations, not cached bounds. |
BuildPair after Offset(-dist, 0) |
Removed | All characterization green. |
TryShiftDirection after Offset(partSpacing, 0) |
Removed | The retained post-vertical-shift recompute covers both offsets. |
TryShiftDirection after Offset(0, verticalShift) |
Retained | Rotated triangle spacing 0.5: box Y 5.6025660004937174 → 5.6025660004937183. Compactor.Push box thresholds consume this box. |
AnchorToWorkArea part1/part2 recompute |
Retained | Part2-only removal changed rectangle differential X 13.499999999999998 → 13.5 and arc pair height 8.0000000000000036 → 8; part1 removal changed X 3.1000000000000005 → 3.100000000000001. Part1-only removal failed 6 of 101 characterization cases; part2-only removal failed 25 of 101, including overlap fallback. The anchor workArea.X - bbox.Left round-trip lands a ulp away from X += anchor. |
BuildRotatedPattern after Clone() |
Retained | Accumulated-translation input at angle 0: box 1.3000000000000003 → 1.3. Source comments now record why each retained site stays. |
No change to Part.Offset, Part.Clone, Part.Rotate, Part.UpdateBounds, PairBbox, pattern.UpdateBounds(), the Task 2 pitch guard, or LegacyFillExtents.cs (frozen SHA-256 b50b3b64014446d9688facc0b711b59cf126b64037fdf667603ce3d85859a324 unchanged). PerfCounters.PartBoundsUpdates (Debug-only, incremented at the top of Part.UpdateBounds) was added per the plan.
Source and measurement provenance
- Base:
4ec92c95ec10c5c877e9fb0f34f197d7e23b87b0onmaster. Final implementation is delivered with this report; resolve withgit log -1 --format=%H -- OpenNest.Engine/Fill/FillExtents.cs. FillExtents.csSHA-256 beforeefaacfca83a6d94bb7fdc65ba4da4be09c97c14c9c4e161491d48d64a69d1be3(Task 2 delivered) → after13be137690ad5f60e0fe3874ec0cb0b93e9864b97d7eee7f3aa65c9b962053a4.FillHelpers.csbeforea1c8a1bc155dbe62439f8345a3a54e06147a5404a6a46835a30b66d52cceccfa→ after0a35075a573c7f17f9c8095a5a7bd56d0a4ea935ec1c9d8da2c4c9d78e0a9af0(comment-only; site retained).- Identical before/after harness
FillPerformanceTests.csSHA-25693ec06c5b59aecfe5cb9311380afea507cc134e77df7535036fb451ab0f63e29. - Before timings ran the actual pre-change production files (restored temporarily, restored back in
finally); after timings the delivered files. Same machine, serial Release:hermes, Ubuntu 24.04.5 LTS x64, KVM, four vCPUs presented as AMD Ryzen 9 5900X, SDK 10.0.112, .NET 8.0.31, 1 GHz stopwatch. Shared VM, no pinning.
Work-removal evidence (genuine red/green)
New Debug counter tests (serialized in FillCacheCollection, reset in finally) failed first on the unmodified production code: Fill_BoundsWork_PerformsFewerUpdates expected fewer than 13 part-bounds updates, actual 13 (both spacings); the group-construction work assertion expected 0/2 updates at angles 0/0.37, actual 2/4. After the change: full triangle fill 13 → 10 updates with 24 bitwise-identical parts (both spacings). The group assertions were rewritten to pin the retained counts (2 at angle 0, 4 at 0.37 = retained clone recompute + rotate recompute per part) because removing the clone recompute failed behavior characterization — disclosed partial delivery, not an optimized site. Pattern.UpdateBounds aggregates boxes and does not call Part.UpdateBounds, so it is outside this counter. Per fill the three removals save three UpdateBounds calls (three Box allocations — 144 bytes) reproduced exactly in every measured batch.
Characterization added (all passing pre-change, 133 targeted Release)
- 16 extents threshold cases: pair-fit, column-tiling, and column-count branches on both sides of
Tolerance.Epsilon, nonzero work-area origin, progress layouts, input snapshots. - 16 adjacent-double pair/column cases (
BitIncrement/BitDecrementneighborhoods with asserted both-side decisions) and 16 adjacent-doubleTryShiftDirectionwidth-acceptance cases (binary-searched rejected/accepted doubles located using only the frozen pre-change path, ±shifts). - 12
BuildRotatedPatterncases: translated, accumulated-translation, pre-rotated, canonical-frame, native-arc, and nonzero-program-origin groups at angles 0 and 0.37, checking exact poses/bounds against a local pre-change reference, input non-mutation, clone program ownership, and no double rotation of pre-rotated inputs. - Frozen-legacy differentials, threshold branch equality, and per-site removal trials (each candidate removed alone, characterization run, restored) reconcile the per-site table above; the trial logs were retained through review in the scratch directory and removed afterwards, with the resulting ulp deltas and failure counts recorded in this report.
Tests (parent independently reran the full suites on the final tree)
| Suite | Passed | Skipped | Failed |
|---|---|---|---|
| Targeted Release (`FillExtentsTests | FillHelpersTests | CanonicalFrameTests | StripeFillerTests`) |
| AGENTS.md Debug work filter | 137 | 0 | 0 |
| Full main Release | 1266 | 16 | 0 |
| Full main Debug | 1283 | 16 | 0 |
| Full engine Release | 300 | 0 | 0 |
Skips are the 12 optional CHR-font fixtures plus four opt-in benchmarks (the new rotated-pattern case adds one). Gate verified: unset and 0 skip all category tests. .NET 8.0.31 adapters confirmed. Changed-file dotnet format whitespace --verify-no-changes and git diff --check passed; no new production warnings.
Reviews
Implementation, spec review, and quality/integration review ran as three independent agent sessions (the earlier same-agent serial constraint was lifted by the user for this session). Spec review initially found one Important issue — the canonical fixture consumed a stale Source.Angle and duplicated the pre-rotated scenario — plus one Minor report-wording error; both were fixed (fixture now refreshes the canonical angle and asserts a nonzero source-to-canonical transformation; the failure counts are stated as 6/101 and 25/101), and the strengthened fixture additionally surfaced a benign test-assertion normalization issue at a baked 2π rotation, fixed by normalizing both sides. Subsequent independent quality/integration review returned PASS with no Critical, Important, or Minor findings: production diff traced through slide/Compactor consumers, counter isolation, adjacent-double oracle independence, hash/median/allocation reconciliation against raw logs, integration sweep across fill-strategy and jobs callers, and disclosure honesty were all verified by that reviewer. Pre-existing DXF-fixture-dependent tests still return early without their Windows fixture; no Windows UI runtime test or ONNX inference was run.
Results
All before/after timing ranges overlap: elapsed-time improvement is inconclusive, consistent with three saved Program.BoundingBox() walks being small against full fills. Allocation savings are exact and reproduce in every batch: extents spacing 0 839,624 → 839,480 bytes/fill and spacing 0.5 1,028,544 → 1,028,400 bytes/fill (−144 B/fill). Rotated-pattern construction is an unchanged-code control: identical allocations per call at both angles.
Extents: milliseconds per 200 production fills, seven batches each, alternating spacing order, warmup excluded. Rotated-pattern: milliseconds per 5,000 constructions of a 32-part native-arc group.
| Workload | Batch | Before ms | After ms |
|---|---|---|---|
| extents spacing 0 | 1 | 461.646777 | 502.378600 |
| extents spacing 0 | 2 | 439.442203 | 446.609639 |
| extents spacing 0 | 3 | 450.272109 | 457.012036 |
| extents spacing 0 | 4 | 433.095759 | 454.402604 |
| extents spacing 0 | 5 | 443.489319 | 447.382522 |
| extents spacing 0 | 6 | 433.504166 | 448.477156 |
| extents spacing 0 | 7 | 441.588374 | 439.879594 |
| extents spacing 0.5 | 1 | 460.691045 | 439.973796 |
| extents spacing 0.5 | 2 | 457.895526 | 443.225746 |
| extents spacing 0.5 | 3 | 444.315207 | 451.898487 |
| extents spacing 0.5 | 4 | 436.733945 | 447.851680 |
| extents spacing 0.5 | 5 | 451.266260 | 431.428815 |
| extents spacing 0.5 | 6 | 438.338321 | 455.657180 |
| extents spacing 0.5 | 7 | 448.995034 | 438.982789 |
| rotated-pattern angle 0 | 1 | 92.121779 | 95.085965 |
| rotated-pattern angle 0 | 2 | 85.115166 | 83.764479 |
| rotated-pattern angle 0 | 3 | 84.692335 | 82.859009 |
| rotated-pattern angle 0 | 4 | 90.233547 | 85.003329 |
| rotated-pattern angle 0 | 5 | 85.081651 | 83.281808 |
| rotated-pattern angle 0 | 6 | 78.809908 | 77.355562 |
| rotated-pattern angle 0 | 7 | 78.280977 | 80.372130 |
| rotated-pattern angle 0.37 | 1 | 184.337007 | 186.875744 |
| rotated-pattern angle 0.37 | 2 | 170.493990 | 155.509393 |
| rotated-pattern angle 0.37 | 3 | 153.992846 | 155.681698 |
| rotated-pattern angle 0.37 | 4 | 155.207434 | 156.663232 |
| rotated-pattern angle 0.37 | 5 | 158.498327 | 149.814054 |
| rotated-pattern angle 0.37 | 6 | 157.148604 | 150.090405 |
| rotated-pattern angle 0.37 | 7 | 150.252399 | 150.547539 |
Medians: extents spacing 0 2207.942 → 2242.386 µs/call, spacing 0.5 2244.975 → 2216.129 µs/call; rotated-pattern angle 0 17.016 → 16.656 µs/call, angle 0.37 31.430 → 31.102 µs/call. Every batch produced the expected part counts (extents 4,800; rotated-pattern 160,000) and layout assertions ran on the timed path's output outside timing. Current-thread synchronous allocations only; no whole-job or RSS claims.
Reproduction and limitations
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'Category=FillPerformance' --logger 'console;verbosity=detailed'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
--filter 'FullyQualifiedName~FillExtentsTests|FullyQualifiedName~FillHelpersTests|FullyQualifiedName~CanonicalFrameTests|FullyQualifiedName~StripeFillerTests'
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Debug \
--filter 'FullyQualifiedName~DefaultFillComparerWorkTests|FullyQualifiedName~FillHelpersTests|FullyQualifiedName~FillExtentsTests|FullyQualifiedName~StrategyOverlapTests'
This is a partial delivery by design: three of six candidate recomputations were removable; the three retained sites are documented with their observed ulp deltas, and their UpdateBounds calls must not be removed without re-running the threshold and overlap-fallback characterization. Timing is inconclusive; only allocation and work-counter removal are demonstrated. No representative production corpus, Windows UI runtime test, or ONNX inference was run. Task 4 and gated follow-ups are not included.