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.
Hello, and thank you for
octl— the per-service endpoint variables (OSC_ENDPOINT_APIand 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_64v0.0.31, release binary, checksum verified againstoctl_0.0.31_checksums.txt. Linux 6.8, 16 cores, load < 3.671 ms of the 686 ms total, across 568 packages, is
github.com/outscale/octl/cmdalone. Everything else is noise.It is paid whatever you ask for, including commands that touch nothing:
octl --versionoctl --no-upgrade --helpoctl --no-upgrade iaas api ReadNets --helpFor comparison on the same station:
scw version17 ms,exo version18 ms,oapi-cli --version19 ms.Where it comes from
cmd/holds 17init()functions, and they build the whole command tree eagerly by reflection over the SDK. Fromcmd/iaas.go:octl iaas api --helplists 237 subcommands, each with its flags, and the same pattern runs forkube,storageand 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 atv0.0.31and atmain826d7be):sync.OnceValue(f)returns a new memoising function on every call, and it is invoked immediately — so the memoisation never spans two calls. EachDefaults()re-opens the embedded zip and re-decodes every YAML in it:defaults_iaas.yamlis 415 KB anddefaults_storage.yaml161 KB uncompressed.The usual shape would be a package-level value:
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
octlas 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 tooctlfrom 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
octlin 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.