filecoin-project/filecoin-pin

filecoin-pin CLI first call slowness

Open

#432 opened on Apr 30, 2026

View on GitHub
 (1 comment) (0 reactions) (0 assignees)TypeScript (18 forks)github user discovery
enhancementgood first issueready-for-workteam/filecoin-pinteam/fs-wg

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 in cli.ts: ~0.25s
  • the src/commands/index.js import graph: ~0.64s. ALL_CLI_COMMANDS statically imports every command module, and those modules import @filoz/synapse-sdk, viem, helia, and @helia/unixfs at 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/node inside command actions rather than at module top, and skip it for version/help. A lightweight global uncaughtException / unhandledRejection hook 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.
  • --version and --help return 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() on cli.ts:13 is checked for startup cost alongside Sentry.

Contributor guide