Skip to content

perf(output): write rendered pages with parallel async I/O#3105

Open
StoneCypher wants to merge 1 commit into
TypeStrong:masterfrom
StoneCypher:perf_26-05-26_async-page-writes
Open

perf(output): write rendered pages with parallel async I/O#3105
StoneCypher wants to merge 1 commit into
TypeStrong:masterfrom
StoneCypher:perf_26-05-26_async-page-writes

Conversation

@StoneCypher

Copy link
Copy Markdown
Contributor

What

The renderer's per-page write step in Renderer.renderDocument was using writeFileSync (sync I/O) called once per page, for 271 sequential writes on a typedoc-on-typedoc run. Sibling output plugins (IconsPlugin, JavascriptIndexPlugin, HierarchyPlugin, SitemapPlugin, NavigationPlugin) already use async writeFile + Promise.all. This PR brings the per-page write to the same pattern.

Why

CPU profile attributes 427 ms inclusive in writeFileSync called from renderDocument (out of ~1565 ms total per-page inclusive). On any SSD, 271 small writes (~5 KB each) pipeline well in parallel — much of that 427 ms can be absorbed by the OS / disk queue while the JavaScript main thread does nothing useful.

How

Same architectural pattern as the async-git PR (#3104): keep the sync event handler synchronous, queue the work, drain in parallel from the outer async wrapper.

  • Renderer gains a private pendingPageWrites: Array<{ filename, contents }> field.
  • renderDocument no longer calls writeFileSync — it pushes `{ filename, contents }` onto the queue.
  • Renderer.render drains the queue with Promise.allSettled after the page-render loop and before the existing postRenderAsyncJobs work. `Promise.allSettled` (not Promise.all) preserves the prior "log and continue" behavior — one failed write does not abort the others.

fs.writeFileSync calls elsewhere in the file (for .nojekyll and `CNAME`) are unaffected; those use a different import (import * as fs from \"fs\").

Perf impact

Estimated 250-400 ms median improvement on the typedoc-on-typedoc workload. Final measurement landing here when CI reproduces it on the reference machine.

Test plan

  • ESLint passes on the changed file (`--max-warnings 0`).
  • No new IDE diagnostics.
  • CI: existing converter / renderer snapshot tests still match byte-identical (the file contents of each page are unchanged; only the timing of their write differs).
  • CI: full mocha suite green across all supported Node versions.

Rollback

Revert this PR. The previous sync write path is fully self-contained — no shared API changes.

Stack context

Part of the typedoc speedup stack tracked by #3103. This PR is in Group B — independent of the async-git stack (Group A, #3104). It can land in any order relative to the other Group B PRs.

Blocks #3103

@@ -260,6 +260,8 @@ export class Renderer extends AbstractComponent<Application, RendererEvents> {

renderStartTime = -1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unnecessarily complicates things, instead of adding an extra field to the class it would be better to have renderDocument return the content to be written, then render is the only method that even has to consider this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would lose the asynchronous parallelism that creates the speed win here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning the string to be written, or the promise returned by writeFile, collecting that in an array in render, and waiting for it there would lose parallelism? I don't think so...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the last time i turned down someone's pr, i still thanked them for the effort, and for caring enough to put time into my work

i think maybe you don't recognize that the way you've treated someone for trying to help is unkind

your tool really has been a big help over the years. i had really hoped that you would let me keep it a viable tool for me.

my github actions got locked this month, and when i looked, it was four things: mac builds on PRs being charged something like 30x the price of linux minutes, a mutation library called stryker, running the full build chain on every platform, and typedoc.

initially typedoc was about 12% of my build time. i invested a bunch of effort cutting mac out of everything but the merge to main, i cut the full build down to a single runner and the rest down to just the compile and testing, and i gave stryker some valium.

at that point, typedoc on repaired repos became 85% of my build minutes. with my current pace of development, that still means that i'm going to get locked out of github actions halfway through each month.

i'm going to use my fork of typedoc for a while, on the hope that sooner or later you're going to say something to yourself like "hey, that guy's helped out before, he was just trying to help this time, and when i run the benchmark against a larger doc set, it really does speed typedoc up a lot."

but it seems like you've positioned this conversation in a way that you derive being the wise one in the room from looking at a pr and exasperatedly pointing out what you believe are small defects, and saying "that means the entire thing is trash," without considering what the benchmark says.

i guess all the hand-written PRs are flawless. perhaps the benchmark lied to me convincingly, like claude said.

my fork is fast enough to give me some space to look for alternatives. i don't want to; my internal uses of your doc generator have a very heavily customized theme, and i would hate to have to remake it somewhere else.

unfortunately, typedoc is performance problematic enough that it's currently costing me eighty dollars a month in extra github actions minutes, and it's getting worse as i automate. this isn't a spend i'm able to keep up with.

the patches were enough, but it seems you aren't interested.

maybe you'll change your mind, and say "actually, thanks for the effort." not even about the code, just about being friendly to someone who was trying to pitch in. but i don't expect it.

good luck moving forwards.

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.

2 participants