Cache immutable command bridge metadata
This commit is contained in:
7
app.go
7
app.go
@@ -1,11 +1,16 @@
|
|||||||
package commandkit
|
package commandkit
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
// App owns one command registry and its application identity.
|
// App owns one command registry and its application identity.
|
||||||
type App struct {
|
type App struct {
|
||||||
identity Identity
|
identity Identity
|
||||||
commands map[string]*commandNode
|
commands map[string]*commandNode
|
||||||
|
commandTree *internalcobra.Command
|
||||||
configRegistered bool
|
configRegistered bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Performance implementation plan
|
# Performance implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–6 are complete, Stages 7–11 have not started.
|
Status: implementation in progress; Stages 1–7 are complete, Stages 8–11 have not started.
|
||||||
|
|
||||||
Date: 2026-10-09.
|
Date: 2026-10-09.
|
||||||
|
|
||||||
@@ -549,7 +549,7 @@ issues remain open.
|
|||||||
|
|
||||||
## Stage 7: cache immutable bridge metadata per app
|
## Stage 7: cache immutable bridge metadata per app
|
||||||
|
|
||||||
**Status:** not started. **Prerequisite:** Stage 6.
|
**Status:** complete. **Prerequisite:** Stage 6.
|
||||||
|
|
||||||
**Files:** [app.go](../../app.go), [parser.go](../../parser.go), registry invalidation,
|
**Files:** [app.go](../../app.go), [parser.go](../../parser.go), registry invalidation,
|
||||||
root parser/registry tests, dispatch benchmarks, and evidence.
|
root parser/registry tests, dispatch benchmarks, and evidence.
|
||||||
@@ -581,7 +581,37 @@ invocation still creates fresh backend values at this stage. Use
|
|||||||
**Completion:** unchanged registrations reuse translated metadata; successful
|
**Completion:** unchanged registrations reuse translated metadata; successful
|
||||||
registration is visible to later parsing and help.
|
registration is visible to later parsing and help.
|
||||||
|
|
||||||
**Evidence:** pending.
|
**Evidence:** `GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none
|
||||||
|
GOSUMDB=off go test -mod=readonly -count=1 .` passed. The new behavioral test
|
||||||
|
builds help metadata, registers a command and both standard command families,
|
||||||
|
then confirms later help and parsing see them without invoking provider
|
||||||
|
callbacks. It also checks visibility after failed and empty registration.
|
||||||
|
Source review confirms the tree is built lazily, contains only neutral
|
||||||
|
declarations/flag/operand metadata, and is cleared only after successful
|
||||||
|
nonempty registration. Root and child ordering is performed by the uncached
|
||||||
|
builder once per metadata generation.
|
||||||
|
|
||||||
|
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 `5816dd3` 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 '^BenchmarkPerformanceDispatch$' -benchmem -benchtime=200ms -count=5 .`.
|
||||||
|
Raw output is in `/tmp/commandkit-performance-dispatch-after-stage7.txt`.
|
||||||
|
|
||||||
|
Repeated small success changed from 31,066–41,041 ns/op, 33,209–33,210 B/op,
|
||||||
|
and 287 allocations to 26,008–31,223 ns/op, 29,408–29,409 B/op, and 267
|
||||||
|
allocations. Wide success changed from 218,073–253,010 ns/op, 269,063–269,070
|
||||||
|
B/op, and 1,898 allocations to 182,431–194,243 ns/op, 247,626–247,635 B/op,
|
||||||
|
and 1,756 allocations. Deep success changed from 119,958–139,625 ns/op,
|
||||||
|
139,787–139,791 B/op, and 1,074 allocations to 103,208–124,879 ns/op,
|
||||||
|
124,240–124,242 B/op, and 879 allocations. Help and usage paths also reduced
|
||||||
|
allocations and bytes. Cold construction plus first execution remained similar
|
||||||
|
for the small app (38,152–50,235 ns/op, 38,275–38,276 B/op, 339 allocations
|
||||||
|
baseline; 39,158–47,365 ns/op, 38,243 B/op, 338 allocations now). The wide
|
||||||
|
cold case changed from 251,945–315,276 ns/op, 291,822–291,827 B/op, and 2,184
|
||||||
|
allocations to 271,403–321,746 ns/op, 305,046–305,051 B/op, and 2,194
|
||||||
|
allocations. Its timing range overlaps baseline; the extra cold storage is the
|
||||||
|
retained neutral tree. The measured tradeoff is lower repeated-execution cost
|
||||||
|
with one tree allocation retained per app metadata generation.
|
||||||
|
|
||||||
## Stage 8: introduce an invocation-owned adapter session
|
## Stage 8: introduce an invocation-owned adapter session
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ func (a *App) bridgeTree() *internalcobra.Command {
|
|||||||
if a == nil {
|
if a == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if a.commandTree == nil {
|
||||||
|
a.commandTree = a.buildBridgeTree()
|
||||||
|
}
|
||||||
|
return a.commandTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) buildBridgeTree() *internalcobra.Command {
|
||||||
root := &internalcobra.Command{Name: a.identity.Name}
|
root := &internalcobra.Command{Name: a.identity.Name}
|
||||||
names := make([]string, 0, len(a.commands))
|
names := make([]string, 0, len(a.commands))
|
||||||
for name, node := range a.commands {
|
for name, node := range a.commands {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package commandkit
|
package commandkit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
||||||
@@ -48,3 +52,77 @@ func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) {
|
|||||||
t.Fatal("accepted a child flag before the complete command path")
|
t.Fatal("accepted a child flag before the complete command path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridgeTreeRefreshesAfterSuccessfulRegistration(t *testing.T) {
|
||||||
|
app, err := New(Identity{Name: "example"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
streams := Streams{Out: &output, Err: io.Discard}
|
||||||
|
renderRootHelp := func() string {
|
||||||
|
t.Helper()
|
||||||
|
output.Reset()
|
||||||
|
outcome := app.Execute(context.Background(), []string{"--help"}, streams)
|
||||||
|
if outcome.Kind != OutcomeSuccess {
|
||||||
|
t.Fatalf("root help outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
return output.String()
|
||||||
|
}
|
||||||
|
initial := renderRootHelp()
|
||||||
|
if strings.Contains(initial, "custom") || strings.Contains(initial, "config") || strings.Contains(initial, "promptkit") {
|
||||||
|
t.Fatalf("initial help unexpectedly lists future registrations: %q", initial)
|
||||||
|
}
|
||||||
|
|
||||||
|
custom, err := NewCommand(CommandSpec[struct{}]{Name: "custom", Run: func(Invocation, struct{}) Outcome { return Success() }})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := app.Register(custom); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
providerCalls := 0
|
||||||
|
if err := RegisterConfig(app, ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{{
|
||||||
|
Name: "app",
|
||||||
|
Operations: []ConfigOperation{ConfigShow},
|
||||||
|
Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
|
||||||
|
providerCalls++
|
||||||
|
return Acquired[ConfigView]{}, nil
|
||||||
|
},
|
||||||
|
}}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := RegisterPromptKit(app, PromptKitSuite[struct{}]{InspectPrompt: func(context.Context, Selection, struct{}, string, string) (Acquired[*promptkit.PromptInspection], error) {
|
||||||
|
providerCalls++
|
||||||
|
return Acquired[*promptkit.PromptInspection]{}, nil
|
||||||
|
}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
updated := renderRootHelp()
|
||||||
|
for _, name := range []string{"custom", "config", "promptkit"} {
|
||||||
|
if !strings.Contains(updated, name) {
|
||||||
|
t.Errorf("updated help omits %q: %q", name, updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if providerCalls != 0 {
|
||||||
|
t.Fatalf("help invoked inspection or configuration callbacks %d times", providerCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
late, err := NewCommand(CommandSpec[struct{}]{Name: "late"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := app.Register(late, custom); err == nil {
|
||||||
|
t.Fatal("Register accepted a batch with an existing-name collision")
|
||||||
|
}
|
||||||
|
if err := app.Register(); err != nil {
|
||||||
|
t.Fatalf("empty registration failed: %v", err)
|
||||||
|
}
|
||||||
|
final := renderRootHelp()
|
||||||
|
if !strings.Contains(final, "custom") || !strings.Contains(final, "config") || !strings.Contains(final, "promptkit") || strings.Contains(final, "late") {
|
||||||
|
t.Fatalf("help after failed and empty registration = %q", final)
|
||||||
|
}
|
||||||
|
if outcome := app.Execute(context.Background(), []string{"custom"}, streams); outcome.Kind != OutcomeSuccess {
|
||||||
|
t.Fatalf("new command did not parse after registration: %#v", outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,5 +31,6 @@ func (a *App) Register(commands ...Command) error {
|
|||||||
for name, node := range pending {
|
for name, node := range pending {
|
||||||
a.commands[name] = node
|
a.commands[name] = node
|
||||||
}
|
}
|
||||||
|
a.commandTree = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user