Skip to content

lib: optimize async context frame activation - #65519

Open
pimterry wants to merge 1 commit into
nodejs:mainfrom
pimterry:optimize-async-context-frame
Open

lib: optimize async context frame activation#65519
pimterry wants to merge 1 commit into
nodejs:mainfrom
pimterry:optimize-async-context-frame

Conversation

@pimterry

Copy link
Copy Markdown
Member

Async context frame is activated lazily. The current implementation did this by waiting for the first call to checkEnabled, and then changing the prototype of AsyncContextFrame to ActiveAsyncContextFrame if enabled. This overrides the current, set, exchange and disable methods to the active versions.

Swapping the prototype like this makes the replaced methods slow. My best understanding is that V8 caches the lookup for the methods on their prototype when they're used initially, and when the prototype is changed (lazily later, via various triggers but notably including socket timeout setup for any server) those caches are lost, and never recover, making every use of those methods about 8x slower. This relates to AsyncLocalStorage, but it doesn't require actually using it - it applies to basically everybody.

That's bad because we call these methods a lot: every tick, immediate & timer. Assigning the methods instead of modifying the prototype avoids this slowdown.

I found this exploring HTTP/1 performance: this tiny change immediately boosts HTTP perf for the http/simple benchmark by about 5%, with no user-visible changes, by dropping overhead on all the ticks involved. I'll do a benchmark run to confirm impact in a minute.

It should also improve performance in plenty of other places too though, really any code heavily using timers or nextTick: as an extreme case, I see pure timer benchmarks like timers-timeout-nexttick jump up to 70% with this change. Micro-optimization, but in very hot & widely used code.

No downside AFAICT. Actual change is very simple (replace ObjectSetPrototypeOf with 4x method assignments) but I've simplified the surrounding code en route (dropped the ActiveAsyncContextFrame class completely, since it's now just loose methods) and moved checkEnabled to the end of the file, which means we can drop the eslint-disable for use-before-define.

Signed-off-by: Tim Perry <pimterry@gmail.com>
@pimterry
pimterry requested a review from Qard August 24, 2026 17:22
@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Aug 24, 2026
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.16%. Comparing base (f509cf1) to head (73ad7b6).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65519      +/-   ##
==========================================
+ Coverage   90.14%   90.16%   +0.01%     
==========================================
  Files         751      751              
  Lines      253585   253579       -6     
  Branches    47772    47786      +14     
==========================================
+ Hits       228596   228637      +41     
+ Misses      16228    16200      -28     
+ Partials     8761     8742      -19     
Files with missing lines Coverage Δ
lib/internal/async_context_frame.js 100.00% <100.00%> (ø)

... and 28 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants