Skip to content

perf(router): remove __debug_manifest endpoint + dead trie ancestry (−21% manifest) - #728

Merged
ivogt merged 1 commit into
mainfrom
perf/drop-debug-manifest
Jul 7, 2026
Merged

ivogt merged 1 commit into
mainfrom
perf/drop-debug-manifest

Conversation

@ivogt

@ivogt ivogt commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What

Removes the ?__debug_manifest query endpoint (an early opt-in JSON dump of the manifest/trie for inspection) and, with it, the dead per-leaf trie ancestry it was the only consumer of. Net effect: the per-router manifest chunk is 21.6% smaller (5.71MB → 4.47MB at 26k routes) on every runtime, with no behavior change.

Why it's safe (nothing user-facing breaks)

The endpoint's only data dependency was TrieLeaf.a — the ancestry shortCodes on every trie leaf. Route matching never reads it (verified: trie-matching.ts and find-match.ts reference leaf.a zero times; the source already flagged it debug-only). So once the endpoint and extractAncestryFromTrie are gone, a is pure dead weight, and removing it can't change matching, href()/reverse(), or any request behavior.

Deliberately not touched:

  • The shortCode system (mount indices used in server/context.ts counter keys and the DSL) — that's load-bearing; only the trie-leaf snapshot of it is removed.
  • The separate debugManifest() router method (router/debug-manifest.ts) — a different surface that re-walks handlers into a SerializedManifest; left fully intact.

Changes

  • rsc/handler.ts — delete the __debug_manifest endpoint block.
  • router.ts / router-options.ts / router-interfaces.ts — remove the internal allowDebugManifest option; cache/cache-key-utils.ts — drop it from the reserved-param set.
  • build/route-trie.ts — remove TrieLeaf.a, the routeAncestry param from buildRouteTrie, and extractAncestryFromTrie; buildPerRouterTrie's "no ancestry ⇒ no routes" guard becomes an explicit empty-routeManifest check.
  • build/generate-manifest.ts — remove captureAncestry, the _routeAncestry field, and the routeAncestry threading through the recursive builder.
  • vite/discovery/discover-routers.ts — drop the _routeAncestry merge and the now-redundant guard.
  • Tests — the trie test fixtures that carried synthetic a: leaves / passed the ancestry arg are updated (5 mechanical, parallelized); per-router-manifest.test.ts swaps its extractAncestryFromTrie-based route-name assertions for a local trieRouteNames() walk; dev-prod-trie-parity.test.ts now asserts the stronger invariant — the per-router trie is byte-identical across mount indices (nothing embeds the mount index anymore).
  • Docs — feature-map.md (drop allowDebugManifest), matching-and-lazy-discovery.md (remove the leaf.a caveat + the "stop serializing leaf.a" backlog item this completes).

Effect

  • Manifest payload −21.6% at 26k routes, everywhere (node/vercel/cloudflare) — shrinks the JSON.parse floor, not just compile. Negligible for normal apps (the win scales with route count), meaningful at the tail.
  • Per-router trie is now fully mount-index-independent — a strictly stronger dev/prod parity guarantee.

