713 lines
75 KiB
Markdown
713 lines
75 KiB
Markdown
# 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: `1b862dc1a896df502935b900094c15c3300999c5` on `master`.
|
||
- Optimized source: the count-first implementation delivered with this report; use `git log -1 --format=%H -- OpenNest.Engine/Fill/DefaultFillComparer.cs` to 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.FillScoreComputations` diagnostic 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 `FillCacheCollection` and reset counters in `finally`.
|
||
- 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
|
||
|
||
```bash
|
||
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: `master` HEAD `b318950a543d2a6c4fd0b305e5e3ae2bbee3c6d9`, already containing Task 1. `FillHelpers.cs` was confirmed unchanged against HEAD before measurement. The actual unchanged production method was measured, not a reference approximation.
|
||
- Before `OpenNest.Engine/Strategies/FillHelpers.cs` SHA-256: `3f01f15b673d33626e49e06f4a381dc7331b33ec0b8850acf56dff4aa71e9198`.
|
||
- After `OpenNest.Engine/Strategies/FillHelpers.cs` SHA-256: `a1c8a1bc155dbe62439f8345a3a54e06147a5404a6a46835a30b66d52cceccfa`.
|
||
- Identical before/after `OpenNest.Tests/Fill/FillPerformanceTests.cs` SHA-256: `a5594148a2b11e37f5d47925c96774855eb622befb54b6736e98112ae8662873`. This extends the historical Task 1 harness; it does not replace its measurements above.
|
||
- Identical red/green `OpenNest.Tests/Strategies/FillHelpersTests.cs` SHA-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.FillScoreComputations` is reused, with the existing nonparallel `FillCacheCollection` and reset in `finally`. 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):
|
||
|
||
```bash
|
||
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: `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.
|
||
|
||
## 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: `4ec92c95ec10c5c877e9fb0f34f197d7e23b87b0` on `master`. Final implementation is delivered with this report; resolve with `git log -1 --format=%H -- OpenNest.Engine/Fill/FillExtents.cs`.
|
||
- `FillExtents.cs` SHA-256 before `efaacfca83a6d94bb7fdc65ba4da4be09c97c14c9c4e161491d48d64a69d1be3` (Task 2 delivered) → after `13be137690ad5f60e0fe3874ec0cb0b93e9864b97d7eee7f3aa65c9b962053a4`.
|
||
- `FillHelpers.cs` before `a1c8a1bc155dbe62439f8345a3a54e06147a5404a6a46835a30b66d52cceccfa` → after `0a35075a573c7f17f9c8095a5a7bd56d0a4ea935ec1c9d8da2c4c9d78e0a9af0` (comment-only; site retained).
|
||
- Identical before/after harness `FillPerformanceTests.cs` SHA-256 `93ec06c5b59aecfe5cb9311380afea507cc134e77df7535036fb451ab0f63e29`.
|
||
- 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`/`BitDecrement` neighborhoods with asserted both-side decisions) and 16 adjacent-double `TryShiftDirection` width-acceptance cases (binary-searched rejected/accepted doubles located using only the frozen pre-change path, ±shifts).
|
||
- 12 `BuildRotatedPattern` cases: 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`) | 133 | 0 | 0 |
|
||
| 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
|
||
|
||
```bash
|
||
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.
|
||
|
||
## Task 4a — scalar-only feature extraction — 2026-09-25
|
||
|
||
### Scope
|
||
|
||
`FeatureExtractor.Extract` always built a 32×32 bitmask — 1024 `Polygon.ContainsPoint` ray-cast tests plus a 1,024-byte array — even though the only production consumer at runtime (the `AngleCandidateBuilder` ML branch) reads nothing from `PartFeatures.Bitmask`. This slice adds `Extract(Drawing, bool includeBitmask)`; the original one-argument signature is preserved as a source- and binary-compatible wrapper that keeps the training bitmap. Every scalar formula, tolerance, material filter, and the canonical-frame normalization run identically in both modes. Training consumers (`OpenNest.Training`) are unchanged, and `AngleCandidateBuilder` is not yet switched over — Task 4b gates the inference call site. No production behavior changes for any existing caller.
|
||
|
||
`PerfCounters.FeatureBitmaskCells` (Debug-only, incremented inside the bitmask scan loop) was added to prove the point-in-polygon work is genuinely skipped.
|
||
|
||
### Source provenance
|
||
|
||
- Base: `6863c8bdb1c7d98eb5b7c45153e062e7cb335464` on `master`. Final implementation is delivered with this report.
|
||
- `FeatureExtractor.cs` SHA-256 before `1094d386595c0513fcb6db1e5ba7b2c7a0b8c19cf351f6cf9a462356156fdd15` → after `2d7b45350abd94d7e728c962aca06f4a86920d0328d8025c75f51ebc7e5ed6b3`.
|
||
- `PerfCounters.cs` before `0790401ae0433e416c4ac40d678403407497226d6bd40057b7c4e959da6e0582` → after `b596437210719981940ebf7ae03b31b7493f44002cf545bbe638c35bebf5b65d`.
|
||
- Harness `FillPerformanceTests.cs` at measurement time: before/after runs identical at `c257b27a5a4335278c99b8f623540b5580b51d1cc9d31a04bc8d8641a9c722a0` (new method added to base `93ec06c5b59aecfe5cb9311380afea507cc134e77df7535036fb451ab0f63e29`; existing methods byte-identical). The delivered tree ships `27c213f202e46cdd9a1e46bb0263784277f2aecc17469ddd83c5db26c1b129aa` — disclosure/comment wording only, timed code paths identical.
|
||
- Environment: same shared KVM VM as prior slices — Ubuntu 24.04.5 LTS x64, four vCPUs presented as AMD Ryzen 9 5900X, SDK 10.0.112, .NET 8.0.31 adapters confirmed, 1 GHz stopwatch, serial Release.
|
||
|
||
### Work-removal evidence (genuine red/green)
|
||
|
||
The change is additive, so red was demonstrated by neutralizing the new flag in the delivered overload (forcing always-bitmap construction — exactly the original production behavior): on the final tree 8 of 38 Debug cases failed — the five `ScalarOnlyOverload_OmitsBitmap` theory cases, `ScribeAndRapidMarks_DoNotAffectEitherOverload`, and `RotatedInput_FeaturesMatchAxisAlignedEquivalent` with `Assert.Null() Failure: Value is not null`, plus `ScalarOnlyOverload_RunsNoBitmaskCellScan` failing the same assertion inside its counted window. Restoring the flag turned all 38 Debug tests green. On the delivered tree: default extraction counts exactly 1,024 bitmask cells per call (matching the documented scan), scalar-only counts 0, and failed extraction (rapid-only program) counts 0.
|
||
|
||
New `OpenNest.Tests/ML/FeatureExtractorTests.cs` (35 behavior cases plus 3 Debug counter cases, serialized in `FillCacheCollection` for the counter tests): exact-equality scalar snapshots across both overloads on five shapes (rectangle, right triangle, L-shape concave profile, native-arc ring, native circle); explicit-true bitmap equality bit-for-bit against the default overload; default training-bitmap shape/content preservation (1,024 cells, 0/1 only, mixed for ring/L-shape, all-set for a full rectangle); rotated-input invariance against an axis-aligned equivalent; canonical-copy input; scribe/rapid marks ignored by both overloads with identical bitmaps; input non-mutation; determinism across repeated extraction. Because `ExplicitTrue_MatchesDefaultOverloadBitForBit` alone cannot detect a rasterization change applied to both overloads (the default delegates to it), the five fixtures are additionally pinned against frozen SHA-256 hashes of the bitmaps emitted by the actual pre-change implementation at `6863c8b` (captured by running the original code, cross-checked by the spec reviewer against an in-memory HEAD compilation). Pre-existing edge behavior is characterized, not changed: rapid-only drawings throw `ArgumentOutOfRangeException` in `ShapeProfile` and null drawings throw `NullReferenceException` identically through both overloads.
|
||
|
||
### Tests
|
||
|
||
| Suite | Passed | Skipped | Failed |
|
||
| --- | ---: | ---: | ---: |
|
||
| Targeted Release (`FeatureExtractorTests|CanonicalFrameTests`) | 40 | 0 | 0 |
|
||
| Targeted Debug (`FeatureExtractorTests`) | 38 | 0 | 0 |
|
||
| Full main Release | 1,301 | 17 | 0 |
|
||
| Full main Debug | 1,321 | 17 | 0 |
|
||
| Full engine Release | 300 | 0 | 0 |
|
||
|
||
Skips: 12 optional CHR-font fixtures plus five opt-in benchmarks (the new feature-extraction case adds one). Gate verified: unset skips all five category tests. Changed-file `dotnet format whitespace --verify-no-changes` and `git diff --check` passed; no new production warnings.
|
||
|
||
### Results
|
||
|
||
Same-harness, same-machine, serial Release; ring OD=20 ID=8 (perimeter plus one circular cutout); warmup 2×200, measured 7×1,000 calls, alternating mode order, correctness checks outside timing; allocations are current-thread synchronous only. The before run exercised the original always-bitmap implementation in both modes (harness scalar lambda temporarily pointed at the full path); the after run is the delivered tree.
|
||
|
||
| Batch | Before full ms / B | Before scalar-mode ms / B | After full ms / B | After scalar-only ms / B |
|
||
| ---: | --- | --- | --- | --- |
|
||
| 1 | 154.692010 / 35,338,120 | 154.066548 / 35,332,896 | 197.474351 / 35,338,120 | 31.020699 / 34,252,896 |
|
||
| 2 | 161.643928 / 35,338,120 | 151.896080 / 35,332,896 | 157.026495 / 35,332,896 | 29.342949 / 34,252,896 |
|
||
| 3 | 149.279498 / 35,332,896 | 155.797847 / 35,332,896 | 154.219132 / 35,332,896 | 32.988987 / 34,268,256 |
|
||
| 4 | 153.772483 / 35,332,896 | 153.226512 / 35,332,896 | 161.453458 / 35,332,688 | 41.092334 / 34,252,688 |
|
||
| 5 | 155.591257 / 35,338,120 | 155.760388 / 35,332,896 | 151.984770 / 35,332,688 | 30.768833 / 34,252,688 |
|
||
| 6 | 153.993059 / 35,332,896 | 154.410087 / 35,332,896 | 153.549576 / 35,332,688 | 29.824879 / 34,252,688 |
|
||
| 7 | 155.984019 / 35,338,120 | 159.061932 / 35,332,896 | 157.327152 / 35,332,688 | 29.530524 / 34,252,688 |
|
||
|
||
Medians (µs/call): before scalar-mode (original behavior) 154.410 → after scalar-only 30.769 (−80.1%). Allocations: medians 35,332.896 → 34,252.688 B/call. Within the after run, full minus scalar-only per batch is 1,085.224 / 1,080.000 / 1,064.640 / 1,080.000 / 1,080.000 / 1,080.000 / 1,080.000 B/call (median 1,080.000): the 1,048-byte `byte[1024]` allocation plus ~32 B from the harness's in-window bitmap-count consumption (a reviewer probe on .NET 8.0.31 measured 1,048 and 32 respectively) — i.e. extraction-plus-consumption overhead, not 1,080 B of production extraction alone; the production `GenerateBitmask` loop itself allocates nothing beyond the array. Before scalar-mode and before-full rows agree within noise, confirming the harness adaptation introduced no bias. Timing likewise includes that enumeration in full mode.
|
||
|
||
Default-overload (training) timing is inconclusive: after-full median 157.026 vs before-full 154.692 µs/call (+1.5%, inside the overlapping batch ranges; the after run's batch 1 at 197.474 is a warmup-adjacent outlier included unmodified). Overlapping ranges establish neither regression nor non-regression for the full mode.
|
||
|
||
### Reproduction and limitations
|
||
|
||
```bash
|
||
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
|
||
--filter 'FullyQualifiedName~FeatureExtraction_ReportsFullAndScalarOnly' --logger 'console;verbosity=detailed'
|
||
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
|
||
--filter 'FullyQualifiedName~FeatureExtractorTests|FullyQualifiedName~CanonicalFrameTests'
|
||
```
|
||
|
||
This is a capability slice, not yet a production speedup: the inference call site keeps using the default overload until Task 4b gates predictor availability, so real nesting runs show no change from this commit. The measured gain applies to inference callers once wired. No representative production corpus, Windows UI runtime test, or ONNX inference was performed; the Debug counter increments compile away in Release.
|
||
|
||
## Task 4b — gate angle features on predictor availability — 2026-09-26
|
||
|
||
### Scope and contract
|
||
|
||
`AngleCandidateBuilder` now checks `AnglePredictor.IsAvailable` before extraction, and available inference requests `includeBitmask: false`. The public parameterless constructor remains; readonly per-instance delegates provide isolated test doubles without process-global overrides. The prediction default threshold (0.3), `Width` then `Length` argument order, prediction-prefix duplicates, tolerance-based appended-angle deduplication, base/sweep order, constraints, classification shortcuts and known-good pruning are unchanged. The legacy repeated-addition sweep includes a value just below π: 37 sweep entries, not an integer-indexed 36-angle replacement.
|
||
|
||
`IsAvailable` and `PredictAngles` use the same initializer. The existing lock/volatile one-attempt pattern lives in the narrow internal `SingleAttemptLoader<T>` (generic solely to test with reference objects instead of ONNX files). Completion is published in `finally`, after assignment or definitive failure; a concurrent reader waits rather than seeing transient unavailability. Missing and failed loads remain cached for the process lifetime; no model reload policy, independent availability cache, model-file changes, packages or projects were added. Training extraction is unchanged.
|
||
|
||
### Provenance and verification
|
||
|
||
- Base: `d70505b7c0e41692a031971fb437bbb165493b9f` on `master`; this section accompanies the final implementation commit. The baseline was an isolated `git archive` with only the identical measurement harness added. Production/project comparison found only `AngleCandidateBuilder.cs` and `AnglePredictor.cs` changed; actual baseline production was measured, not a reimplementation.
|
||
- Environment: Ubuntu 24.04.5 LTS x64 shared KVM VM, four vCPUs presented as AMD Ryzen 9 5900X; SDK 10.0.112. Test adapters and benchmark output confirm .NET 8.0.31; Stopwatch frequency 1,000,000,000 ticks/s. Serial Release processes, forward then reversed before/after order.
|
||
- Parent applied whitespace-only fixes to the builder constructor and the loader concurrency test after implementation-agent tests; full suites were rerun afterward. Published measurements use the final source/harness hashes below. An initial short no-model warmup showed transient timing spread; it was replaced in **both** trees by 2×20,000 warmup calls and 7×20,000 measured calls. Only the complete rerun pairs below are acceptance measurements.
|
||
|
||
| Source | Before SHA-256 | Delivered/measurement SHA-256 |
|
||
| --- | --- | --- |
|
||
| `OpenNest.Engine/Fill/AngleCandidateBuilder.cs` | `5182f69f604332e1b8bfb46d807c8f830194a88b47f4fd4e90bcea6e91e522c4` | `97bbb82680b90d950f480c65dffab8702bd9402ef849639286d7fc90f1b6b162` |
|
||
| `OpenNest.Engine/ML/AnglePredictor.cs` | `84bc0eafffe84447dc83e3c7084e5562a67409572fa413f78abe6e7102ba3c72` | `047e3bdb8d9aec1add49ba36e5b7947d1b375e6099a3f36f54061b6ae4d16f7a` |
|
||
| `OpenNest.Engine/ML/FeatureExtractor.cs` | `2d7b45350abd94d7e728c962aca06f4a86920d0328d8025c75f51ebc7e5ed6b3` | `2d7b45350abd94d7e728c962aca06f4a86920d0328d8025c75f51ebc7e5ed6b3` |
|
||
| `OpenNest.Tests/Fill/FillPerformanceTests.cs` | `f90412c87393f8542769aae5abef5e340b827003c134d0bf525efef3a3fefa62` | `f90412c87393f8542769aae5abef5e340b827003c134d0bf525efef3a3fefa62` |
|
||
|
||
The harness hash in both columns is the newly added identical harness, not the original base harness. Existing feature-extraction timed code is unchanged.
|
||
|
||
Genuine RED/GREEN evidence (temporary source mutations restored in `finally`): removing the availability gate failed the exact call sequence (expected availability only, actual availability/extraction/prediction); forcing `true` failed the scalar flag assertion; the real-extractor Debug integration check counted 1,024 bitmap cells instead of 0; publishing before the blocked load failed all three concurrent-reader cases (success, missing, throwing). Restored targeted suites pass. Counter tests use the nonparallel `FillCacheCollection` and reset in `finally`. Release-zero counters are not evidence. The loader concurrency test observes a blocked dedicated reader, not merely an unscheduled task; ten additional process runs passed all three cases.
|
||
|
||
| Verification | Passed | Skipped | Failed |
|
||
| --- | ---: | ---: | ---: |
|
||
| Pre-change builder characterization | 17 | 0 | 0 |
|
||
| Targeted Release (builder/predictor/extractor) | 77 | 0 | 0 |
|
||
| Targeted Debug (builder/predictor/extractor) | 81 | 0 | 0 |
|
||
| Full main Release, final code | 1,335 | 18 | 0 |
|
||
| Full main Debug, final code | 1,356 | 18 | 0 |
|
||
| Full engine Release, final code | 300 | 0 | 0 |
|
||
| Performance category, variable unset | 0 | 6 | 0 |
|
||
| Performance category, variable `0` | 0 | 6 | 0 |
|
||
| Each enabled two-case measurement process (four runs) | 2 | 0 | 0 |
|
||
|
||
Full-suite skips are 12 optional CHR-font fixtures plus six opt-in benchmarks. Parent parsed TRX outcomes and reconciled all published raw measurement rows against console output. Changed-file whitespace verification and `git diff --check` passed. Existing optional-fixture, frozen-reference nullable and obsolete API warnings remain outside this slice.
|
||
|
||
### Warm no-model path measurements
|
||
|
||
Closed concave L vertices `(0,0),(20,0),(20,6),(8,6),(8,14),(0,14)`, primary angle 0.13 radians, work area `(3,5,120,80)`, `ForceFullSweep=true`. Public production builder, no delegate substitution. Missing-model initialization, construction, assertions and output occur outside timing. Exact ordered angles are checked against the pre-change fallback expression. Each process has two 20,000-call warmups and seven 20,000-call batches. Loop/result-count consumption and GC are included; allocations are synchronous current-thread bytes, not process RSS. The benchmark skips if a model exists and never removes it.
|
||
|
||
| Batch | Before forward ms / B | After forward ms / B | After reverse ms / B | Before reverse ms / B |
|
||
| ---: | --- | --- | --- | --- |
|
||
| 1 | 1217.927296 / 503,670,984 | 55.374701 / 119,200,000 | 52.644238 / 119,200,000 | 1184.910444 / 503,670,984 |
|
||
| 2 | 1187.003770 / 503,660,640 | 54.922257 / 119,200,000 | 53.317338 / 119,200,000 | 1245.986050 / 503,660,640 |
|
||
| 3 | 1191.853980 / 503,660,640 | 53.574434 / 119,200,000 | 52.454731 / 119,200,000 | 1234.360386 / 503,660,640 |
|
||
| 4 | 1207.375477 / 503,660,640 | 52.648817 / 119,200,000 | 51.054148 / 119,200,000 | 1247.194291 / 503,660,640 |
|
||
| 5 | 1212.013076 / 503,670,984 | 52.498333 / 119,200,000 | 51.060020 / 119,200,000 | 1236.030807 / 503,670,984 |
|
||
| 6 | 1223.337623 / 503,660,640 | 51.969916 / 119,200,000 | 51.673347 / 119,200,000 | 1265.240358 / 503,660,640 |
|
||
| 7 | 1249.298485 / 503,660,640 | 51.376286 / 119,200,000 | 52.600186 / 119,200,000 | 1264.421373 / 503,660,640 |
|
||
|
||
| Process | µs/call min / median / max | B/call min / median / max |
|
||
| --- | --- | --- |
|
||
| before-measure | 59.350 / 60.601 / 62.465 | 25,183.0320 / 25,183.0320 / 25,183.5492 |
|
||
| after-measure | 2.569 / 2.632 / 2.769 | 5,960.0000 / 5,960.0000 / 5,960.0000 |
|
||
| after-repeat | 2.553 / 2.623 / 2.666 | 5,960.0000 / 5,960.0000 / 5,960.0000 |
|
||
| before-repeat | 59.246 / 62.299 / 63.262 | 25,183.0320 / 25,183.0320 / 25,183.5492 |
|
||
|
||
Measured median warm-path time changes are -95.66% and -95.79% in the two process orders. Median allocations fall from 25,183.032 to 5,960 B/call (19,223.032 B/call, 76.33%). After allocations are exactly 5,960 B/call in every batch; baseline totals have small variations disclosed above. These results support a local warm no-model improvement, not a stable cross-machine latency guarantee or a whole-nesting-job speedup. No cold-start or failed-model timing is claimed.
|
||
|
||
### Separate extraction control
|
||
|
||
Existing `FeatureExtraction_ReportsFullAndScalarOnly`: native-arc ring OD20/ID8, two 200-call warmups, seven 1,000-call batches per mode, alternating mode order. This is a separate extraction benchmark, not available-model inference. Feature extraction is identical across these trees; the full versus scalar difference is the already-delivered Task 4a capability, now used by the builder. Timing/bytes include the harness's bitmap-count consumption for full mode (see Task 4a's ~32-byte consumption disclosure), so the delta is not solely production extraction allocation. Process-to-process differences are shared-VM noise/control observations, not a Task 4b extraction speedup claim.
|
||
|
||
| Process | Batch | Full ms / B | Scalar-only ms / B |
|
||
| --- | ---: | --- | --- |
|
||
| before-measure | 1 | 217.757693 / 35,332,688 | 37.378244 / 34,263,032 |
|
||
| before-measure | 2 | 170.635584 / 35,332,688 | 34.053642 / 34,252,688 |
|
||
| before-measure | 3 | 172.566367 / 35,343,032 | 34.951535 / 34,252,688 |
|
||
| before-measure | 4 | 170.000626 / 35,332,688 | 33.384049 / 34,273,376 |
|
||
| before-measure | 5 | 169.570514 / 35,332,688 | 34.399103 / 34,252,688 |
|
||
| before-measure | 6 | 171.122593 / 35,332,688 | 33.865496 / 34,283,720 |
|
||
| before-measure | 7 | 168.040086 / 35,332,688 | 32.055312 / 34,263,032 |
|
||
| after-measure | 1 | 227.885101 / 35,332,896 | 32.285726 / 34,252,896 |
|
||
| after-measure | 2 | 175.358064 / 35,338,120 | 30.359862 / 34,252,896 |
|
||
| after-measure | 3 | 184.505182 / 35,332,896 | 31.867147 / 34,252,896 |
|
||
| after-measure | 4 | 169.872394 / 35,332,896 | 29.338054 / 34,252,896 |
|
||
| after-measure | 5 | 168.238732 / 35,338,120 | 30.606238 / 34,252,896 |
|
||
| after-measure | 6 | 189.879784 / 35,332,896 | 30.637576 / 34,252,896 |
|
||
| after-measure | 7 | 174.944123 / 35,332,896 | 30.820803 / 34,252,896 |
|
||
| after-repeat | 1 | 272.298581 / 35,332,896 | 52.314757 / 34,252,896 |
|
||
| after-repeat | 2 | 194.604106 / 35,338,120 | 52.072921 / 34,252,896 |
|
||
| after-repeat | 3 | 159.231266 / 35,332,896 | 33.244075 / 34,252,896 |
|
||
| after-repeat | 4 | 152.710834 / 35,332,896 | 31.001913 / 34,252,896 |
|
||
| after-repeat | 5 | 154.032296 / 35,338,120 | 31.102123 / 34,252,896 |
|
||
| after-repeat | 6 | 150.289445 / 35,332,896 | 29.016949 / 34,252,896 |
|
||
| after-repeat | 7 | 156.205127 / 35,332,896 | 30.082710 / 34,252,896 |
|
||
| before-repeat | 1 | 215.909255 / 35,332,688 | 37.149342 / 34,252,688 |
|
||
| before-repeat | 2 | 188.558239 / 35,332,688 | 35.593749 / 34,252,688 |
|
||
| before-repeat | 3 | 198.129599 / 35,343,032 | 38.193683 / 34,252,688 |
|
||
| before-repeat | 4 | 186.170223 / 35,332,688 | 36.974173 / 34,252,688 |
|
||
| before-repeat | 5 | 193.646279 / 35,332,688 | 46.604443 / 34,252,688 |
|
||
| before-repeat | 6 | 195.491390 / 35,332,688 | 50.731669 / 34,252,688 |
|
||
| before-repeat | 7 | 191.895666 / 35,332,688 | 40.247489 / 34,252,688 |
|
||
|
||
| Process | Full µs/call min / median / max | Scalar µs/call min / median / max |
|
||
| --- | --- | --- |
|
||
| before-measure | 168.040 / 170.636 / 217.758 | 32.055 / 34.054 / 37.378 |
|
||
| after-measure | 168.239 / 175.358 / 227.885 | 29.338 / 30.638 / 32.286 |
|
||
| after-repeat | 150.289 / 156.205 / 272.299 | 29.017 / 31.102 / 52.315 |
|
||
| before-repeat | 186.170 / 193.646 / 215.909 | 35.594 / 38.194 / 50.732 |
|
||
|
||
### Reproduction, review and remaining limits
|
||
|
||
```bash
|
||
OPENNEST_RUN_FILL_PERF=1 dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
|
||
--filter 'FullyQualifiedName~IrregularAngles_ReportsWarmNoModelPath|FullyQualifiedName~FeatureExtraction_ReportsFullAndScalarOnly' \
|
||
--logger 'console;verbosity=detailed'
|
||
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
|
||
--filter 'FullyQualifiedName~AngleCandidateBuilderTests|FullyQualifiedName~AnglePredictorTests|FullyQualifiedName~FeatureExtractorTests'
|
||
```
|
||
|
||
Repeat the targeted command in Debug for bitmap work assertions. Shared safeguards moved intact (with Task 4b additions) from near-capacity `AGENTS.md` into [fill verification](fill-verification.md); the combined `AGENTS.md` + `CLAUDE.md` is now 31,434 bytes, below 32 KiB. README and the shared instructions describe availability-gated scalar inference; `CLAUDE.md` remains the thin import.
|
||
|
||
Independent spec review **PASS**, followed by independent quality/integration review **APPROVED**; neither found Critical, Important or Minor issues. Both reconciled source/harness provenance, raw measurements, summaries, red/green and full-suite evidence. Each independently reran the targeted suites (77 Release, 81 Debug) and both disabled benchmark gates (six skipped each); the spec reviewer ran ten more three-case concurrency processes, and the quality reviewer ran three plus the full engine suite (300 passed). Raw scratch evidence was removed after review, retaining the tables, hashes and test summaries here. No actual ONNX inference or model accuracy test, Windows UI runtime test, or representative production corpus measurement was performed. Task 5 combined acceptance/report remains separate; gated A/B/C follow-ups were not started.
|
||
|
||
## Task 5 — combined initial-batch acceptance — 2026-09-26
|
||
|
||
This section closes the initial batch (Tasks 1–4b, delivered as `b318950`, `cec7396`, `6efa6b1`, `4553f8a`/`6863c8b`, `8188533`, `1e8e532`) by rerunning the complete verification surface on the final tree and adding the first whole-job before/after measurement. No production code changed in this task.
|
||
|
||
### Acceptance suite results on the final tree
|
||
|
||
Final tree: `1e8e532063434cbbc4aeeea6d50e090f4998ac00` on `master` (SDK 10.0.112, target runtime Microsoft.NETCore.App 8.0). Same machine as all earlier sections (KVM guest, 4 vCPUs presented as AMD Ryzen 9 5900X, Ubuntu 24.04).
|
||
|
||
| Run | Passed | Skipped | Failed | vs Task 0 baseline |
|
||
| --- | ---: | ---: | ---: | --- |
|
||
| Main suite, Release | 1,335 | 18 | 0 | +195 passed; skips are 12 optional CHR fixtures + 6 opt-in benchmarks |
|
||
| Main suite, Debug | 1,356 | 18 | 0 | +216 passed; same skip composition |
|
||
| Engine suite, Release | 300 | 0 | 0 | identical to baseline |
|
||
|
||
Baseline was 1,140 passed / 12 skipped / 0 failed (main) and 300 / 0 / 0 (engine). Every added test from Tasks 1–4b passes; no test failures were observed. With `OPENNEST_RUN_FILL_PERF` unset the Release category run skips all six benchmarks (verified: 6 skipped / 0 failed).
|
||
|
||
### Final-tree opt-in benchmark reproduction
|
||
|
||
All six `Category=FillPerformance` cases passed enabled on the final tree in one run. Medians reproduce the per-task delivered numbers within shared-VM process noise (no before/after pair was rerun here; each task section retains its own same-harness pairs):
|
||
|
||
| Case | Final-tree median | Delivered reference |
|
||
| --- | --- | --- |
|
||
| unequal-counts actual | 1.583 ns/call | 1.878 ns/call (Task 1) |
|
||
| unequal-counts reference | 11,956.654 ns/call | 11,979.646 ns/call (Task 1) |
|
||
| equal-count control actual/reference | 11,164.062 / 11,190.333 ns/call | same range as reference (Task 1) |
|
||
| group-pattern default / custom | 640.955 / 635.597 µs/call | overlapping-range control (Task 1b) |
|
||
| extents spacing 0 / 0.5 | 2,188.589 µs/call, 839,480 B/call; 2,201.340 µs/call, 1,028,400 B/call | 839,624 / 1,028,544 B (Task 2/3) |
|
||
| feature full / scalar-only | 163.135 / 29.558 µs/call (−81.9%); Δ 1,080.2 B/call | −80.1%; 1,080 B (Task 4a) |
|
||
| rotated-pattern angle 0 / 0.37 | 15.923 / 30.306 µs/call | Task 3 control |
|
||
| no-model-angles | 2.647 µs/call, 5,960 B/call | 2.632 / 2.623 µs/call, 5,960 B/call (Task 4b) |
|
||
|
||
### First whole-job before/after corpus measurement
|
||
|
||
Inspecting `OpenNest.Benchmark/Program.cs` first, as planned: the CLI accepts a `.nest` file, folder, or `*.manifest.json`; registered engines are `Default`, `Strip`, `Vertical Remnant`, `Horizontal Remnant`, `StockLadder`; no `Engines/` plug-ins are installed. The repository contains no `.nest` corpus, so a small representative corpus was built from the user-supplied production archive `/mnt/rogers/PEP Drawings/Archive/` (27,722 files, 27,420 of them `.dxf` by case-insensitive name) as a four-DXF manifest — a 1.5-inch tube with tapped hole, a 17×13 bent plate (`1214 A02 PT04`), a 4.44×2 bracket (`1000-164-11`), and a 1.5-inch pipe saddle, quantities 60/28/45/36 (169 parts), one 48x96 sheet candidate, part/edge spacing 0.5, rotation allowed. Manifest SHA-256 `3f13c7a674b2a33a26f31dcbf8fec3fc70ab7ef7f10bd801fb784d3d9179987b`; DXF SHA-256s `c44ee5f49a8b0bccd810b202fcc96f065bac5e1b49b51b5073003116011b1852`, `a8155c68ac30e165743676f68c339c03f4633945ccd1559c4e929c0f8d6f8f2c`, `3dff4381653e1bf7843f25dd8d4e09a253393e320285ec29faabea37c8270e59`, `366e2e3fceea673d0563ae6a16feac4e93b7867ed6b8313d9c8841cf40c6ff01`. Selection was bounded to a handful of drawings per the user's instruction.
|
||
|
||
Corpus limitations recorded honestly: circle-heavy archive parts (small washers, OD plates) validate INVALID at baseline `1b862dc` — identical on both trees, a pre-existing spacing-validation behavior outside this batch's scope — so they were excluded rather than papered over. At higher quantities (280 parts) both trees crash identically with `InvalidOperationException: Candidate placements violate required part spacing`, again baseline-equal and out of scope. The accepted workload is the 169-part job that validates cleanly on both trees.
|
||
|
||
Before tree: baseline `1b862dc` built via a separate git worktree. Final tree: `1e8e532`. Neither build has `Models/angle_predictor.onnx`, so both run under the same model-absent conditions with Task 4b's fallback behavior preserved (the executed paths intentionally differ: baseline extracts features before discovering prediction is unavailable, the final tree checks availability first). Four alternating baseline/final process repetitions each, `--engines Default,StockLadder --parallel 1`, same manifest and arguments; no salvage options were passed, so salvage credit resolves disabled on both trees. Each repetition is a fresh process (no process-global or JIT state carries across repetitions), and each solve builds its own `NestJob` snapshot and materialized drawings; within one repetition the two engine solves run sequentially in that process (StockLadder first, then Default). No `.nest` best-fit caches are involved and DXF import happens once during manifest loading, outside the timed region. Reported `Time(ms)` starts before `BuildNestJob` and covers geometry-snapshot construction, solving, materialization and validation per `BenchmarkRunner`, and no whole-job allocation totals were measured. Wall-clock time per solve:
|
||
|
||
| Engine | Baseline times (ms) | Final times (ms) | Median Δ |
|
||
| --- | --- | --- | --- |
|
||
| Default | 41,108 / 40,711 / 40,396 / 40,553 | 40,466 / 40,689 / 40,229 / 40,939 | −0.13% |
|
||
| StockLadder | 1,216 / 1,164 / 1,108 / 1,171 | 1,156 / 1,197 / 1,134 / 1,165 | −0.60% |
|
||
|
||
All eight runs classify identically: valid, fully placed 169/169, cost 9,216.00, 2 plates (48x96×2), empty validation notes — the acceptance gate's valid/invalid, requested/placed, and cost non-regression requirements pass on outcomes. Baseline run-to-run spread alone is 712 ms (Default) and 108 ms (StockLadder), wider than the medians' differences and overlapping the final ranges, so no reproducible material timing difference was observed in either direction — timing is inconclusive, not a measured equivalence. That fits scope: the hot phases on this job (NFP pair evaluation, BestFit sweeps, validation) were not touched by Tasks 1–4b, whose measured wins are localized to comparer dispatch, discarded extents pitch geometry, redundant bounds walks, and the no-model feature path. No scheduling was altered to influence these results.
|
||
|
||
Preserved raw evidence (outside the repository): manifest `pep-archive-benchmark-manifest.json`, per-run `baseline-rep1..4` / `final-rep1..4` CSV and console logs, and the full-tree benchmark log `fillperf-final-tree.log`, all under `/home/aj/extracted/2026-09-26/`. Reproduce with:
|
||
|
||
```bash
|
||
dotnet OpenNest.Benchmark/bin/Release/net8.0/OpenNest.Benchmark.dll \
|
||
/home/aj/extracted/2026-09-26/pep-archive-benchmark-manifest.json \
|
||
--engines Default,StockLadder --parallel 1 --csv <path>
|
||
```
|
||
|
||
### Batch-level conclusion and limits
|
||
|
||
- Removed work is demonstrated by the deterministic Debug counters and red/green evidence in each task section; correctness is carried by the final-tree 1,335/1,356/300 passing suites; measurable local gains exist where allocation or scanning work was actually eliminated (Tasks 2, 3, 4a, 4b), with timing claims bounded to their same-harness pairs.
|
||
- The whole-job corpus measurement above confirms identical valid classification, placed/requested counts, and cost on one small real-drawing job, with timing differences inside baseline run-to-run spread — inconclusive timing, not a measured performance equivalence, and not a speedup claim. A representative production `.nest` corpus at scale, Windows UI runtime testing, and actual ONNX-model inference remain untested.
|
||
- Gated follow-ups A (offset-geometry reuse), B (sorted overlap broad phase), and C remain unauthorized and were not started.
|