Conversation
…dency tracking hot path Flame chart profiling revealed that 98% of active CPU per frame during card prerendering was spent in the runtime dependency tracking system, primarily constructing URL objects. The render produced 22 identical ~9-second long tasks (one per card deserialization), totaling ~200 seconds of blocked main thread for a card with 23 linksToMany relationships. Three optimizations applied: 1. trimModuleIdentifier (loader.ts): Replace `new URL(id).href` with string slice operations + a Map cache. Module identifiers are already full URL strings, so extension trimming only needs string ops. This was the single largest CPU consumer at 52.8% of active time (~5s per card). 2. collectKnownModuleDependencies (loader.ts): Cache the flattened dependency set per module identifier. Once a module is evaluated its consumedModules never change, so repeated graph walks for the same module return the cached result. This turns O(cards × modules) into O(modules). 3. trackRuntimeRelationshipModuleDependencies (card-api.gts): Track which modules have already had their full dep trees tracked and skip redundant getKnownConsumedModules() calls. This function was called on every linksTo field getter access during rendering, each time walking the full module dependency graph. Additionally, normalizeModuleURL/normalizeInstanceURL/canonicalURL in dependency-tracker.ts now use string operations instead of URL construction, eliminating another hot source of URL() calls in the tracking pipeline. Closes CS-10473 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rf-url-and-dep-tracking
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e215f9dc4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR targets prerender performance bottlenecks in runtime dependency tracking by removing repeated new URL() construction in hot paths and caching module dependency graph traversals, aiming to drastically reduce SystemCard prerender time.
Changes:
- Replace URL-construction-based executable-extension trimming with string operations + caching.
- Cache flattened dependency sets for evaluated modules to avoid repeated full dependency graph walks.
- Add a fast path to skip redundant relationship module dependency tracking, and optimize URL normalization in the dependency tracker.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
packages/runtime-common/loader.ts |
Adds caching for known module dependency traversal and replaces URL-based trimming with string ops + cache. |
packages/runtime-common/dependency-tracker.ts |
Replaces URL-constructor-based canonicalization/normalization with string operations. |
packages/base/card-api.gts |
Skips repeated relationship module dependency tracking via a module-level cache. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Host Test Results 1 files ± 0 1 suites ±0 4h 16m 23s ⏱️ + 1h 54m 12s For more details on these errors, see this check. Results for commit 2754d6f. ± Comparison against base commit 505f18d. ♻️ This comment has been updated with latest results. |
Address review feedback: - getKnownConsumedModules: filter instead of delete to avoid mutating the cached Set returned by collectKnownModuleDependencies - Remove trackedRelationshipModules skip cache from card-api.gts — it was process-global and not cleared between dependency tracking sessions, which could cause subsequent renders to under-report module deps. The Loader-level caching in collectKnownModuleDependencies already makes getKnownConsumedModules fast enough without a caller-side skip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
backspace
left a comment
There was a problem hiding this comment.
Have you already tried this?
- Verify on staging that SystemCard prerender time drops significantly
Staging verification after deployMeasured the SystemCard prerender on staging (
Timeline:
Previously this card was consistently timing out at the 90-second prerender limit on staging. |
Post-fix flame chart analysisAfter deploying PR, a new flame chart of the same SystemCard render shows the bottleneck landscape has completely changed:
What changed
InterpretationThe render is now network + GC bound rather than CPU-bound on a single hot path. The ~17s is spent on: fetching ~137 resources, compiling .gts modules via Babel/content-tag WASM, and GC from object allocations during card deserialization. There is no single obvious optimization target remaining — it is a healthy distribution. |
…racking hot path (#4223) * Optimize prerender performance: eliminate URL() construction in dependency tracking hot path Flame chart profiling revealed that 98% of active CPU per frame during card prerendering was spent in the runtime dependency tracking system, primarily constructing URL objects. The render produced 22 identical ~9-second long tasks (one per card deserialization), totaling ~200 seconds of blocked main thread for a card with 23 linksToMany relationships. Three optimizations applied: 1. trimModuleIdentifier (loader.ts): Replace `new URL(id).href` with string slice operations + a Map cache. Module identifiers are already full URL strings, so extension trimming only needs string ops. This was the single largest CPU consumer at 52.8% of active time (~5s per card). 2. collectKnownModuleDependencies (loader.ts): Cache the flattened dependency set per module identifier. Once a module is evaluated its consumedModules never change, so repeated graph walks for the same module return the cached result. This turns O(cards × modules) into O(modules). 3. trackRuntimeRelationshipModuleDependencies (card-api.gts): Track which modules have already had their full dep trees tracked and skip redundant getKnownConsumedModules() calls. This function was called on every linksTo field getter access during rendering, each time walking the full module dependency graph. Additionally, normalizeModuleURL/normalizeInstanceURL/canonicalURL in dependency-tracker.ts now use string operations instead of URL construction, eliminating another hot source of URL() calls in the tracking pipeline. Closes CS-10473 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix cached Set mutation and remove session-scoping issue in dep tracking Address review feedback: - getKnownConsumedModules: filter instead of delete to avoid mutating the cached Set returned by collectKnownModuleDependencies - Remove trackedRelationshipModules skip cache from card-api.gts — it was process-global and not cleared between dependency tracking sessions, which could cause subsequent renders to under-report module deps. The Loader-level caching in collectKnownModuleDependencies already makes getKnownConsumedModules fast enough without a caller-side skip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
new URL()construction from the runtime dependency tracking hot pathResults
Measured on staging with the SystemCard (23
linksToMany→linksTochain,clearCache=true):Problem
Flame chart profiling of the SystemCard prerender revealed 22 identical ~9-second long tasks, each with 98% of active CPU in the dependency tracking system:
URL()constructors(minified; also callsURL())trimModuleIdentifiercollectKnownModuleDependenciesThe root cause: every
linksTofield getter access triggerstrackRuntimeRelationshipModuleDependencies, which callsloader.getKnownConsumedModules(), which walks the entire module dependency graph callinggetModule()→trimModuleIdentifier()→new URL()on every node. With 22 cards each triggering this on render, it's 22 full graph walks with hundreds of thousands of URL constructions.Changes
1.
trimModuleIdentifier→ string ops + cache (loader.ts)Replace
trimExecutableExtension(new URL(moduleIdentifier)).hrefwithstring.slice()+ aMapcache. Module identifiers are already valid URL strings — extension trimming only needs string operations.2. Cache
collectKnownModuleDependenciesresults (loader.ts)The flattened dependency set for an evaluated module is immutable. Cache it so 22 identical graph walks become 1, with subtree cache hits for shared dependencies.
3. String-based normalization in
dependency-tracker.tsReplace
new URL()calls incanonicalURL,normalizeModuleURL, andnormalizeInstanceURLwith string operations.Test plan
@cardstack/runtime-commonand@cardstack/baseCloses CS-10473
🤖 Generated with Claude Code