Repository navigation
Speed up API page regeneration in serve to under 4 seconds - #4322
Conversation
| DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: "1" | ||
| # API page regeneration allocates heavily on every core; workstation GC | ||
| # throttled it to a few cores (about 10 s per regeneration vs 3-4 s). | ||
| DOTNET_gcServer: "1" |
There was a problem hiding this comment.
I would, but the runtime reads this one case-sensitively on Linux and macOS. A one-line app that prints GCSettings.IsServerGC shows True for DOTNET_gcServer=1, and False for DOTNET_GCSERVER=1 and DOTNET_GCserver=1. Uppercasing it would silently turn server GC off. I added a comment in the compose file saying so (f7b6db5).
There was a problem hiding this comment.
Requesting changes due to a reliability regression: API regeneration in serve can become permanently stuck after a single transient root-index fetch failure because the new long-lived VersionIndexClient memoizes that failure for the lifetime of the process.
What is this? | From workflow: PR Review
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
| private volatile bool _forceApiRegeneration; | ||
| private readonly SemaphoreSlim _apiSemaphore = new(1, 1); | ||
| // Outlives each OpenApiGenerator so regenerations reuse the version index and downloaded specs. | ||
| private readonly VersionIndexClient _versionIndexClient = new() { CacheSpecBodies = true }; |
There was a problem hiding this comment.
[HIGH] Persistent VersionIndexClient can make API regeneration permanently fail after one transient index outage
Reusing a single VersionIndexClient instance across the full serve lifetime changes failure behavior for root-index fetches: VersionIndexClient.GetRootIndexAsync memoizes the first failure (_rootIndexFetched = true plus _rootIndexFetchError) and then returns that cached error on every later call, without retrying.
That means one temporary index.json fetch failure during serve startup can keep all subsequent API regenerations failing until the process is restarted, even after connectivity recovers.
Please keep spec-body caching, but avoid permanently caching root-index failures for the long-lived client path (for example: only mark _rootIndexFetched when index fetch/parse succeeds, or clear the fetched/error state after a failure so the next regeneration retries).
There was a problem hiding this comment.
Agreed, fixed in bd47520. serve no longer keeps a VersionIndexClient for its whole life. Each regeneration creates its own client, so the root index is fetched again and a failure is retried next time. Only a new SpecBodyCache, passed in via SpecBodies, outlives regenerations, so specs are still downloaded once. Within a single build, the root index result is still memoized per client as before. Warm regeneration stays at about 2.5–5 s.
The space normalizer reused one shared inline node for every match. Markdig inlines are linked-list nodes, so a second match moved the node and the earlier space disappeared. The output also depended on parse order across documents, so parallel parses could race on the shared node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regeneration was bound by workstation GC, a full sidebar rescan on every page, and specs downloaded and resolved one version at a time. The dev container now runs server GC. The sidebar marker scans spans and edits in one pass. Pages and versions render in parallel, and serve keeps downloaded spec bodies between regenerations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hint dedupe used a process-wide set. A later build in the same process, such as a serve reload or a test that reuses a file name, got no hint for that file. The set is now scoped to the build's diagnostics collector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f3c05d2 to
f7b6db5
Compare
Docs preview (local build)Handbook preview: https://docs-v3-preview.elastic.dev/elastic/docs-builder/pull/4322/ |
One VersionIndexClient lived for the whole serve process. It memoizes the root index result, including a failure, so a single network error kept every later regeneration from resolving specs. Serve now creates a client per regeneration and shares only a SpecBodyCache, so specs are still downloaded once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
No actionable issues found in the current revision; approving.
What is this? | From workflow: PR Review
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
Regenerating API Explorer pages in
./dev.sh servetakes 3–4 s instead of about 10 s. A thin-space bug that dropped spaces from rendered Markdown is also fixed.Affects: API reference, Authoring, Site UI
Stack: 2 of 2, on top of #4321.
Prompt summary: The author wants faster API page regeneration in the dev server. The goal is to render pages within one API version in parallel, and to stop downloading specs again on every regeneration.
Why
After #4321, every code or asset change regenerates all API pages, and one regeneration takes 9–11 s on an 18-core Mac. A profile shows the process uses only 3–4 cores. Workstation GC throttles the allocation-heavy render. Each page also rescans the full sidebar HTML with one substring per tag. HtmlSanitizer serializes on a process-wide AngleSharp pool lock. Specs download and parse one version at a time on every regeneration.
What
Server GC in the dev container
The
serveservice setsDOTNET_gcServer=1. With workstation GC, extra render threads add no speed. With server GC, a regeneration takes 3.3–4.3 s. The setting applies to the dev container only, so CI and the AOT binaries keep their current GC.Parallel pages and versions
Pages of one API version now render in parallel, and the versions of one product download and parse in parallel. The shared schema cache becomes concurrent. Sanitized description HTML is cached per version, so each distinct fragment goes through the sanitizer lock once. When two navigation items share a URL, the last one still wins, as in the old depth-first order.
Spec bodies are kept between regenerations
VersionIndexClienttakes an optionalSpecBodyCache.servekeeps one cache for the life of the server and creates a new client for each regeneration. A regeneration re-parses specs without downloading them again, and a failed version index fetch is retried on the next regeneration. Assembler and isolated builds pass no cache, so they do not hold spec bodies in memory.Faster sidebar marking for every page
The marker that stamps
currenton the cached sidebar HTML now inspects tags as spans and applies all edits in one pass. Before, it allocated one string per tag and copied the whole sidebar once per edit. This code also runs for Markdown pages in isolated and assembler builds.Irregular spaces render as spaces
The space normalizer reused one shared Markdig inline node for every match. A second irregular space in a paragraph moved that node, so an earlier space disappeared. The result also depended on parse order across documents. Each match now gets its own node. In this repo's docs, 23 Elasticsearch API pages change:
placement — orused to render asplacement —or. The "irregular space" hint is now tracked per build instead of per process, so a serve reload or a second test with the same file name still gets the hint.Verify
A docs build from this branch matches one from #4321 once asset hashes are normalized. The only differences are the 23 pages with the thin-space fix.
🤖 Generated with Claude Code