Perf/large spec rendering - #2834
Closed
mtoutcalt wants to merge 3 commits into
Closed
Conversation
The lint script single-quoted its glob. npm run executes scripts through
cmd.exe on Windows, which does not treat single quotes as quoting, so eslint
received the literal string 'src/**/*.{js,ts,tsx}' -- quote characters
included -- matched no files and exited non-zero.
That made the husky pre-commit hook fail, blocking every commit made from a
Windows checkout. Double quotes are stripped by cmd.exe and by POSIX shells
alike, so this is a no-op for existing Unix contributors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Redoc's cost was proportional not to spec size but to the number of distinct paths through the reference graph, which grows multiplicatively with reference depth. A 70 KB generated spec allocated 1.2 GB; a 0.29 MB one exhausted a 3 GB heap outright with "Ineffective mark-compacts near heap limit". Three independent causes, all measured: 1. Schema expansion was eager. SchemaModel's constructor called buildFields, which built a FieldModel per property, which built another SchemaModel, terminating only on the refsStack cycle check. Since refsStack tracks the current path, a schema reachable by N paths was materialised N times, each fully expanded. Nothing needed that work up front: the renderer shows one level at a time and only expands a row on demand. `fields` is now a lazy getter. 199,132 -> 766 models on the tiny preset. 2. Payload samples were eager. Every media type of every operation generated a sample at construction, walking to generatedSamplesMaxDepth (10) and retaining the result. hasSample made it worse by answering a yes/no question through `.examples`, forcing generation for the whole document. Generation is now deferred and hasSample consults a cheap predicate. 3. Ref stacks compounded. mergeAllOf stamps x-refsStack onto every merged property and deref concatenates that stored stack onto the current path, so length grew multiplicatively rather than with depth -- observed max depth 10,546 against a MAX_DEREF_DEPTH guard of 999, and 147M array entries allocated. refsStack is only consulted via includes() and a length check, so duplicates carry no information; deduping is semantics-preserving and takes it to 4.7M entries, max depth 12. Adds a stress-spec generator and a measurement CLI. Use the CLI rather than the jest suite when chasing a regression: jest and jsdom add roughly 10x overhead, enough to hide which layer is actually slow. Still outstanding: mergeAllOf is eager and re-walks the graph (871,763 calls for 3,836 models). Memoising does not help -- only 7.5% of calls repeat a key. The medium preset remains blocked on it, marked with a skipped test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Redoc mounts every operation into the DOM on load, so opening a large spec
builds and retains models and payload samples for the entire API before the
reader has looked at any of it. focusMode makes the sidebar selection drive a
single rendered section, so cost tracks what is on screen.
Selecting an operation renders that operation. Selecting a tag renders its
description plus links to its operations rather than expanding them -- a tag
with 200 operations would otherwise reintroduce the problem. Rendering those
links is free: it reads sidebarLabel and id, never the schemas.
Scroll spy is disabled in this mode; only one section is mounted, so there is
nothing to track and leaving it running would fight the menu by reactivating
whatever is on screen. scrollToActive goes to the top of the content pane
instead of to an element, since activation is what causes the section to render
and there is no target at the time the scroll is requested. Deep links, search
and history are unchanged -- they all route through MenuStore.activate.
On the small preset with samples on (what a browser actually does):
render everything 3836 models 12.5 s 1142 MB
focus mode 54 models 0.08 s 9 MB
Focus mode does not rescue the deepest specs. On the large preset, building a
single operation still exhausts a 4 GB heap, because the outstanding mergeAllOf
cost is per schema and driven by depth, not by how many operations render.
Bounding what renders is necessary but not sufficient. Documented in
docs/large-spec-performance.md.
Verified with jsdom render tests, not in a real browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What/Why/How?
Reference
Tests
Screenshots (optional)
Check yourself