Repository metrics
- Stars
- (22 stars)
- PR merge metrics
- (PR metrics pending)
Description
Problem
Even no-op commands sit silent for most of a second before any output. Measured on a built CLI (cold start, Apple Silicon):
| Invocation | Wall time |
|---|---|
node -e 0 (baseline) |
0.04s |
filecoin-pin --version |
~0.85s |
filecoin-pin --help |
~0.84s |
--version does no work: no network, no command logic. So roughly 0.8s is pure startup overhead. Two costs dominate:
src/instrument.js(Sentry init), imported first incli.ts: ~0.25s- the
src/commands/index.jsimport graph: ~0.64s.ALL_CLI_COMMANDSstatically imports every command module, and those modules import@filoz/synapse-sdk,viem,helia, and@helia/unixfsat module-eval time, before any command runs.
This is the "did anything happen?" dead time users feel on the first call.
Metrics this tracks
Time to First Output (TTFO): invoke → first byte printed to stdout/stderr. What users feel as "did anything happen?"
Perceived Startup Latency: invoke → first visible signal (log line, spinner, progress). What users feel as "ok, something is happening."
Silent Latency (dead time): invoke → any output when nothing is shown during execution. What users feel as "is this broken or stuck?"
Potential improvements to explore
Rough order of likely payoff, to confirm with measurement:
- A fast path for
--version,--help, and the bare invocation that prints from static metadata before constructing the full program or loading Sentry. Cheapest win, and it targets the measured case directly. - Moving Sentry off the universal startup path: load
@sentry/nodeinside command actions rather than at module top, and skip it for version/help. A lightweight globaluncaughtException/unhandledRejectionhook can still catch early failures. - Lazy command registration: register each command from static metadata (name, description, options) and dynamic-import its implementation inside the action handler, so an invocation only loads the dependencies it uses. Commander already runs
parseAsync, so async actions work; dynamic-import failures would need consistent error handling, and option validation has to stay in the static metadata. - Pushing heavy imports below the command boundary too: avoid broad core barrels that pull in Helia/Synapse/viem eagerly, so even commands that need them defer the cost to first use.
module.enableCompileCache()(Node 22+) as a warm-start aid. It caches V8 bytecode for later runs and does not reduce first-run module work, so it complements the above rather than replacing it.- Bundling the entry with esbuild to flatten ESM resolution. Worth a spike and a measurement first, since the payoff estimate varies and Helia/Synapse conditional exports can complicate it.
SEA/snapshot looks like a poor fit given native dependencies (better-sqlite3, helia).
Done criteria
- Dominant startup costs documented (above) and re-confirmed after changes.
--versionand--helpreturn fast enough to feel instant (rough target ~50-150ms), since they need no command dependencies.- Commands that require viem/Helia print a first visible signal promptly. Their floor is higher (~200-500ms before real work begins), and the target reflects that.
configureTelemetry()oncli.ts:13is checked for startup cost alongside Sentry.