Test plan

  • pnpm run typecheck
  • pnpm run test:unit:all — 1195+ tests across all packages, incl. find-match, trie-matching, trie-regex-parity, per-router-manifest, dev-prod-trie-parity.
  • semantic-matrix 67/67 (dev + production) — middleware scope/ordering/context visibility/PE-JS parity/PPR all intact.
  • stress-demo build: manifest 5.71MB → 4.47MB, M0L0R/"a":[ markers gone, real routes 200, genuine miss 404.
  • pnpm run lint, pnpm run format, pnpm check:e2e-bucketing.

Notes

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@728

commit: 60d2277

…ancestry (21% smaller manifest)

The `?__debug_manifest` query endpoint (rsc/handler.ts) was an early
experiment — an opt-in JSON dump of the manifest/trie/ancestry for
inspection, gated by the internal `allowDebugManifest` option. Removed
end to end: the endpoint, the option, and `extractAncestryFromTrie`.

Its only data dependency was the per-leaf ancestry array (`TrieLeaf.a` —
mount shortCodes from root to route), which route matching never reads
(verified: trie-matching.ts and find-match.ts touch it zero times). With
the sole reader gone the field was pure dead weight, so the whole
ancestry pipeline is deleted: `a` off the leaf, the `routeAncestry`
argument threaded through buildRouteTrie/buildPerRouterTrie and the
recursive manifest builder, `captureAncestry`, and the `_routeAncestry`
internal manifest field. buildPerRouterTrie's old "no ancestry => no
routes" guard becomes an explicit empty-routeManifest check.

Effect: the per-router manifest chunk drops 5.71MB -> 4.47MB (-21.6%) at
26k routes (the ancestry arrays plus their double-quote escaping) on
every runtime — it shrinks the JSON.parse floor everywhere, not just the
compile. A side benefit: the per-router trie is now fully mount-index-
independent (nothing embeds the mount index), a strictly stronger
dev/prod parity guarantee.

The shortCode SYSTEM (server/context.ts counter keys, the DSL) is
untouched — only the trie-leaf ancestry snapshot is removed. The
separate `debugManifest()` router method (router/debug-manifest.ts,
re-walks handlers -> SerializedManifest) is a different surface and is
left intact.

Verified: typecheck; unit (1195+ across all packages, incl. trie/
find-match/per-router/dev-prod-parity — dev-prod-parity now asserts full
byte-equality across mount indices); semantic-matrix 67/67; stress-demo
build matches real routes (200) + genuine miss (404) with ancestry gone;
lint; format.
@ivogt
ivogt force-pushed the perf/drop-debug-manifest branch from c89bb16 to 60d2277 Compare July 7, 2026 21:30
@ivogt
ivogt merged commit 95f5648 into main Jul 7, 2026
64 of 66 checks passed
@ivogt
ivogt deleted the perf/drop-debug-manifest branch July 7, 2026 22:10
ivogt added a commit that referenced this pull request Jul 7, 2026
…ll and guide

#728 deleted the ?__debug_manifest HTTP endpoint and its routeAncestry
data, but the debug-manifest skill and docs guide still told users to open
http://localhost:PORT/?__debug_manifest and documented the deleted
{ routerId, routeManifest, routeAncestry, routeTrie, precomputedEntries }
shape. Remove both browser-endpoint sections; both files already carry the
correct router.debugManifest() programmatic path ({ routes, layouts,
totalRoutes, totalLayouts }), now the primary documented route.
ivogt added a commit that referenced this pull request Jul 8, 2026
…ites (#725 gap) + drop removed __debug_manifest docs (#730)

* fix(router): handler-invoked loaders under cache() throw on header writes (closes #725 gap)

The cached-scope header guard (#725) exempted every loader body under a
cache() boundary via isInsideAnyLoaderScope(). That let a handler-invoked
loader — await ctx.use(Loader) from a handler, never registered with
loader() — write a cookie/header on the MISS. On a HIT the handler is
skipped and only DSL-registered loaders re-run, so that loader never
executes and its write silently vanishes — the exact MISS/HIT divergence
#725 was built to eliminate.

Narrow the exemption to isInsideLoaderScope() (DSL scope only): registered
loaders re-run on every HIT and merge into every response, so their writes
stay allowed; a handler-invoked loader now throws deterministically like a
handler write. The DSL scope ALS survives nested ctx.use bodies, so a
handler-invoked loader nested under a DSL loader stays exempt (its parent
re-runs it every HIT). The broad predicate is deferred to the throw path for
the error label so the PPR handler-invoked-loader message stays "loader",
and the throw reason keys on scope.kind so the cache() case explains the
handler-skip cause. Re-ground the neighbouring read-purity guard comment,
which this change makes contradictory: reads stay on the broad predicate
under the consumption-lane baked-copy tradeoff (#672/#674), only writes
narrow to DSL scope.

Unit (cached-header-guard.test.ts): DSL-allowed pin, handler-invoked-throws
pin (red before this change, green after), nested-under-DSL-allowed pin.
E2e (cache-scope-guard, dev + production): /handler-loader-cookie-blocked
asserts the error boundary renders and the cookie never reaches the wire;
the DSL /loader-cookie-allowed control stays green in both modes.

* docs: drop the removed __debug_manifest browser endpoint from the skill and guide

#728 deleted the ?__debug_manifest HTTP endpoint and its routeAncestry
data, but the debug-manifest skill and docs guide still told users to open
http://localhost:PORT/?__debug_manifest and documented the deleted
{ routerId, routeManifest, routeAncestry, routeTrie, precomputedEntries }
shape. Remove both browser-endpoint sections; both files already carry the
correct router.debugManifest() programmatic path ({ routes, layouts,
totalRoutes, totalLayouts }), now the primary documented route.
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