Performance and scale

Tracker: pm-mi2x under the pm-9rxu scale-out initiative. CLI transport overhead is tracked by pm-yse5dt.

pm treats wall time, memory, and agent token cost as one performance contract: project management is context management, so a fast command that emits an unbounded payload is still slow for its caller.

Current targets

Every measured CLI and SDK operation targets:

  • p95 wall time at or below 1,000 ms;
  • default output at or below 5,000 estimated tokens;
  • no feature, flag, output, history, or validation loss.

The committed regression budgets in scripts/bench/scale-budgets.json protect the current baseline while the product targets stay fixed. Short local checks use the best observed latency as a regression floor; statistically meaningful runs of 20 or more use p95. Both receive 25% baseline headroom plus a 25 ms scheduler/filesystem noise margin. Reports always retain min/p50/p95, and product-target status always uses p95. This lets a busy workstation detect deterministic code slowdowns without pretending that three samples produce a meaningful p95 or hiding the real tail-latency target.

Reproducible fixtures

Generate a deterministic isolated workspace without touching the current tracker:

pnpm build
pnpm benchmark:scale:generate --output /tmp/pm-scale --items 10000 --mode direct
PM_PATH=/tmp/pm-scale/.agents/pm PM_GLOBAL_PATH=/tmp/pm-scale-global pm validate --json

Named tiers are smoke (100), ci (10,000), large (100,000), and million (1,000,000). The ci name describes the fixture size for compatibility with existing benchmark data; these performance workloads run locally and are intentionally absent from GitHub Actions. direct writes bounded batches for large fixtures; sdk uses the public SDK atomic-write and history primitives. Both modes serialize the same deterministic item and history bytes.

The fixture models built-in item types, open/in-progress/blocked/closed/canceled states, parents, dependencies, tags, bodies, comments, notes, learnings, and one valid history stream per item. It refuses to generate inside this repository. --force replaces a non-empty target only when that target already contains the scale-fixture manifest, so a mistyped arbitrary directory is never recursively removed.

Benchmark runner

Run CLI cold-process and SDK warm-process measurements together:

pnpm benchmark:scale --items ci --iterations 3 --transport both --check

The JSON report records fixture generation time plus one excluded warmup observation and measured p50/p95/min/max latency, peak RSS on Linux, output bytes, and estimated tokens for list, get, next, context, search, create, and claim. The warmup exposes initial index-build cost while regression percentiles measure the continuously warm derived-index contract across real cold CLI processes and in-process SDK calls. Run the committed local regression check with pnpm benchmark:scale:check. GitHub-hosted runners do not execute the scale suite, which keeps expensive performance work off Actions and avoids consuming hosted-runner capacity.

The scale report also records the CLI-minus-SDK transport delta for each common operation and gates it independently from the absolute CLI/SDK budgets. This separates process/bootstrap cost from storage or domain cost instead of allowing one transport to hide a regression in the other. The synthetic workspace has no installed extensions, while the SDK client explicitly opts out, so both paths measure the same core operation without project package work. CLI mutations do not inject PM_AUTHOR; they exercise the normal harness/default attribution resolver. The 10,000-item delta budgets are measured ratchets. The 100,000-item delta bounds are conservative ceilings derived from the existing absolute CLI budgets until a stable large-fixture measurement can replace them; they do not weaken either transport's existing absolute gate.

For a fast, deterministic one-item cold-start floor covering get, list, context, next, create, and claim, run:

pnpm benchmark:transport:check

The committed CLI transport report contains best/p50/p95 measurements and a stage breakdown. SDK entrypoint import costs are independently gated with pnpm benchmark:sdk-entrypoints:check and documented in the SDK entrypoint report.

The metadata read cache is rebuildable derived state. Workspaces with at least 500 indexed items use its directory-validated fast path to avoid per-item stats; validation and recovery can force a canonical source scan. Every metadata, body, and collection tier carries one source_cursor. A small manifest exposes the base cursor and item count without parsing the full index. A supported create, update, move, or delete acquires the cross-process derived-index writer lock before the authoritative item commit and atomically publishes one collapsed delta containing every compatible tier projection, directory signatures, and the next cursor before releasing the lock. Mutation cost therefore follows changed items rather than total workspace size. A torn, corrupt, or base-mismatched delta is rejected and rebuilt from source.

Large indexes also carry runtime/metadata-query-index.sqlite, a rebuildable row/index projection with the same context fingerprint and effective source_cursor. Source scans replace it atomically; supported mutations update it inside the existing derived-index writer critical section. Bounded default-order light list pages query only their result window plus a count, instead of parsing the full JSON cache. Unsupported filters, heavy body/collection projections, trees, runtime fields, active read hooks, missing files, stale cursors, and corrupt databases fail closed to the established authoritative/JSON path. The SQLite file is optimization state and is safe to delete or rebuild with pm reindex.

The same database carries bounded FTS candidate retrieval for the opt-in create/copy duplicate-governance probe. Minimal/default governance keeps the probe off and performs no query on the create path; advisory/strict modes reuse one dependency-light SDK scorer with the package-owned dedupe audit. Mutation history events use a separate rebuildable ordered SQLite projection so pm events --since <cursor> catch-up cost follows new history rows instead of workspace size. See SDK Context Coordination.

SDK hosts that commit authoritative item documents outside the stock mutation commands use acquireItemMetadataDerivedIndexLock and refreshItemMetadataDerivedIndex from @unbrained/pm-cli/sdk around the same commit boundary. Repeated writes collapse by item path in the delta instead of growing an event log; a later source scan compacts the projection into fresh base tiers. Projection failure removes the rebuildable tiers, delta, and manifest and returns a warning; it never rolls back or outranks the authoritative item/history write. Small workspaces without an active index receive a no-op release function and retain direct external-edit detection.

Context-signal snapshots follow the same rule: they are versioned and cursor-stamped, never authoritative, and rebuild from the metadata index or source-scan fallback when missing, stale, or corrupt. The stock context and next projections use separate snapshot namespaces, persist only caller-independent signals, and recompute author, time, semantic, and usage overlays on every read. SDK hosts can use readWorkspaceContextSignals for the same automatic cursor binding or compose ContextSignalStore directly, while retaining explicit fresh/rebuilt and derived_index/scan_fallback diagnostics. See Context relevance and packing.

At 10,000 items, context and search also build runtime/workspace-memory.json: compact calendar-epoch and epic-lineage summaries over completed history. The snapshot is cursor-bound, token-bounded when attached to context, searchable without expanding closed rows, and skipped entirely below the threshold. Missing, stale, corrupt, or unwritable memory state rebuilds or degrades without outranking item documents or history.

To refresh a baseline after an intentional, measured improvement:

pnpm benchmark:scale --items ci --iterations 5 --transport both --update --headroom 1.25

Review the report and budget diff together. Never update a budget merely to silence a regression.

Startup and observability

Sentry is loaded only when an error-reporting path actually initializes it. Disabled and normal successful commands do not resolve or compile the @sentry/node → OpenTelemetry → Undici graph. The loader uses the package's supported CommonJS export after the opt-out gate, preserving the repo-wide ban on dynamic/inline imports while keeping enabled capture, sanitization, and flush behavior intact.


Performance remote
Report an issue