From b7b4000aabef53b603ec820cd093b1105135678e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 9 Oct 2026 00:57:23 +0000 Subject: [PATCH] Record prepared snapshot ownership evidence --- docs/roadmap/implementation.md | 109 ++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 0436cbb..b136f41 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -3,7 +3,7 @@ Implement the remaining local work in the [performance roadmap](performance.md) in order. Each sequential stage is scoped for one Luna coding-agent prompt. Update status and evidence as each work unit lands. -Stages 1–11 are complete; Stages 12–14 remain pending. The completed +Stages 1–12 are complete; Stages 13–14 remain pending. The completed inspection plan is preserved at `481e2d4:docs/roadmap/implementation.md` in Git history. @@ -457,6 +457,13 @@ separate from engine construction and provider latency. ## Stage 12 — Establish Snapshot Ownership And Live-Heap Evidence +**Status: Complete (2026-10-09).** Internal snapshot and fixed-batch retained- +heap benchmarks are committed at `11ecb92`. The ownership inventory confirms +that message/cache metadata, hashes, and schema are preparation-owned, while +nested `ExtraParams` still need a deep copy before any transfer. Repeated +measurements and lifecycle assumptions are recorded below; no production +behavior changed. + **Outcome:** A measured ownership inventory for Stage 13's conditional change. No production change; keep this analysis separate from creating the assembled workflow benchmarks. @@ -785,6 +792,106 @@ than point claims. Focused verification passed: `go test -mod=readonly .`; representative rendered messages, prepared details, generated output, and structured validation were checked outside benchmark timing. +### Prepared Snapshot Ownership And Live-Heap Baseline + +The benchmark-only baseline is committed at `11ecb92` (`Add prepared snapshot +ownership benchmarks`). `BenchmarkPreparedSnapshot` exercises the internal +runner with the existing fake prompt/profile repositories, renderer, artifact +reader, a stateless injected client, and a fast prepared validator. It measures +`PrepareExecution` and discard without compiling a schema. Its small fixture +uses one rendered message, no provider extras, and a one-property schema. The +mutable fixture uses eight rendered messages plus one appended message, cache +metadata on each, one input/hash, 64 nested provider-option entries, and a +256-property schema. Fixture checks run outside timing; each measured handle is +discarded and only the latest discarded handle is retained as a sink. + +`BenchmarkRetainedPreparedHandles` uses the mutable fixture and a fixed batch +of four handles. After fixture setup and checks, it preallocates an empty handle +slice, forces GC, and records a setup-only `HeapAlloc` baseline. It then retains +four ready, completed, or discarded handles, forces GC again, and reports the +heap delta. The retained benchmark command uses `-benchtime=1x`; the single +timed operation is followed by filling the remaining batch slots outside the +timer. Both samples call `runtime.KeepAlive` for the runner, request, and +retained batch. The metric is a live-heap difference, not a derivation from +`B/op`. + +Environment: Linux/amd64, Intel Core i5-7260U @ 2.20 GHz, benchmark parallelism +4, Go 1.26.8, ConfigKit `v0.5.0`, JSON Schema `v6.0.2`, and backend modules +`v3.20260930.0`. + +Snapshot command (five 500 ms samples, without the race detector): + +```sh +GOTOOLCHAIN=local GOWORK=off GOPROXY=off GONOPROXY=none GOSUMDB=off \ + go test -mod=readonly -run '^$' -bench '^BenchmarkPreparedSnapshot$' \ + -benchmem -benchtime=500ms -count=5 ./internal/usecase +``` + +Times list the five `ns/op` samples followed by median and range; allocation +figures are `B/op; allocs/op` (ranges where they varied). + +| Snapshot case | `ns/op` samples (median; range) | Allocations | +| --- | --- | --- | +| Small | 45776, 51524, 46933, 46834, 44663 (46834; 44663–51524) | 13014–13016; 216 | +| Mutable payload | 5343223, 5234718, 4213550, 4419520, 5046181 (5046181; 4213550–5343223) | 1211931–1212104; 25243–25244 | + +Retained-heap command (five fixed-batch samples): + +```sh +GOTOOLCHAIN=local GOWORK=off GOPROXY=off GONOPROXY=none GOSUMDB=off \ + go test -mod=readonly -run '^$' -bench '^BenchmarkRetainedPreparedHandles$' \ + -benchmem -benchtime=1x -count=5 ./internal/usecase +``` + +| Retained handle state | `retained-B` samples (median; range) | Timed-operation allocations | +| --- | --- | --- | +| Ready | 1458352, 1458256, 1459320, 1459048, 1459160 (1459048; 1458256–1459320) | 1211896–1213256 B; 25242–25251 | +| Completed | 731040, 730768, 729352, 729448, 729624 (729624; 729352–731040) | 1213024–1214368 B; 25255–25264 | +| Discarded | 728560, 730928, 730544, 730496, 730496 (730496; 728560–730928) | 1211896–1213256 B; 25242–25251 | + +The ready four-handle batch retains about 1.46 MB above setup; completed and +discarded batches retain about 0.73 MB. The roughly 0.73 MB difference is +consistent with the extra execution snapshot held only while a handle is +ready. After claim/run or discard, `payload.clear` removes that execution +payload while the independent details snapshot remains available. Heap deltas +include runtime/GC noise; the five observed ranges are reported rather than +treated as an exact object-size guarantee. + +The source ownership inventory is: + +- `completePreparationWithStructuredOutput` builds a fresh input-hash map and + obtains rendered messages through `domain.ConcatRenderedMessages`, which + copies message slices and each `CacheControl` pointer while sharing immutable + string contents. +- `structuredOutputFromValidationPlan` copies the validation plan's schema + document with `jsonvalue.Copy` before storing it in the prepared run. +- `resolveExecutionTarget` and `mergeExecutionTargetOverride` use the + shallow-copy helper `copyExtraParams`. The configuration repository's full + `GetProfile` and the public request conversion deep-copy extra values, but + internal profile fakes return only shallow struct copies and internal + `RunRequest` override maps may be source-owned. Nested extras can therefore + remain aliased through the runner's outer-map copy. Stage 13 must keep a + `jsonvalue.CopyMap` boundary before transferring the prepared run. +- `PrepareExecution` deep-clones `operation.run` for execution, then clones + that snapshot again for details. `Details()` deep-clones details per call; + the root facade conversion also creates caller-owned maps, schema, messages, + and cache metadata. Claim completion and `Discard` clear only the private + payload, so the details copy survives both lifecycles. +- The injected-client adapter converts generated requests to caller-owned + prompt messages/cache metadata, execution-target extra maps, and structured + schema values. Its client cannot mutate the private prepared snapshot through + a generation request. + +The assembled public `Details()` benchmark independently measured 670315–670339 +B and 11548 allocations for one mutable-payload details call. The internal +snapshot benchmark isolates creation with fakes; together these baselines show +that mutable schema/extras copies dominate the small message-slice cost. The +Stage 13 candidate can remove the execution clone of preparation-owned +messages, hashes, and schema, but must retain a deep copy of extras and the +separate details clone. Focused verification passed: `go test -mod=readonly +./internal/usecase .`; representative content, copied schema, fake validation +status, and post-completion details were checked outside timing. + ### Validation And Compiled-Rendering Baseline The benchmark-only baseline is commit