Skip to content

Package init builds the whole command tree by reflection: ~690 ms before any command runs #313

Description

@stephrobert

Hello, and thank you for octl — the per-service endpoint variables (OSC_ENDPOINT_API and friends) made it straightforward to point it at a local emulator, which is exactly what we needed.

While measuring it as a conformance client we found that every invocation pays ~690 ms before any command runs, and almost all of it is one package's init(). Reported with the measurements rather than an impression.

Measured

octl_Linux_x86_64 v0.0.31, release binary, checksum verified against octl_0.0.31_checksums.txt. Linux 6.8, 16 cores, load < 3.

$ GODEBUG=inittrace=1 octl --version 2>&1 | awk '/^init /{print $5, $2}' | sort -rn | head -4
671 github.com/outscale/octl/cmd
7.2 github.com/alecthomas/chroma/v2/lexers
3.3 github.com/alecthomas/chroma/v2/styles
1.3 k8s.io/client-go/kubernetes/scheme

671 ms of the 686 ms total, across 568 packages, is github.com/outscale/octl/cmd alone. Everything else is noise.

It is paid whatever you ask for, including commands that touch nothing:

command wall clock (rc=0, output verified)
octl --version 693 ms
octl --no-upgrade --help 689 ms
octl --no-upgrade iaas api ReadNets --help 712 ms

For comparison on the same station: scw version 17 ms, exo version 18 ms, oapi-cli --version 19 ms.

Where it comes from

cmd/ holds 17 init() functions, and they build the whole command tree eagerly by reflection over the SDK. From cmd/iaas.go:

func init() {
	rootCmd.AddCommand(iaasCmd)
	b := builder.NewBuilder[osc.Client]("iaas", "https://docs.outscale.com/api.html")
	b.BuildAPI(iaasCmd, func(m reflect.Method) bool {
		return m.Type.NumIn() == 4 && m.Type.NumOut() == 2 && !strings.HasSuffix(m.Name, "Raw")
	}, oapi)
	b.Build(iaasCmd, nil)
}

octl iaas api --help lists 237 subcommands, each with its flags, and the same pattern runs for kube, storage and the rest. Printing a version number constructs all of them first.

A lazy build — constructing only the branch the command line actually names — would remove essentially all of it, since cobra already knows which subcommand was requested before it needs that subtree's flags. We understand that is not a small change to builder, which is why we are reporting rather than assuming.

A second, smaller one, in the same area

pkg/config/defaults.go (identical at v0.0.31 and at main 826d7be):

func Defaults() Configs {
	return sync.OnceValue(func() Configs {
		r, err := zip.NewReader(bytes.NewReader(defaults), int64(len(defaults)))
		...
	})()
}

sync.OnceValue(f) returns a new memoising function on every call, and it is invoked immediately — so the memoisation never spans two calls. Each Defaults() re-opens the embedded zip and re-decodes every YAML in it: defaults_iaas.yaml is 415 KB and defaults_storage.yaml 161 KB uncompressed.

The usual shape would be a package-level value:

var defaultsOnce = sync.OnceValue(func() Configs { ... })

func Defaults() Configs { return defaultsOnce() }

config.For(provider) calls it on the request path, so this one costs at run time rather than at init — smaller than the above, but free to fix.

Why it matters to us, for context

We drive octl as the client of a conformance suite for a local cloud emulator: ~700 invocations per run. At ~690 ms of fixed startup, that is roughly 8 minutes of the run spent before any request is composed, on a suite whose HTTP exchanges take about 30 ms each against a loopback server. Our CI went from 4 min 24 to 11 minutes when we migrated to octl from the (now archived) oapi-cli.

That is our unusual workload, not a typical interactive one — but the same 690 ms is paid by anyone scripting octl in a loop.

Happy to help

We are glad to contribute the lazy-build change if the approach suits you, or to test a branch against our suite, which exercises 77 IaaS operations and would notice a regression in the command tree. Tell us what you would prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions