Skip to content

feat: Add opt-in module state persistence for warm starts (scan 416 ms -> load 36 ms)#4

Draft
jeswr wants to merge 1 commit into
masterfrom
perf/module-state-cache
Draft

feat: Add opt-in module state persistence for warm starts (scan 416 ms -> load 36 ms)#4
jeswr wants to merge 1 commit into
masterfrom
perf/module-state-cache

Conversation

@jeswr

@jeswr jeswr commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an opt-in moduleStateCachePath option to ComponentsManagerBuilder that persists the component-discovery result (IModuleState) to a file and reuses it on subsequent invocations — the follow-up flagged in #3. Discovery currently rescans the entire node_modules tree on every build (~416 ms for a Community Solid Server install with 1,381 modules), even though the result only changes when the dependency tree changes.

Behaviour

  • When moduleStateCachePath is set and no explicit moduleState is provided: the builder loads the persisted state if it is fresh, otherwise runs a normal discovery scan and persists its result (atomically, write-then-rename, so concurrent processes never observe a partial file).
  • When moduleState is provided, the cache is not consulted (unchanged precedence).
  • Any load/parse failure degrades silently to a fresh scan; save failures log a warning and never break the build.

Staleness story (explicit)

The cache entry embeds a fingerprint of:

  1. the cache format version (bumped when IModuleState's shape changes),
  2. the componentsjs version,
  3. the main module path,
  4. mtime + size of the main module's package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, npm-shrinkwrap.json, and the node_modules directory itself.

Any mismatch → the cache is ignored (with an info log) and rebuilt. This detects package installs/removals and lockfile changes (they touch a lockfile and/or node_modules's mtime). It deliberately does not detect in-place edits deep inside node_modules (e.g. hand-editing an installed package's components/ files): scanning for that would cost as much as discovery itself. That limitation is why the feature is opt-in, and it is documented on the option and the class. Escape hatch: delete the cache file, or drop the option.

Measurements

Real Community Solid Server tree, warm file cache, medians, alternating in one gated run (pss-solid-test, 2-core shared, nice -n 18):

median
full discovery scan 416 ms
cache load (fingerprint check + read + parse, 2.4 MB file) 36 ms

~11.5×, −380 ms per warm start, with identical module state (verified key-by-key). Composes with #3 (which speeds up the cold scan itself) and with #1 (which removes the JSON-LD parse cost after discovery).

Tests

13 new unit tests for ModuleStateCache (fingerprint sensitivity, staleness, corrupt/missing files, round-trip, atomic write, failure logging) + 3 builder wiring tests (cache hit, miss + persist, explicit-moduleState precedence). Full suite green, 100 % coverage retained.


Generated by an agent (Claude Fable 5) on @jeswr's behalf — staged on the fork for his review before upstreaming.

Review timing: This draft was prepared with Claude; I (@jeswr) will personally review it before it progresses. I'm currently batching a lot of work in flight, so expect active review Wednesday–Friday (8–10 July).

ComponentsManager runs component discovery (a scan of the entire
node_modules tree) on every build: ~416 ms for a Community Solid Server
install (1,381 modules), even though the result only changes when the
dependency tree changes.

This adds an opt-in `moduleStateCachePath` option to
ComponentsManagerBuilder. When set (and no explicit `moduleState` is
provided), the builder tries to load the persisted module state from
that file, and persists a freshly built state to it (atomically, via
write-then-rename) after a scan.

Staleness handling: the cache stores a fingerprint of the cache format
version, the componentsjs version, the main module path, and the
modification time and size of the main module's package.json, lock
files (package-lock.json, yarn.lock, pnpm-lock.yaml,
npm-shrinkwrap.json) and node_modules directory. Any mismatch or read
failure falls back to a fresh scan. This is a heuristic: package
installs/removals touch a lock file and node_modules and are detected,
but in-place edits deep inside node_modules are not - which is why the
feature is opt-in and documented accordingly.

A/B on the real Community Solid Server tree (2-core shared box, warm
file cache, medians): full discovery scan 416 ms vs cache load 36 ms
(~11.5x, -380 ms per warm start; 2.4 MB cache file), with identical
module state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant