Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Node.js performance using an AsyncIterable #9614

Merged
merged 16 commits into from
Feb 14, 2024
Merged

Conversation

matthewp
Copy link
Contributor

@matthewp matthewp commented Jan 4, 2024

Changes

  • In Node.js, use an AsyncIterable instead of a ReadableStream, as the latter is slow.

Testing

  • Benchmarks should be better.

Docs

N/A, bug fix

Copy link

changeset-bot bot commented Jan 4, 2024

🦋 Changeset detected

Latest commit: 2826812

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added pkg: astro Related to the core `astro` package (scope) pr: docs A PR that includes documentation for review labels Jan 4, 2024
@matthewp
Copy link
Contributor Author

matthewp commented Jan 4, 2024

!bench render

@matthewp
Copy link
Contributor Author

matthewp commented Jan 4, 2024

!bench render

Copy link
Contributor

github-actions bot commented Jan 4, 2024

PR Benchmark

Render

Page Avg (ms) Stdev (ms) Max (ms)
/astro 4.45 4.74 44.71
/md 0.69 2.24 22.82
/mdx 129.49 19.36 226.28

Main Benchmark

Render

Page Avg (ms) Stdev (ms) Max (ms)
/astro 4.38 4.16 34.33
/md 0.60 2.17 22.09
/mdx 130.09 20.30 214.27

@delucis
Copy link
Member

delucis commented Jan 5, 2024

FWIW I don’t expect !bench to reflect the benefits here — also saw the same in #9603 — as they only kick in beyond a certain threshold.

With the #9603 preview release the benefit kicked in once list items in one Array#map passed around 2000:

chart showing build time increasing exponentially as list size increases for current Astro and a more or less flat curve as list size increases with a preview release with streaming disabled

That benchmark also suggests a similar slight slowdown for smaller list sizes seen in the benchmark above:

sidebar items build time build time (preview release) % change
0 5,488 ms 6,981 ms +27%
1000 7,086 ms 8,202 ms +15%
2000 16,888 ms 8,770 ms -48%

Not sure if those are statistically significant though — just 10 runs each using hyperfine.

Would love to run this same benchmark on a preview release of this PR when it’s ready!

@matthewp
Copy link
Contributor Author

matthewp commented Jan 5, 2024

@delucis I'm going to update the benchmark (or create a new one, not sure), which shows the benefit.

@sarah11918 sarah11918 removed the pr: docs A PR that includes documentation for review label Jan 9, 2024

Improves Node.js streaming performance

This uses an AsyncIterable instead of a ReadableStream to do streaming in Node.js. This is a non-standard enhancement by Node, so this is done only in that environment.
Copy link
Member

Choose a reason for hiding this comment

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

What's the implication of this being "a non-standard enhancement by Node" and why does the reader care about this?

"Non-standard" doesn't always imply something positive, (is this a hack? are we jumping on to something experimental?) and in fact might make people worry/wonder. So this feels maybe a little too jargon-y when I'm assuming that this could actually be framed as something cool and helpful!

Just a thought!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It just means that it won't be run non-Node environments, for ex. in Cloudflare. Those will take the slower path.

Yeah it can be tricky, I'm not always sure who the target of these changesets are. A lot of changes are implementation details that most won't understand or care about, but some will. And they can be useful for understanding the change on the part of maintainers. But we don't usually do that.

Thinking... do you have a suggested way to rephrase this?

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Thank you! Looks good

@ematipico
Copy link
Member

!preview render-nodejs

Copy link
Contributor

Snapshots have been released for the following packages:

  • @astrojs/vercel@experimental--render-nodejs
  • astro@experimental--render-nodejs
Publish Log
🦋  warn ===============================IMPORTANT!===============================
🦋  warn Packages will be released under the experimental--render-nodejs tag
🦋  warn ----------------------------------------------------------------------
🦋  info npm info astro
🦋  info npm info @astrojs/prism
🦋  info npm info @astrojs/rss
🦋  info npm info create-astro
🦋  info npm info @astrojs/alpinejs
🦋  info npm info @astrojs/lit
🦋  info npm info @astrojs/markdoc
🦋  info npm info @astrojs/mdx
🦋  info npm info @astrojs/node
🦋  info npm info @astrojs/partytown
🦋  info npm info @astrojs/preact
🦋  info npm info @astrojs/react
🦋  info npm info @astrojs/sitemap
🦋  info npm info @astrojs/solid-js
🦋  info npm info @astrojs/svelte
🦋  info npm info @astrojs/tailwind
🦋  info npm info @astrojs/vercel
🦋  info npm info @astrojs/vue
🦋  info npm info @astrojs/internal-helpers
🦋  info npm info @astrojs/markdown-remark
🦋  info npm info @astrojs/telemetry
🦋  info npm info @astrojs/underscore-redirects
🦋  info npm info @astrojs/upgrade
🦋  info astro is being published because our local version (0.0.0-render-nodejs-20240213145612) has not been published on npm
🦋  warn @astrojs/prism is not being published because version 3.0.0 is already published on npm
🦋  warn @astrojs/rss is not being published because version 4.0.5 is already published on npm
🦋  warn create-astro is not being published because version 4.7.2 is already published on npm
🦋  warn @astrojs/alpinejs is not being published because version 0.4.0 is already published on npm
🦋  warn @astrojs/lit is not being published because version 4.0.1 is already published on npm
🦋  warn @astrojs/markdoc is not being published because version 0.9.0 is already published on npm
🦋  warn @astrojs/mdx is not being published because version 2.1.1 is already published on npm
🦋  warn @astrojs/node is not being published because version 8.2.0 is already published on npm
🦋  warn @astrojs/partytown is not being published because version 2.0.4 is already published on npm
🦋  warn @astrojs/preact is not being published because version 3.1.0 is already published on npm
🦋  warn @astrojs/react is not being published because version 3.0.10 is already published on npm
🦋  warn @astrojs/sitemap is not being published because version 3.0.5 is already published on npm
🦋  warn @astrojs/solid-js is not being published because version 4.0.1 is already published on npm
🦋  warn @astrojs/svelte is not being published because version 5.0.3 is already published on npm
🦋  warn @astrojs/tailwind is not being published because version 5.1.0 is already published on npm
🦋  info @astrojs/vercel is being published because our local version (0.0.0-render-nodejs-20240213145612) has not been published on npm
🦋  warn @astrojs/vue is not being published because version 4.0.8 is already published on npm
🦋  warn @astrojs/internal-helpers is not being published because version 0.2.1 is already published on npm
🦋  warn @astrojs/markdown-remark is not being published because version 4.2.1 is already published on npm
🦋  warn @astrojs/telemetry is not being published because version 3.0.4 is already published on npm
🦋  warn @astrojs/underscore-redirects is not being published because version 0.3.3 is already published on npm
🦋  warn @astrojs/upgrade is not being published because version 0.2.2 is already published on npm
🦋  info Publishing "astro" at "0.0.0-render-nodejs-20240213145612"
🦋  info Publishing "@astrojs/vercel" at "0.0.0-render-nodejs-20240213145612"
🦋  success packages published successfully:
🦋  [email protected]
🦋  @astrojs/[email protected]
🦋  Creating git tags...
🦋  New tag:  [email protected]
🦋  New tag:  @astrojs/[email protected]
Build Log

> [email protected] build /home/runner/work/astro/astro
> turbo run build --filter=astro --filter=create-astro --filter="@astrojs/*" --filter="@benchmark/*"

• Packages in scope: @astrojs/alpinejs, @astrojs/cloudflare, @astrojs/internal-helpers, @astrojs/lit, @astrojs/markdoc, @astrojs/markdown-remark, @astrojs/mdx, @astrojs/netlify, @astrojs/node, @astrojs/partytown, @astrojs/preact, @astrojs/prism, @astrojs/react, @astrojs/rss, @astrojs/sitemap, @astrojs/solid-js, @astrojs/svelte, @astrojs/tailwind, @astrojs/telemetry, @astrojs/underscore-redirects, @astrojs/upgrade, @astrojs/vercel, @astrojs/vue, @benchmark/timer, astro, create-astro
• Running build in 26 packages
• Remote caching enabled
::group::@astrojs/internal-helpers:build
cache miss, executing cafe614d8f90f991

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/internal-helpers
> astro-scripts build "src/**/*.ts" && tsc -p tsconfig.json

::endgroup::
::group::@astrojs/telemetry:build
cache miss, executing 96b68332b9a39aa4

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/telemetry
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::create-astro:build
cache miss, executing 7db6df91899ada84

> [email protected] build /home/runner/work/astro/astro/packages/create-astro
> astro-scripts build "src/index.ts" --bundle && tsc

::endgroup::
::group::@astrojs/prism:build
cache miss, executing ee171735e01c46d0

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/astro-prism
> astro-scripts build "src/**/*.ts" && tsc -p ./tsconfig.json

::endgroup::
::group::@astrojs/upgrade:build
cache miss, executing 17b33db66429ea75

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/upgrade
> astro-scripts build "src/index.ts" --bundle && tsc

::endgroup::
::group::@astrojs/markdown-remark:build
cache miss, executing 51686ee3b9f52c0a

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/markdown/remark
> astro-scripts build "src/**/*.ts" && tsc -p tsconfig.json

::endgroup::
::group::astro:build
cache miss, executing d47a0927e1df8c17

> [email protected] build /home/runner/work/astro/astro/packages/astro
> pnpm run prebuild && astro-scripts build "src/**/*.{ts,js}" && tsc && pnpm run postbuild


> [email protected] prebuild /home/runner/work/astro/astro/packages/astro
> astro-scripts prebuild --to-string "src/runtime/server/astro-island.ts" "src/runtime/client/{idle,load,media,only,visible}.ts"


> [email protected] postbuild /home/runner/work/astro/astro/packages/astro
> astro-scripts copy "src/**/*.astro" && astro-scripts copy "src/**/*.wasm"

::endgroup::
::group::@astrojs/underscore-redirects:build
cache miss, executing d34706d5fb93fe45

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/underscore-redirects
> astro-scripts build "src/**/*.ts" && tsc -p tsconfig.json

::endgroup::
::group::@astrojs/rss:build
cache miss, executing dc22ff9b204296fb

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/astro-rss
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@benchmark/timer:build
cache miss, executing fc16085dc5a926e1

> @benchmark/[email protected] build /home/runner/work/astro/astro/benchmark/packages/timer
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/partytown:build
cache miss, executing 9238f80a27dead70

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/partytown
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/solid-js:build
cache miss, executing 79a3e33328fa5b49

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/solid
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/node:build
cache miss, executing 2134776a53157f5d

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/node
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/mdx:build
cache miss, executing 3ab19c2bbc22e277

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/mdx
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/vercel:build
cache miss, executing 681f64f77becf249

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/vercel
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/preact:build
cache miss, executing 10b77d63ce5d043b

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/preact
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/svelte:build
cache miss, executing ddab6df6d522ffaf

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/svelte
> astro-scripts build "src/index.ts" && astro-scripts build "src/editor.cts" --force-cjs --no-clean-dist && tsc

::endgroup::
::group::@astrojs/lit:build
cache miss, executing 120a9604f9536977

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/lit
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/alpinejs:build
cache miss, executing 0bd590fd26a551a2

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/alpinejs
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/react:build
cache miss, executing 7b0bf11b791e6be8

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/react
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/tailwind:build
cache miss, executing 645b8fe5cc99d178

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/tailwind
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/sitemap:build
cache miss, executing 4f37a961915ecf80

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/sitemap
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/markdoc:build
cache miss, executing 1043bc51c0266fce

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/markdoc
> astro-scripts build "src/**/*.ts" && tsc

::endgroup::
::group::@astrojs/vue:build
cache miss, executing dafbdf9f72b15f37

> @astrojs/[email protected] build /home/runner/work/astro/astro/packages/integrations/vue
> astro-scripts build "src/index.ts" && astro-scripts build "src/editor.cts" --force-cjs --no-clean-dist && tsc

::endgroup::

 Tasks:    24 successful, 24 total
Cached:    0 cached, 24 total
  Time:    47.608s 

@delucis
Copy link
Member

delucis commented Feb 13, 2024

Ran the same benchmark as mentioned in #9614 (comment) using the prerelease and saw a similar improvement to benchmarks for #9603: build time roughly doubles from 5s to 10s as sidebar size increases from 0 to 7,000 items. With the public release, build time increased c. 20x for the same sidebar size.

graph showing prerelease build times almost flat while public release build time increases exponentially as sidebar size increases

@github-actions github-actions bot added the semver: minor Change triggers a `minor` release label Feb 14, 2024
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is blocked because it contains a minor changeset. A reviewer will merge this at the next release if approved.

@matthewp matthewp added this to the 4.4 milestone Feb 14, 2024
@matthewp matthewp merged commit d469beb into main Feb 14, 2024
13 checks passed
@matthewp matthewp deleted the async-iterator branch February 14, 2024 15:14
@astrobot-houston astrobot-houston mentioned this pull request Feb 14, 2024
}

// Create a new array with total length and merge all source arrays.
let mergedArray = new Uint8Array(length);

Choose a reason for hiding this comment

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

Did you consider using Buffer.concat? In Bun, we've optimized Buffer.concat a lot, it should be meaningfully faster than this approach.

If for some reason Buffer.concat cannot be used, Buffer.allocUnsafe should also be faster here - there's no risk of uninitialized memory leaking since the full length is counted and will not change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No I did not know that it would be faster. Would be happy to take a PR on this, we have benchmarks we can compare.

};

const destination: RenderDestination = {
write(chunk) {

Choose a reason for hiding this comment

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

Would you be open to a PR that makes this fast in Bun as well?

It would use [Symbol.asyncIterator] and you can yield either ArrayBufferView or string literals, so you can skip the encode/decode step (if I'm reading this code correctly). This would make faster and use a lot less memory.

Then the iterator function would essentially be:

// earlier
let { promise, resolve, reject } = Promise.withResolvers();
let pendingChunks = [];

// inside the iterator
async function* iterator() {
  while (true) {
    await promise;
    ({ promise, resolve, reject } = Promise.withResolvers());
    const chunks = pendingChunks;
    pendingChunks = [];
    yield* chunks;
    if (chunks.length === 0) return;
  }
}

const destination: RenderDestination = {
   write(chunk) {
       // ....

Copy link
Contributor Author

@matthewp matthewp Feb 16, 2024

Choose a reason for hiding this comment

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

Thanks, assuming this would need to conditionally run only in Bun we'd like to hold off on that for now. I didn't particularly want to do the change in this PR but the difference was too big to ignore.

Copy link

Choose a reason for hiding this comment

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

I've not looked into performance, but seems you could even return an async generator of strings directly. I don't see why you would need to construct the async iterator yourself, and why you need to mess around with that buffer at all, but perhaps I'm missing something? Returning the generator would be a lot less code, and the following works in Node.js:

async function* renderToAsyncIterable() {
  yield await Promise.resolve('a');
  yield await Promise.resolve('b');
  yield await Promise.resolve('c');
}

const res = new Response(renderToAsyncIterable())
await res.text()

Note that you can also use for await...of to iterate over async iterables when needed.

Copy link
Member

Choose a reason for hiding this comment

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

In my past refactors, I found that generators tend to be slow. So I think we should avoid it even if it may be easier to read.

Copy link

Choose a reason for hiding this comment

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

@bluwy right... seems like generators are 1.8-2x slower than doing it manually (at least in node.js), but seems that they scale equally. But that still leaves the question why not to return a AsyncIterable<string | Uint8Array> and avoid creating another buffer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: astro Related to the core `astro` package (scope) semver: minor Change triggers a `minor` release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants