node icon indicating copy to clipboard operation
node copied to clipboard

process: preserve AsyncLocalStorage on queueMicrotask only when needed

Open gurgunday opened this issue 3 weeks ago • 8 comments

Does the same optimization as https://github.com/nodejs/node/pull/59873 for queueMicrotask

branch:

./node benchmark/run.js --filter queue-microtask process

process/queue-microtask-breadth.js
process/queue-microtask-breadth.js n=400000: 18,938,347.488090977

process/queue-microtask-depth.js
process/queue-microtask-depth.js n=1200000: 35,862,646.06556887

main

./node benchmark/run.js --filter queue-microtask process

process/queue-microtask-breadth.js
process/queue-microtask-breadth.js n=400000: 5,198,220.087856157

process/queue-microtask-depth.js
process/queue-microtask-depth.js n=1200000: 12,707,696.847598467

gurgunday avatar Nov 30 '25 20:11 gurgunday

Codecov Report

:white_check_mark: All modified and coverable lines are covered by tests. :white_check_mark: Project coverage is 88.58%. Comparing base (6274eb7) to head (2f8776e). :warning: Report is 80 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #60913      +/-   ##
==========================================
+ Coverage   88.55%   88.58%   +0.02%     
==========================================
  Files         703      703              
  Lines      208291   208296       +5     
  Branches    40170    40166       -4     
==========================================
+ Hits       184443   184509      +66     
+ Misses      15874    15807      -67     
- Partials     7974     7980       +6     
Files with missing lines Coverage Δ
lib/internal/process/task_queues.js 100.00% <100.00%> (ø)

... and 48 files with indirect coverage changes

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

codecov[bot] avatar Nov 30 '25 21:11 codecov[bot]

cc @nodejs/performance

WDYT?

gurgunday avatar Dec 05 '25 20:12 gurgunday

Before merging, some concerns raised in another PR with same changes:

One thing worth noting is that this would be a breaking change as currently the AsyncLocalStorage.bind(...) will always bind, even if there are no hooks or stores in use, but async_hooks could start listening at any time. In the current model you could start listening after the bind call there but before the callback is called and you would see the events. After this change you would not. By @Qard

Would mind giving some thoughts on this one, @mcollina and @Qard?

Edit: Benchmark: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1770/

H4ad avatar Dec 06 '25 00:12 H4ad

Failed to start CI
   ⚠  No approving reviews found
   ✘  Refusing to run CI on potentially unsafe PR
https://github.com/nodejs/node/actions/runs/19980375054

github-actions[bot] avatar Dec 06 '25 01:12 github-actions[bot]

@H4ad Yes, the same concern applies. It is, in my opinion, problematic that it was ever possible to start seeing events for a given task partway through its lifecycle rather than deciding at emit time if any of the events should be present. But that is certainly a breaking change and so should be marked as such. A desirable change, I think. But a breaking one nonetheless.

At the same time though, async_hooks is still marked as experimental, and AsyncLocalStorage no longer relies on it, so the break is likely a lot less critical. We may be able to allow it without a major bump. I'm not strongly opinionated either way.

Qard avatar Dec 06 '25 07:12 Qard

I'm ok in this being a minor, but I don't think we can backport it to 22.

mcollina avatar Dec 06 '25 09:12 mcollina

Benchmark result:

                                                                                                                            confidence improvement accuracy (*)    (**)   (***)
process/bench-env.js operation='delete' n=1000000                                                                                           0.89 %       ±5.64%  ±7.51%  ±9.77%
process/bench-env.js operation='enumerate' n=1000000                                                                                        1.65 %       ±3.36%  ±4.47%  ±5.82%
process/bench-env.js operation='get' n=1000000                                                                                             -0.79 %       ±1.68%  ±2.24%  ±2.91%
process/bench-env.js operation='query' n=1000000                                                                                            1.36 %       ±2.64%  ±3.51%  ±4.57%
process/bench-env.js operation='set' n=1000000                                                                                              0.90 %       ±3.45%  ±4.59%  ±5.98%
process/bench-hrtime.js type='bigint' n=1000000                                                                                            -1.11 %       ±4.98%  ±6.62%  ±8.62%
process/bench-hrtime.js type='diff' n=1000000                                                                                        *     -3.27 %       ±3.21%  ±4.27%  ±5.57%
process/bench-hrtime.js type='raw' n=1000000                                                                                               -0.25 %       ±3.02%  ±4.02%  ±5.24%
process/coverage.js n=100000                                                                                                                0.03 %       ±1.10%  ±1.47%  ±1.92%
process/getActiveResourcesInfo.js n=100000 immediatesCount=10000 timeoutsCount=10000 requestsCount=10000 handlesCount=10000                -1.21 %       ±5.47%  ±7.28%  ±9.48%
process/memoryUsage.js n=100000                                                                                                             0.18 %       ±0.41%  ±0.55%  ±0.71%
process/next-tick-breadth-args.js n=10000000                                                                                                0.05 %       ±0.35%  ±0.46%  ±0.60%
process/next-tick-breadth.js n=10000000                                                                                                     0.01 %       ±0.75%  ±1.00%  ±1.30%
process/next-tick-depth-args.js n=7000000                                                                                                  -0.79 %       ±1.24%  ±1.65%  ±2.14%
process/next-tick-depth.js n=7000000                                                                                                        0.48 %       ±1.89%  ±2.51%  ±3.27%
process/next-tick-exec-args.js n=4000000                                                                                                   -0.02 %       ±8.70% ±11.57% ±15.06%
process/next-tick-exec.js n=4000000                                                                                                        -0.03 %       ±0.17%  ±0.23%  ±0.30%
process/next-tick-loop-args.js loop=100 n=10000                                                                                            -0.84 %       ±2.36%  ±3.14%  ±4.08%
process/next-tick-loop-args.js loop=100 n=20000                                                                                            -0.43 %       ±0.87%  ±1.16%  ±1.51%
process/next-tick-loop-args.js loop=100 n=40000                                                                                             0.35 %       ±0.58%  ±0.78%  ±1.01%
process/next-tick-loop-args.js loop=200 n=10000                                                                                             0.29 %       ±1.80%  ±2.40%  ±3.12%
process/next-tick-loop-args.js loop=200 n=20000                                                                                             0.29 %       ±0.80%  ±1.07%  ±1.39%
process/next-tick-loop-args.js loop=200 n=40000                                                                                            -0.50 %       ±0.64%  ±0.85%  ±1.10%
process/next-tick-loop.js loop=100 n=10000                                                                                                 -0.73 %       ±1.50%  ±2.00%  ±2.61%
process/next-tick-loop.js loop=100 n=20000                                                                                                 -0.02 %       ±1.85%  ±2.46%  ±3.21%
process/next-tick-loop.js loop=100 n=40000                                                                                                 -0.24 %       ±1.29%  ±1.72%  ±2.24%
process/next-tick-loop.js loop=200 n=10000                                                                                                 -0.41 %       ±1.30%  ±1.73%  ±2.26%
process/next-tick-loop.js loop=200 n=20000                                                                                                 -0.27 %       ±1.40%  ±1.87%  ±2.43%
process/next-tick-loop.js loop=200 n=40000                                                                                                  0.19 %       ±1.12%  ±1.49%  ±1.94%
process/queue-microtask-breadth.js n=400000                                                                                        ***    221.40 %       ±5.59%  ±7.53%  ±9.98%
process/queue-microtask-depth.js n=1200000                                                                                         ***    137.38 %       ±5.80%  ±7.81% ±10.33%
process/resourceUsage.js n=100000                                                                                                          -0.67 %       ±2.84%  ±3.77%  ±4.91%

Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 32 comparisons, you can thus
expect the following amount of false-positive results:
  1.60 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.32 false positives, when considering a   1% risk acceptance (**, ***),
  0.03 false positives, when considering a 0.1% risk acceptance (***)

Impressive 🚀

H4ad avatar Dec 06 '25 22:12 H4ad

CI: https://ci.nodejs.org/job/node-test-pull-request/70406/

nodejs-github-bot avatar Dec 06 '25 22:12 nodejs-github-bot

At the same time though, async_hooks is still marked as experimental, and AsyncLocalStorage no longer relies on it, so the break is likely a lot less critical. We may be able to allow it without a major bump. I'm not strongly opinionated either way.

The linked PR https://github.com/nodejs/node/pull/59873 was marked semver major.

Flarna avatar Dec 10 '25 20:12 Flarna