743 lines
45 KiB
Markdown
743 lines
45 KiB
Markdown
# Performance implementation plan
|
||
|
||
Status: implementation in progress; Stages 1–5 are complete, Stages 6–11 have not started.
|
||
|
||
Date: 2026-10-09.
|
||
|
||
## Scope and authority
|
||
|
||
Implement PF-01 through PF-06 from the [performance roadmap](performance.md).
|
||
Read this entire plan before starting and complete the numbered stages in order.
|
||
Each stage is a separate Luna implementation assignment and must leave the
|
||
repository buildable. The roadmap owns the target and selected OD-1/OD-2
|
||
approaches; this plan owns execution choices, sequencing, and implementation
|
||
evidence.
|
||
|
||
Follow the [architecture policy](../policy/architecture.md), accepted ADRs,
|
||
[agent policy](../policy/agents.md), [testing policy](../policy/testing.md), and
|
||
[documentation policy](../policy/documentation.md). Public Go declarations and
|
||
the [CLI reference](../cli.md) own existing contracts. Private names proposed
|
||
below identify implementation responsibilities, not new public APIs or durable
|
||
internal interfaces.
|
||
|
||
This file replaces the completed ReleaseKit plan. Release tooling is already
|
||
implemented and is outside this sprint. Stage 11 repairs navigation that still
|
||
identifies this file as the ReleaseKit plan. The previous record is available
|
||
through Git history; do not restore its assignments here.
|
||
|
||
No required product or architecture questions remain open. Measurements assess
|
||
the selected improvements; they do not authorize substituting persistent mutable
|
||
backends or segmented YAML delivery for the user's choices.
|
||
|
||
## Baseline and implementation choices
|
||
|
||
The planning baseline is `6612b54`, with a clean worktree. Production code still
|
||
matches the efficiency review at `54cfbd8`; intervening changes are documentation.
|
||
The root facade, internal Cobra adapter, inspection families, and release tooling
|
||
are implemented. No performance benchmarks were found in the indexed code, and
|
||
this planning pass has not executed tests, benchmarks, or profiles. Reinspect
|
||
current state before implementation and record the actual measurement revisions.
|
||
|
||
Use the following concrete choices:
|
||
|
||
- Render PromptKit's requested format in its handler, then pass completed bytes
|
||
to a shared delivery helper. Preserve the existing report DTOs and serializers.
|
||
- Index backend commands directly to neutral declarations. Walk Cobra's existing
|
||
parent links inside the adapter to reconstruct only the selected path.
|
||
- Scan YAML opening lines with byte offsets. Return complete framed bytes plus
|
||
whether the native document has directives, so the aggregate writer can use
|
||
one analysis. Retain the full framed buffer selected in OD-2.
|
||
- Prepare and clone pending command registrations before updating the existing
|
||
registry. Keep batch validation atomic and preserve owned declaration storage.
|
||
- Lazily cache one neutral bridge tree per `App`, invalidating it after a
|
||
successful nonempty registration. Construct fresh mutable backend state for
|
||
each invocation, then reuse it for parsing, help, and usage within that
|
||
invocation, as selected in OD-1.
|
||
- Represent registered configuration metadata with a private generic prepared
|
||
suite containing normalized declarations, ordered scopes, name lookup, and
|
||
discovery inventory. Use name/short-name maps for selector duplicate checks.
|
||
Keep scope selection in preflight and handlers as inexpensive indexed passes.
|
||
Selected-scope memoization and additional selector-membership indexes are
|
||
deferred; their mutable bookkeeping is unnecessary for removing the observed
|
||
quadratic scope searches.
|
||
|
||
PF-05 and the bounded PF-06 changes above are included in the sprint despite their
|
||
lower priority. Implement the straightforward algorithms and measure their cost;
|
||
do not silently omit either coverage ID. Escalate concrete correctness conflicts
|
||
or material regressions rather than add a new consumer restriction or change
|
||
the selected scope.
|
||
|
||
## Instructions for every stage
|
||
|
||
1. Start with [developer orientation](../development.md), inspect Git state, and
|
||
read the assigned sources and prior stage evidence. Prefer knowledge-graph
|
||
tools for code discovery. Preserve unrelated work and read applicable policies.
|
||
2. Implement the assigned stage and necessary supporting tests/documentation.
|
||
Preserve public signatures and the fields of value structs such as
|
||
`Invocation`, `CommandSpec`, and `ConfigSuite`; do not add execution state to
|
||
them. The `App` handle may gain private implementation fields. Cobra/pflag
|
||
objects and errors stay inside their adapter.
|
||
3. Preserve grammar, conversion of every supplied singleton occurrence, explicit
|
||
presence, collection ownership, callback lifecycle, supported error identity,
|
||
diagnostic delivery, and report semantics. Reuse existing coverage; add only
|
||
missing protection for the change's realistic risks. Do not assert private
|
||
helper counts or fixed timings in behavioral tests.
|
||
4. Format changed Go files. Run the stage's focused validation and measurements.
|
||
Dependency preparation, if needed, is separate from offline measurement.
|
||
Apply the measurement protocol's workspace, local-toolchain, and offline
|
||
environment settings to Go validation commands as well.
|
||
Tests and benchmarks added by this plan are development evidence, not new
|
||
release gates. Leave the opt-in ReleaseKit publication exercise unselected.
|
||
5. Record actual commands, results, fixture dimensions, relevant comparisons,
|
||
and unresolved issues in the stage's evidence field. Never replace pending
|
||
evidence with an expected result. Investigate a regression before proceeding;
|
||
absence of a timing speedup is not permission to invent a faster contract.
|
||
6. Review staged changes and commit each completed stage under the agent policy.
|
||
Report its commit, checks, and remaining work. Proceed only when its completion
|
||
conditions hold. Update the roadmap's overall status when implementation
|
||
begins and ends; detailed execution results belong here.
|
||
|
||
No concurrency guarantee, public freeze step, new parser, provider cache, library
|
||
version change, or release publication is needed. Application callbacks retain
|
||
their effects. Do not forbid registration from callbacks to simplify caching;
|
||
refresh metadata-dependent presentation when successful registration has made
|
||
the invocation's prepared metadata stale, without repeating input callbacks.
|
||
|
||
## Measurement protocol
|
||
|
||
Use standard Go benchmarks named with prefix `BenchmarkPerformance`. Use the
|
||
same fixtures and measurement scope before and after a change. Record Go version,
|
||
GOOS/GOARCH, CPU/machine description, source revision, and fixture dimensions.
|
||
Separate app construction plus first execution from execution on an existing
|
||
app. Exclude fixture construction from repeated-execution measurements and include
|
||
it only when explicitly measuring construction/registration.
|
||
|
||
Use in-memory provider fixtures and `io.Discard` for ordinary delivery
|
||
measurements. Observe outcomes so invalid fixtures cannot appear as fast success.
|
||
Actual ConfigKit/PromptKit loading is covered by existing integration tests;
|
||
these measurements isolate CommandKit overhead and do not contact providers.
|
||
Keep a few representative shapes rather than a Cartesian product of dimensions.
|
||
|
||
With module caches prepared, the common measurement command is:
|
||
|
||
```sh
|
||
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance' -benchmem -benchtime=200ms -count=5 . ./internal/cobra
|
||
```
|
||
|
||
Narrow `-bench` and the package list for each stage. Save raw output outside the
|
||
worktree and summarize representative ns/op, B/op, and allocs/op results here,
|
||
including variation or inconclusive differences. When measurements span a
|
||
different machine or Go version, repeat the baseline in an isolated checkout
|
||
with the same benchmark sources before making a timing claim. Profile CPU or
|
||
allocations only when needed to explain a remaining cost or regression. No new
|
||
benchmark-analysis dependency or permanent numeric pass threshold is selected.
|
||
|
||
## Stage inventory
|
||
|
||
| Stage | Deliverable | Roadmap coverage |
|
||
| --- | --- | --- |
|
||
| 1 | Dispatch and registration baseline benchmarks | PF-02, PF-03, PF-05 |
|
||
| 2 | Inspection and scope baseline benchmarks | PF-01, PF-04, PF-06 |
|
||
| 3 | PromptKit rendering for the selected format | PF-01 |
|
||
| 4 | Direct declaration lookup and ancestry traversal | PF-02 |
|
||
| 5 | YAML preamble analysis with complete framed delivery | PF-04, OD-2 |
|
||
| 6 | Registration without copying existing entries | PF-05 |
|
||
| 7 | Cached immutable bridge metadata | PF-03, OD-1 |
|
||
| 8 | An invocation-owned adapter session | PF-03, OD-1 |
|
||
| 9 | Root dispatch, help, and usage integration | PF-03, OD-1 |
|
||
| 10 | Prepared configuration suite metadata | PF-06 |
|
||
| 11 | Final behavioral/performance audit and documentation | PF-01–06 |
|
||
|
||
## Stage 1: establish dispatch and registration measurements
|
||
|
||
**Status:** complete. **Prerequisite:** orientation and this plan.
|
||
|
||
**Files:** new `performance_dispatch_test.go`,
|
||
`internal/cobra/performance_test.go`, and evidence here. Production code stays
|
||
unchanged in this stage.
|
||
|
||
1. Create small test-only command fixtures: a small flat registry with 8 custom
|
||
commands, a wide registry with 128 custom commands, and a nested path of
|
||
depth 24 with sibling commands. Add one flag-heavy leaf case with 24 flags.
|
||
These dimensions are benchmark inputs, not supported-size limits.
|
||
2. Add root benchmarks for repeated success, local `--help`, explicit
|
||
`help COMMAND`, parser usage failure, and handler-returned usage. Use the
|
||
small fixture for the full path set and the wider/deeper fixtures for success
|
||
and help. Add construction plus first-execution cases for small and wide apps.
|
||
Use real routing and bindings with a cheap typed handler and stable streams.
|
||
3. Add adapter `Parse` benchmarks for shallow/wide and deep neutral trees to
|
||
isolate PF-02 from root report work. Keep the neutral declarations outside
|
||
the timed region; include backend construction in the `Parse` measurement.
|
||
4. Add registration benchmarks comparing individual and batch registration of
|
||
8 and 128 preconstructed declarations. Include fresh `New` plus registration
|
||
in both timed paths so the comparison has the same construction overhead.
|
||
5. Run and record the baseline under the measurement protocol. Preserve benchmark
|
||
inputs for subsequent stages. Verify setup and expected outcome classes;
|
||
fix the fixture if it unexpectedly measures an error path.
|
||
|
||
**Validation:** compile both packages with `go test -mod=readonly -run '^$'
|
||
. ./internal/cobra`, then execute the new benchmarks. No timing assertions,
|
||
production instrumentation, or full dependency test matrix is needed.
|
||
|
||
**Completion:** reproducible baseline results distinguish registration, cold
|
||
execution, repeated execution, parsing, and help/usage costs.
|
||
|
||
**Evidence:** Benchmark fixtures were measured from source revision `1274ae7`
|
||
with the new benchmark files in the working tree (committed with this evidence).
|
||
Environment: Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
|
||
i5-7260U CPU @ 2.20 GHz. The required compile-only command passed:
|
||
`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' . ./internal/cobra`.
|
||
The five-sample measurement command also passed:
|
||
`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance' -benchmem -benchtime=200ms -count=5 . ./internal/cobra`.
|
||
Raw output is in `/tmp/commandkit-performance-baseline.txt`; focused repeat output
|
||
for noisy cases is in `/tmp/commandkit-performance-baseline-repeat.txt`.
|
||
|
||
Fixtures: root dispatch used 8 and 128 flat commands, a 24-level path with
|
||
siblings at each parent, and a leaf with 24 flags. Dispatch covered success,
|
||
local and explicit help, parser and handler usage, cold construction plus first
|
||
execution, and repeated execution. Registration used 8 and 128 prebuilt
|
||
declarations with fresh `New` in individual and batch paths. Adapter parsing
|
||
used 128 shallow siblings and the same depth-24 path. All repeated app and
|
||
adapter fixtures were built outside the timed loops; benchmark outcomes were
|
||
checked for their intended classes.
|
||
|
||
Representative five-sample ranges (ns/op; B/op; allocs/op): small repeated
|
||
success 31,066–41,041; 33,209–33,210; 287. Small local help 103,022–116,183;
|
||
78,176–78,177; 810. Parser usage 62,596–74,475; 66,363–66,364; 591. Wide
|
||
success 218,073–253,010; 269,063–269,070; 1,898. Deep success 119,958–139,625;
|
||
139,787–139,791; 1,074. Wide help 491,172–622,188; 549,923–549,940; 4,033.
|
||
Explicit help measured 101,998–111,895 ns/op, 78,152–78,153 B/op, and 815
|
||
allocs/op; handler usage measured 61,878–72,264 ns/op, 66,211–66,212 B/op, and
|
||
583 allocs/op. Cold construction plus first execution measured 38,152–50,235
|
||
ns/op, 38,275–38,276 B/op, and 339 allocs/op for the small app, and
|
||
251,945–315,276 ns/op, 291,822–291,827 B/op, and 2,184 allocs/op for the wide
|
||
app. The 24-flag leaf focused repeat was 43,662–56,492 ns/op, 40,027–40,028
|
||
B/op, and 401 allocs/op.
|
||
Deep help was noisy in the full run (429,873–1,634,363 ns/op); a focused repeat
|
||
was 300,155–326,094 ns/op, 294,882–294,896 B/op, and 2,408 allocs/op. The initial
|
||
flag-heavy timing also varied widely before the focused range reported above.
|
||
Registration at 8 declarations measured individual 7,141–7,645 ns/op,
|
||
5,497 B/op, 64 allocs/op versus batch 4,159–5,190 ns/op, 3,456 B/op,
|
||
48 allocs/op. At 128 declarations, individual measured 414,618–492,261 ns/op,
|
||
405,011–405,018 B/op, 784 allocs/op versus batch 33,985–36,207 ns/op,
|
||
23,013–23,014 B/op, 288 allocs/op. Adapter parse measured shallow/wide
|
||
150,918–171,598 ns/op, 242,936 B/op, 1,705 allocs/op, and deep
|
||
86,282–109,697 ns/op, 119,920 B/op, 827 allocs/op. These are baselines, not
|
||
speedup claims; the large initial deep-help and flag-heavy timing variation
|
||
shows why later comparisons should report run variation.
|
||
|
||
## Stage 2: establish report and scope measurements
|
||
|
||
**Status:** complete. **Prerequisite:** Stage 1.
|
||
|
||
**Files:** new `performance_reports_test.go` and evidence here. Production code
|
||
stays unchanged in this stage.
|
||
|
||
1. Use direct `PromptKitSuite` callbacks returning fixed inspection records.
|
||
Measure prompt text/YAML with 4 and 128 inputs, and profile text/YAML with
|
||
small and larger nested `ExtraParams` maps. Construct fixtures outside the
|
||
timed loop; use `App.Execute` so parsing and delivery are represented.
|
||
2. Add isolated framing and aggregate `config show --scope all` benchmarks with
|
||
two native document fixtures: approximately 4 KiB and 1 MiB with many short
|
||
lines. Keep native content and provider callbacks fixed. Include an ordinary
|
||
preamble and a directive-bearing preamble, without multiplying every size by
|
||
every marker variation. Measure single-scope display as a reference case.
|
||
3. Measure `config scopes`, named selection, and aggregate validation with 2 and
|
||
32 scopes. Give every aggregate fixture the requested capability and use cheap
|
||
acquisition callbacks. Add suite registration with 4 and 64 distinct selectors
|
||
to expose duplicate-check cost without using duplicate declarations.
|
||
4. Run and record these baselines before handler or scope changes. Label isolated
|
||
helper results separately from complete command execution. Adjust internal
|
||
benchmark calls after later signature refactors only as necessary to measure
|
||
the same logical operation and fixtures.
|
||
|
||
**Validation:** compile the root package and run the new report/scope benchmarks
|
||
under the common protocol. Confirm the fixtures produce the intended formats and
|
||
success outcomes outside timed setup; existing integration coverage remains the
|
||
owner of actual library loading semantics.
|
||
|
||
**Completion:** PF-01, PF-04, and PF-06 have comparable baseline measurements.
|
||
|
||
**Evidence:** Fixtures were measured from source revision `aed4c59` with
|
||
`performance_reports_test.go` in the working tree (committed with this evidence).
|
||
Environment matched the first baseline: Go 1.26.8, linux/amd64, Linux
|
||
6.17.0-41-generic, Intel Core i5-7260U CPU @ 2.20 GHz. Root package compilation
|
||
passed with `GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' .`. The five-sample benchmark command passed:
|
||
`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance(PromptReports|YAMLFraming|ConfigScopes|ConfigRegistration)$' -benchmem -benchtime=200ms -count=5 .`.
|
||
Raw output is in `/tmp/commandkit-performance-reports-baseline.txt`.
|
||
|
||
Fixtures: PromptKit callbacks returned prebuilt inspection records directly;
|
||
prompt reports used 4 and 128 input records, while profile `ExtraParams` used
|
||
2 groups with 3 nested values each or 24 groups with 8 nested values each and a
|
||
nested sequence. Each was measured as text and YAML through `App.Execute`.
|
||
Native YAML fixtures contain unique short mapping keys and are 4,096 bytes with
|
||
an ordinary comment preamble and 1,048,591 bytes with a `%YAML` directive
|
||
preamble. Framing was measured directly; aggregate `config show --scope all`
|
||
measured two documents of each selected fixture size/style, and single-scope
|
||
display used the 4 KiB ordinary document. Scope operations used 2 or 32 scopes
|
||
with the requested show/validate capabilities and cheap in-memory acquisition
|
||
callbacks. Suite registration used 4 or 64 distinct selectors and a fresh app.
|
||
The scope counts also covered discovery, named validation, and aggregate
|
||
validation. Setup verified success and report shape outside timed loops; native
|
||
fixtures, records, suites, and apps were prepared outside repeated loops.
|
||
|
||
Representative five-sample ranges (ns/op; B/op; allocs/op): prompt, 4 inputs,
|
||
text 41,287–52,115; 26,160–26,161; 294, and YAML 88,991–104,671;
|
||
74,730–74,733; 557. Prompt, 128 inputs, text 169,155–198,281;
|
||
145,524–145,531; 1,171, and YAML 1,418,692–1,490,181; 1,393,508–1,393,568;
|
||
6,039–6,040. Profile, small extra map, text 74,109–89,337; 70,914–70,918;
|
||
462, and YAML 161,470–368,813; 162,688–162,717; 755. Profile, large extra map,
|
||
text 1,277,396–2,583,950; 836,141–836,296; 3,027–3,028, and YAML
|
||
1,756,571–2,377,499; 1,586,728–1,586,849; 5,878–5,879. Larger profile timings
|
||
varied, while allocation counts and bytes were stable.
|
||
|
||
Direct framing measured 4 KiB ordinary at 5,883–6,842 ns/op, 11,056 B/op,
|
||
3 allocs/op, and 1 MiB directive at 1,208,678–1,502,417 ns/op,
|
||
2,539,520–2,539,523 B/op, 2 allocs/op. Aggregate show measured 4 KiB documents
|
||
at 44,395–48,161 ns/op, 55,537–55,538 B/op, 211 allocs/op, and 1 MiB documents
|
||
at 4,818,907–6,009,877 ns/op, 8,695,182–8,695,197 B/op, 215–216 allocs/op.
|
||
Single-scope 4 KiB display measured 20,229–21,791 ns/op, 17,935–17,936 B/op,
|
||
183 allocs/op. With 2 scopes, discovery measured 23,840–30,007 ns/op,
|
||
21,161–21,162 B/op, 223 allocs/op; named validation 24,841–26,340 ns/op,
|
||
21,817 B/op, 228 allocs/op; aggregate validation 25,231–31,126 ns/op,
|
||
22,361–22,362 B/op, 237 allocs/op. With 32 scopes, discovery measured
|
||
31,439–37,167 ns/op, 28,141–28,142 B/op, 257 allocs/op; named validation
|
||
29,123–74,374 ns/op, 22,775–22,778 B/op, 228 allocs/op; aggregate validation
|
||
45,548–63,471 ns/op, 29,781–29,782 B/op, 269 allocs/op. Registration with 4
|
||
selectors measured 12,573–14,789 ns/op, 8,911 B/op, 104 allocs/op; with 64 it
|
||
measured 91,675–105,548 ns/op, 82,817–82,818 B/op, 485 allocs/op. Large-report
|
||
cost includes full output framing/delivery; no segmented-delivery comparison
|
||
was selected. These measurements establish baselines and show timing variation;
|
||
they are not fixed thresholds or speedup claims.
|
||
|
||
## Stage 3: render only the selected PromptKit format
|
||
|
||
**Status:** complete. **Prerequisite:** Stage 2.
|
||
|
||
**Files:** [promptkit.go](../../promptkit.go),
|
||
[promptkit_test.go](../../promptkit_test.go), relevant report benchmarks, and
|
||
evidence here.
|
||
|
||
1. In `runPromptKitPrompt` and `runPromptKitProfile`, select YAML or text before
|
||
calling either text formatter. Preserve report DTO construction and all
|
||
acquisition, cancellation, nil-result, and diagnostic handling.
|
||
2. For YAML, encode into a `bytes.Buffer` and use its bytes directly. For text,
|
||
format once and convert the completed string to bytes without copying it
|
||
through an additional builder. Preserve profile text-formatting errors.
|
||
3. Reduce `writePromptKitReport` to shared delivery of completed bytes plus
|
||
diagnostics. Keep the pre-delivery cancellation check, checked writer, and
|
||
incomplete-report outcome handling. Both formats finish rendering before
|
||
this delivery step. No lazy public formatter API is needed.
|
||
4. Reuse the real inspection tests for report fields, input order, nil parameter
|
||
maps, nested `ExtraParams`, cancellation, and output failures. Add focused
|
||
missing cases rather than collaborator-call assertions or broad snapshots.
|
||
5. Rerun prompt/profile measurements and compare them with Stage 2. Explain any
|
||
text-path regression or unexpectedly unchanged YAML allocation result.
|
||
|
||
**Validation:** root PromptKit inspection and cancellation tests, followed by
|
||
the applicable report benchmarks. Use `go test -mod=readonly -count=1 .` for
|
||
root behavioral validation. Preserve public error identity and existing
|
||
report confidentiality; this change introduces no redaction policy.
|
||
|
||
**Completion:** YAML requests do not construct text reports, and text delivery
|
||
has no redundant intermediate builder.
|
||
|
||
**Evidence:** `go test -mod=readonly -count=1 .` passed with the workspace,
|
||
local toolchain, and offline environment settings from the measurement protocol.
|
||
The same five-sample report fixtures passed with:
|
||
`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformancePromptReports$' -benchmem -benchtime=200ms -count=5 .`.
|
||
The environment matched Stage 2: Go 1.26.8, linux/amd64, Linux
|
||
6.17.0-41-generic, Intel Core i5-7260U CPU @ 2.20 GHz. The measured source was
|
||
revision `a431540` plus the `promptkit.go` working-tree change committed with
|
||
this evidence. Raw output is in `/tmp/commandkit-performance-prompt-after.txt`.
|
||
|
||
Every five-sample report case reduced allocation count. Prompt, 4 inputs, text
|
||
changed from 41,287–52,115 ns/op, 26,160–26,161 B/op, 294 allocs/op to
|
||
29,898–35,259 ns/op, 25,280–25,282 B/op, 291 allocs/op; YAML changed from
|
||
88,991–104,671 ns/op, 74,730–74,733 B/op, 557 allocs/op to 83,454–91,286
|
||
ns/op, 71,853–71,855 B/op, 505 allocs/op. Prompt, 128 inputs, text changed
|
||
from 169,155–198,281 ns/op, 145,524–145,531 B/op, 1,171 allocs/op to
|
||
161,185–185,023 ns/op, 128,960–128,966 B/op, 1,168 allocs/op; YAML changed
|
||
from 1,418,692–1,490,181 ns/op, 1,393,508–1,393,568 B/op, 6,039–6,040
|
||
allocs/op to 1,255,623–1,360,926 ns/op, 1,264,474–1,264,486 B/op, 5,104
|
||
allocs/op.
|
||
|
||
Profile, small extra map, text changed from 74,109–89,337 ns/op,
|
||
70,914–70,918 B/op, 462 allocs/op to 75,818–88,197 ns/op, 69,955–69,959
|
||
B/op, 459 allocs/op; the timing ranges overlap. YAML changed from
|
||
161,470–368,813 ns/op, 162,688–162,717 B/op, 755 allocs/op to 107,244–127,744
|
||
ns/op, 114,793–114,795 B/op, 533 allocs/op. Profile, large extra map, text
|
||
changed from 1,277,396–2,583,950 ns/op, 836,141–836,296 B/op, 3,027–3,028
|
||
allocs/op to 839,692–936,229 ns/op, 826,407–826,448 B/op, 3,024 allocs/op;
|
||
YAML changed from 1,756,571–2,377,499 ns/op, 1,586,728–1,586,849 B/op,
|
||
5,878–5,879 allocs/op to 796,779–976,685 ns/op, 780,391–780,436 B/op,
|
||
3,086 allocs/op. These results confirm that YAML requests avoid text-report
|
||
construction; text requests retain their report semantics and formatting errors.
|
||
|
||
## Stage 4: index backend declarations and traverse parents
|
||
|
||
**Status:** complete. **Prerequisite:** Stage 3.
|
||
|
||
**Files:** [adapter](../../internal/cobra/parse.go),
|
||
[adapter tests](../../internal/cobra/parse_test.go), relevant benchmarks, and
|
||
evidence here.
|
||
|
||
1. Change the construction index to map backend command pointers directly to
|
||
neutral declarations. Update `Parse`, `RenderHelp`, and `UsageLine` to use
|
||
direct lookup instead of scanning declaration-to-backend entries.
|
||
2. Reconstruct a selected path by walking the backend's `Parent()` links and
|
||
reading each mapped declaration's name, excluding the root. Reverse the
|
||
collected names once. Remove recursive root-based parent discovery.
|
||
3. Keep neutral failures for missing trees/unmapped selections and exact-path
|
||
verification. Do not rely on Cobra prefix matching to define accepted paths.
|
||
4. Extend adapter coverage only where needed for a deep path, wide siblings,
|
||
unknown paths, root selection, and help/usage path resolution. Existing
|
||
parsing and flag-state tests remain applicable.
|
||
5. Compare adapter and complete dispatch benchmarks with Stage 1. The tree still
|
||
rebuilds here; distinguish reduced path-search cost from that remaining cost.
|
||
|
||
**Validation:** `go test -mod=readonly -count=1 ./internal/cobra .`, then the applicable
|
||
parse and dispatch benchmarks.
|
||
|
||
**Completion:** declaration lookup uses the index and ancestry traversal is
|
||
proportional to selected depth after backend construction.
|
||
|
||
**Evidence:** `go test -mod=readonly -count=1 ./internal/cobra .` passed with
|
||
the common offline environment settings. The focused test covers a 24-level
|
||
path, 128 siblings, root selection, unknown help/usage paths, and exact help and
|
||
usage resolution. Existing exact-path and parse failure tests remain in place.
|
||
|
||
Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
|
||
i5-7260U CPU @ 2.20 GHz, based on source revision `628dc78` with this stage's
|
||
changes in the working tree. The five-sample adapter command was
|
||
`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformanceParse$' -benchmem -benchtime=200ms -count=5 ./internal/cobra`;
|
||
the dispatch command used the same flags with `-bench
|
||
'^BenchmarkPerformanceDispatch$' .`. Both passed. Raw outputs are in
|
||
`/tmp/commandkit-performance-parse-after-stage4.txt` and
|
||
`/tmp/commandkit-performance-dispatch-after-stage4.txt`.
|
||
|
||
Adapter shallow/wide Parse measured 149,426–171,199 ns/op,
|
||
242,936 B/op, and 1,705 allocs/op, compared with baseline 150,918–171,598
|
||
ns/op, 242,936 B/op, and 1,705 allocs/op. Deep Parse measured 90,608–104,770
|
||
ns/op, 119,536 B/op, and 826 allocs/op, compared with baseline 86,282–109,697
|
||
ns/op, 119,920 B/op, and 827 allocs/op. Timings overlap; deep parsing saves
|
||
384 B and one allocation. End-to-end dispatch allocations remain effectively
|
||
unchanged across the measured cases. Timing variation, including deep-help
|
||
outliers, is substantial, so these runs do not support an end-to-end speedup
|
||
claim. The comparison isolates the selected-path lookup change while backend
|
||
tree construction remains in each parse.
|
||
|
||
## Stage 5: scan YAML preambles without splitting documents
|
||
|
||
**Status:** complete. **Prerequisite:** Stage 4.
|
||
|
||
**Files:** [show.go](../../show.go), [show_test.go](../../show_test.go),
|
||
[configkit_test.go](../../configkit_test.go), framing benchmarks, and evidence.
|
||
|
||
1. Introduce a private preamble analysis using `bytes.IndexByte` to locate line
|
||
boundaries and byte trimming for classification. Track marker positions,
|
||
preamble end, directives, and inline marker content. Stop when the opening
|
||
framing decision is complete. Preserve Unicode whitespace behavior and
|
||
accepted space/tab marker forms; do not add a general YAML parser or limits.
|
||
2. Have `frameEffectiveYAML` return complete framed bytes and a directive flag
|
||
from that analysis. Update its internal callers/tests for the result pair.
|
||
Remove the independent full-document split in `hasYAMLDirective` and use the
|
||
same analysis for aggregate completion-marker decisions.
|
||
3. Retain the complete native buffer and framed buffer per document. Preserve
|
||
placement of labels around inline content/block scalars, authored comments,
|
||
directives, native byte slices, quoted scope labels, and boundary newlines.
|
||
Keep the existing aggregate document-end marker where directives require it.
|
||
4. Leave acquisition-before-output, direct single-scope writing, cancellation
|
||
boundaries, and write-error outcomes intact. Do not introduce segmented native
|
||
delivery, indentation wrappers, or reserialization of provider documents.
|
||
5. Reuse `TestEffectiveYAMLFramingPreservesOneNativeDocument` and aggregate
|
||
acquisition/delivery tests. Fill meaningful gaps for absent trailing newline,
|
||
CRLF, directive preambles, and control characters in labels. Verify native
|
||
content preservation as well as decoded values and document count.
|
||
6. Compare framing/aggregate measurements with Stage 2, emphasizing allocation
|
||
growth with line count. Report the full framed copy as an intentional retained
|
||
cost under OD-2.
|
||
|
||
**Validation:** root framing/configuration behavior with
|
||
`go test -mod=readonly -count=1 .`, then the relevant report benchmarks.
|
||
|
||
**Completion:** each document has one opening analysis, no document-wide line
|
||
index, and the selected complete-buffer delivery model.
|
||
|
||
**Evidence:** `GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none
|
||
GOSUMDB=off go test -mod=readonly -count=1 .` passed. Existing aggregate tests
|
||
continue to cover acquisition-before-output, directive document separators,
|
||
decoded values, diagnostics, and writer failures. New framing cases cover a
|
||
missing final newline, CRLF, a directive preamble, a control character in the
|
||
scope label, unchanged native bytes, decoded values, and one-document output.
|
||
|
||
Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
|
||
i5-7260U CPU @ 2.20 GHz, based on source revision `ae656d8` with this stage's
|
||
changes in the working tree. The five-sample command passed:
|
||
`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformanceYAMLFraming$' -benchmem -benchtime=200ms -count=5 .`.
|
||
Raw output is in `/tmp/commandkit-performance-yaml-after-stage5.txt`.
|
||
|
||
Direct 4 KiB ordinary framing measured 1,135–1,455 ns/op, 4,864 B/op, and one
|
||
allocation, compared with 5,883–6,842 ns/op, 11,056 B/op, and three allocations
|
||
at baseline. Direct 1 MiB directive framing measured 189,303–359,655 ns/op,
|
||
1,056,768–1,056,769 B/op, and one allocation, compared with 1,208,678–1,502,417
|
||
ns/op, 2,539,520–2,539,523 B/op, and two allocations. Aggregate 4 KiB show
|
||
measured 29,951–49,108 ns/op, 36,999–37,001 B/op, and 206 allocations, compared
|
||
with 44,395–48,161 ns/op, 55,537–55,538 B/op, and 211 allocations. Aggregate
|
||
1 MiB show measured 1,326,104–1,557,880 ns/op, 4,246,730–4,246,767 B/op, and
|
||
211 allocations, compared with 4,818,907–6,009,877 ns/op, 8,695,182–8,695,197
|
||
B/op, and 215–216 allocations. Single-scope 4 KiB display remained at
|
||
17,935–17,936 B/op and 183 allocations; timing variation overlaps the baseline.
|
||
The complete framed output still requires a full-size copy under OD-2. The
|
||
allocation reduction comes from eliminating document-wide split/index storage
|
||
and reusing the opening analysis for framing and aggregate directive handling.
|
||
|
||
## Stage 6: install validated registrations without copying the registry
|
||
|
||
**Status:** not started. **Prerequisite:** Stage 5.
|
||
|
||
**Files:** [registry.go](../../registry.go),
|
||
[registry_test.go](../../registry_test.go), registration benchmarks, and evidence.
|
||
|
||
1. Preserve the nil-app check. Validate each declaration and check its name
|
||
against both existing registrations and pending entries. Clone declarations
|
||
into private pending storage before modifying the app.
|
||
2. After the whole batch succeeds, initialize the app's map if needed and add
|
||
the pending entries to it. Leave existing entries untouched. An empty batch
|
||
remains a successful no-op on a non-nil app.
|
||
3. Preserve batch failure behavior for invalid later declarations, duplicates
|
||
within a batch, existing paths, and reserved help/version paths. Keep existing
|
||
cloning boundaries rather than sharing caller-owned child slices.
|
||
4. Reuse atomicity and callback-free registration tests. Add a missing late-failure
|
||
or within-batch collision case only if existing coverage is insufficient.
|
||
5. Compare individual/batch registration with Stage 1. Sorting and metadata
|
||
preparation remain separate until Stage 7.
|
||
|
||
**Validation:** registry/declaration behavior with
|
||
`go test -mod=readonly -count=1 .`, then registration benchmarks.
|
||
|
||
**Completion:** registration copies only pending declarations and preserves
|
||
all-or-nothing validation without rebuilding the existing registry map.
|
||
|
||
**Evidence:** pending.
|
||
|
||
## Stage 7: cache immutable bridge metadata per app
|
||
|
||
**Status:** not started. **Prerequisite:** Stage 6.
|
||
|
||
**Files:** [app.go](../../app.go), [parser.go](../../parser.go), registry invalidation,
|
||
root parser/registry tests, dispatch benchmarks, and evidence.
|
||
|
||
1. Add a private cached neutral tree pointer to `App`. Split bridge translation
|
||
into a cache accessor and an uncached builder. Build lazily on first use;
|
||
sort root names and child lists once per registry state while creating owned
|
||
flag/operand metadata. The cached neutral tree is read-only thereafter.
|
||
2. Invalidate the pointer at successful nonempty registration's completion point.
|
||
Failed and empty registrations retain valid cached metadata. Built-in
|
||
construction remains before first use; do not create metadata eagerly for
|
||
each individual command registration.
|
||
3. Keep this cache free of contexts, streams, parsed values, provider views, and
|
||
Cobra/pflag objects. Adapter construction reads the neutral tree without
|
||
mutating it. A pointer to the neutral tree identifies a prepared metadata
|
||
generation for Stage 9, avoiding a separate counter.
|
||
4. Protect observable behavior after success, failure, and empty registration
|
||
following earlier execution/help. Check that newly installed configuration
|
||
and PromptKit families appear after an earlier help request without provider
|
||
loading. Preserve owned declarations and sorted metadata.
|
||
5. Compare cold and repeated execution with Stage 1. Account for the one retained
|
||
neutral tree per app and confirm rebuilding occurs only when needed through
|
||
source review and measurements, not public allocation/count assertions.
|
||
|
||
**Validation:** root registry/parser/help tests and dispatch benchmarks. Each
|
||
invocation still creates fresh backend values at this stage. Use
|
||
`go test -mod=readonly -count=1 .` for root behavioral validation.
|
||
|
||
**Completion:** unchanged registrations reuse translated metadata; successful
|
||
registration is visible to later parsing and help.
|
||
|
||
**Evidence:** pending.
|
||
|
||
## Stage 8: introduce an invocation-owned adapter session
|
||
|
||
**Status:** not started. **Prerequisite:** Stage 7.
|
||
|
||
**Files:** the adapter, its tests/benchmarks, and evidence. Root execution remains
|
||
functional through the existing package-level adapter helpers during this stage.
|
||
|
||
1. Add an internal `Session` handle with private backend root, neutral metadata,
|
||
and reverse declaration index. Use `Prepare(*Command, ParseOptions) *Session`;
|
||
construction normalizes options and builds a fresh mutable tree. A missing
|
||
tree is represented by a session whose operations return neutral failures.
|
||
Methods are `Parse([]string) Result`, `RenderHelp([]string) (string, *Failure)`,
|
||
and `UsageLine([]string) (string, *Failure)`. They expose no backend types.
|
||
2. Move existing parse/help/usage logic into those methods. Reuse the prepared
|
||
tree and Stage 4's index/path traversal. Help renders to a private buffer;
|
||
temporarily redirect and restore the selected backend's output/error streams
|
||
so generated bytes remain under root delivery control.
|
||
3. Keep package-level helpers as thin one-session conveniences for current root
|
||
calls and focused adapter benchmarks/tests. They must delegate to the same
|
||
methods, not maintain parallel parsing or help implementations.
|
||
4. Keep malformed/nil-tree results neutral. Metadata remains read-only; parsed
|
||
values belong to the session. Test parse followed by help and usage on one
|
||
session, and fresh values/streams on separately prepared sessions. Do not
|
||
add a public session API, pool, persistent backend cache, or reset framework.
|
||
5. Add a small parse-plus-help session benchmark using an existing fixture.
|
||
Label it as a new internal measurement; existing end-to-end benchmarks are
|
||
still the before/after evidence for root integration in Stage 9.
|
||
|
||
**Validation:** `go test -mod=readonly -count=1 ./internal/cobra .`, then adapter benchmarks.
|
||
|
||
**Completion:** the adapter can perform parsing and metadata presentation on
|
||
one prepared invocation tree while legacy root entry points still compile.
|
||
|
||
**Evidence:** pending.
|
||
|
||
## Stage 9: reuse the adapter session throughout root execution
|
||
|
||
**Status:** not started. **Prerequisite:** Stage 8.
|
||
|
||
**Files:** [execute.go](../../execute.go), [builtins.go](../../builtins.go),
|
||
private parser coordination, [execute_external_test.go](../../execute_external_test.go),
|
||
dispatch benchmarks, and evidence.
|
||
|
||
1. Introduce private execution coordination local to `App.Execute`, retaining
|
||
the app, parse options, neutral tree pointer, and session. Prepare after the
|
||
existing context/stream checks. Keep early nil-app/context outcomes and
|
||
generic usage fallbacks functional without a usable session.
|
||
2. Route parse, root/group/local help, usage after parse/input failures, and
|
||
usage returned by handlers through that local coordination. Keep the existing
|
||
input preparation and callback order, including supplied-value validation
|
||
for help. Normal domain handlers still receive the public `Invocation`.
|
||
3. Handle the reserved top-level `help` command explicitly in the dispatcher
|
||
after its ordinary typed preparation/checks: use its private `helpInput`,
|
||
resolve the target with `helpCommandTarget`, and render through the local
|
||
session. Share target/render logic with its registered built-in handler.
|
||
A one-off private convenience may prepare a session for direct internal
|
||
helper calls; the `Execute` path must use its existing session. Preserve
|
||
runnable help metadata and the semantics of `help --help` and unknown targets.
|
||
4. Do not store the active session on `App`, add it to public `Invocation`, or
|
||
put it in caller contexts. For help/usage presentation, compare the current
|
||
cached metadata pointer with the session's pointer. If successful registration
|
||
by a callback changed metadata, prepare presentation against the new tree
|
||
without rerunning parsing, factories, checks, or handlers. This preserves
|
||
registration freedom without caching stale presentation.
|
||
5. Preserve outcome normalization, cause/error identity, diagnostic ownership,
|
||
delivery errors, and incomplete-report reporting. Remove obsolete duplicate
|
||
preparation paths once all root execution branches are integrated.
|
||
6. Reuse the existing help-entry and fresh-invocation tests. Fill gaps for a
|
||
successful flag-bearing invocation followed by help and omitted/false/empty
|
||
values, distinct streams/contexts, handler usage, and registration followed
|
||
by metadata presentation. Test contracts, not exact session-construction counts.
|
||
7. Compare complete help/error/repeated/cold execution with Stage 1. Stable
|
||
metadata should need one backend preparation through each ordinary invocation,
|
||
including explicit `help`; verify structurally and through measurements.
|
||
|
||
**Validation:** root dispatcher/help/outcome/cancellation tests, adapter tests,
|
||
with `go test -mod=readonly -count=1 . ./internal/cobra`, then complete dispatch
|
||
benchmarks. Use existing facade boundary coverage/review
|
||
to confirm that signatures and backend isolation are unchanged.
|
||
|
||
**Completion:** every normal `Execute` path uses the selected OD-1 lifecycle and
|
||
all existing public handler and presentation semantics remain intact.
|
||
|
||
**Evidence:** pending.
|
||
|
||
## Stage 10: prepare configuration scope metadata once
|
||
|
||
**Status:** not started. **Prerequisite:** Stage 9.
|
||
|
||
**Files:** [config.go](../../config.go), [selectors.go](../../selectors.go),
|
||
configuration handler files, configuration/cancellation tests, scope benchmarks,
|
||
and evidence. Keep public `ConfigSuite`/`ConfigScope` unchanged.
|
||
|
||
1. Introduce private `preparedConfigSuite[C]` with the normalized public suite,
|
||
ordered scope slice, `map[string]ConfigScope[C]` lookup, and discovery DTO.
|
||
Prepare it once in `RegisterConfig` after normalization. Use the existing
|
||
ordering comparator: `app`, `promptkit`, then other identifiers alphabetically.
|
||
Normalize/copy metadata before preparation so it remains instance-owned.
|
||
2. Replace the previous-selector loop in `normalizeConfigSuite` with seen-name
|
||
and seen-short maps, retaining validation order, scope-reference deduplication,
|
||
and the same collision categories. Preserve all existing naming rules.
|
||
3. Pass the private prepared suite into standard command constructors and
|
||
preflight/run helpers. Use its ordered slice for aggregate selection and
|
||
its map for named lookup, capability preflight, and acquisition. Update
|
||
validate/show/sources/explain together; no public integration signature changes.
|
||
4. Construct the discovery inventory from declared/installed operations during
|
||
registration and retain the private DTO. Encode text/YAML at invocation time.
|
||
This preparation must not call `NewContext`, acquisition, input checking,
|
||
native writers, or other runtime callbacks.
|
||
5. Keep selection validation in preflight and handlers, now using prepared
|
||
metadata. Keep selector applicability scans and fresh context projection.
|
||
Do not add mutable selected-scope memoization or public/internal input hooks
|
||
solely to avoid a second inexpensive validation pass.
|
||
6. Reuse discovery ordering/capability, selector projection/requiredness, unknown
|
||
scope, provider-error, and cancellation coverage. Ensure aggregate operations
|
||
check every selected capability before acquisition; discovery remains
|
||
metadata-only. Add only missing post-registration ownership/collision cases.
|
||
7. Compare scope execution and suite registration with Stage 2, including the
|
||
normal two-scope case. Record the retained slice/map/DTO cost and any timing
|
||
tradeoff; extra metadata is private to the registered suite.
|
||
|
||
**Validation:** root configuration and cancellation tests plus scope/selector
|
||
benchmarks. Use `go test -mod=readonly -count=1 .` for root behavioral validation.
|
||
Preserve native reporting and per-operation output timing.
|
||
|
||
**Completion:** scope sorting/discovery preparation occurs at registration,
|
||
scope lookup is indexed, and duplicate selector checks are linear in selector
|
||
count plus declared scope references.
|
||
|
||
**Evidence:** pending.
|
||
|
||
## Stage 11: audit the completed target and reconcile documentation
|
||
|
||
**Status:** not started. **Prerequisite:** Stages 1–10.
|
||
|
||
**Files:** this plan, `performance.md`,
|
||
[internal overview](../internal/overview.md), developer/README navigation,
|
||
and stale references to the retired ReleaseKit plan. Focused corrections only
|
||
where the audit proves an incomplete target.
|
||
|
||
1. Audit PF-01–06 against source and observed behavior: selected formatting,
|
||
indexed ancestry, prefix-only YAML analysis, full framed buffering, pending
|
||
registrations, immutable cache invalidation, invocation sessions across all
|
||
help/usage paths, and prepared configuration metadata. Identify authoritative
|
||
evidence for each target rather than infer completion from stage statuses.
|
||
2. Run the default suite once for the complete sprint:
|
||
|
||
```sh
|
||
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -count=1 ./...
|
||
```
|
||
|
||
Keep live services, the optional release publication exercise, and additional
|
||
race/platform/coverage gates unselected. Review production imports and exported
|
||
declarations using existing boundary checks and source evidence.
|
||
3. Rerun representative benchmarks with unchanged fixtures and comparable
|
||
environment. Summarize before/after data by PF ID, separate cold/repeated and
|
||
isolated/whole-command costs, and record remaining full-tree/full-buffer work.
|
||
Reuse a recent identical measurement when source and environment are unchanged.
|
||
Explain material regressions or collect more evidence before declaring success.
|
||
4. Update internal documentation to describe implemented cache/session ownership
|
||
and prepared metadata. Current CLI/GoDoc contracts should remain unchanged;
|
||
update them only for an actual clarification without inventing a new promise.
|
||
5. Update README and `docs/development.md` so current implementation navigation
|
||
points to this performance plan and roadmap. In `releasekit.md`, `initial.md`,
|
||
and `remediation.md`, remove or replace obsolete ReleaseKit-plan references
|
||
with the owning completed roadmap/runbook as appropriate. Remove its obsolete
|
||
final-stage fragment references. Do not preserve the retired stage assignments
|
||
or restart completed release work.
|
||
6. Check affected document links/fragments, prose status, and whitespace. Mark
|
||
the roadmap implemented only when every selected target has evidence. Record
|
||
the stage commits, measurement revisions, check results, and any qualifications
|
||
here, then commit the audit and leave a clean workspace.
|
||
|
||
**Validation:** the default suite, comparative measurements, facade/import
|
||
review, and affected documentation/link checks described above. Reuse unchanged
|
||
stage evidence where it directly proves the final target.
|
||
|
||
**Completion:** all six target improvements and both selected approaches are
|
||
implemented with relevant behavioral and efficiency evidence; maintained docs
|
||
identify their current owners. No performance guarantee or additional release
|
||
gate has been introduced.
|
||
|
||
**Evidence:** pending.
|