Skip to content

lib: add a diagnostic channel for observing AsyncContextFrame.set #58229

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

szegedi
Copy link
Contributor

@szegedi szegedi commented May 8, 2025

Adds an async_context_frame.set diagnostic channel for observing AsyncContextFrame.set.

This is helpful for functionality in extensions written in C++ that need to keep some derived native state from one or more AsyncLocalStorages in sync with changes in active frame. It is possible that such an extension needs to use the state when it can not enter V8 to obtain the data through AsyncLocalStorage JS API. A subscriber to this diagnostic channel can be used instead to eagerly keep the native state updated.

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label May 8, 2025
Copy link
Member

@Qard Qard left a comment

Choose a reason for hiding this comment

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

You should also add the channel to the diagnostics_channel docs here: https://github.com/nodejs/node/blob/main/doc/api/diagnostics_channel.md?plain=1#L1104

Copy link

codecov bot commented May 8, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 90.23%. Comparing base (b197355) to head (1dc7cfd).

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #58229   +/-   ##
=======================================
  Coverage   90.23%   90.23%           
=======================================
  Files         633      633           
  Lines      186818   186822    +4     
  Branches    36668    36667    -1     
=======================================
+ Hits       168578   168587    +9     
- Misses      11036    11040    +4     
+ Partials     7204     7195    -9     
Files with missing lines Coverage Δ
lib/internal/async_context_frame.js 100.00% <100.00%> (ø)

... and 24 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.

@szegedi szegedi requested a review from Qard May 8, 2025 12:40
Copy link
Member

@Qard Qard left a comment

Choose a reason for hiding this comment

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

Might also be a good idea to have the tests validate that the frame can be used to look up the state for a given store. The store is the key so it should just be frame.get(store) to get the value.

@szegedi szegedi requested a review from Qard May 8, 2025 13:08
@Qard Qard added request-ci Add this label to start a Jenkins CI on a PR. diagnostics_channel Issues and PRs related to diagnostics channel async_local_storage AsyncLocalStorage doc Issues and PRs related to the documentations. and removed needs-ci PRs that need a full CI run. labels May 8, 2025
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label May 8, 2025
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@@ -23,6 +26,7 @@ class ActiveAsyncContextFrame extends SafeMap {

static set(frame) {
setContinuationPreservedEmbedderData(frame);
onSet.publish(frame);
Copy link
Member

Choose a reason for hiding this comment

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

Should we expose/document the AsyncContextFrame class via public API?
With doing just this users get some undocumented, internal thing into their hand and all they can do is to guess what it is, which API it exposes.
Any internal change on AsyncContextFrame would be breaking for all users of this channel.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I discussed this with @Qard; he thinks (but can of course speak for himself here 😁) that since all channels are by default experimental, we aren't promising stability in what's returned here. As far as I'm concerned, I would've been fine with not even exposing the parameter at all.

Copy link
Member

Choose a reason for hiding this comment

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

One of the probles to never stabilize async_hooks was because it leaks internal.
We should avoid to add again experimental APIs leaking internal as they can't be stabilized.

If the frame is not needed it's likely best to remove it. Alternative could be to replace it by a well documented wrapper object exposing only parts of AsyncContextFrame or other data like the cause for the set,...


* `frame` {Object}

Emitted when the async context frame for the current execution context is set.
Copy link
Member

Choose a reason for hiding this comment

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

Does this hold true for await?
As far as I remember await or more concrete for Promises the activation/setting happens by v8.

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, an event won't be published on await continuation. If we were to implement that, we'd be right back to using Isolate::SetPromiseHook. Fortunately, it is not necessary. V8 appropriately propagates whatever is set as the Continuation Preserved Embedder Data (CPED) into the "then" callbacks. I was a bit vague when saying "native state is kept in sync with change in the active frame" but the idea really is that the derived native state itself should be stored in and retrieved from the AsyncContextFrame (ACF) object, e.g. by defining a private symbol-keyed property on it. This way when V8 changes current ACF as its CPED, the native state automatically tracks and we "only" need to observe when Node sets ACF in CPED for the current execution to ensure the native state in the current ACF object is up to date.
Unsurprisingly, I'm working on doing exactly this in the Datadog's profiler if you want to see a C++ example :-)

Copy link
Member

Choose a reason for hiding this comment

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

Are you really interested in the set() calls?
If I understand this correct it's actually the creation of a new AsyncContextFrame - so the ALS.enterWith() and run() calls which are of your interest but not the ACF.exchange() calls.

exchange() is called by e.g. timers, nextTick,... - to mimic what v8 does automatically for promises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async_local_storage AsyncLocalStorage diagnostics_channel Issues and PRs related to diagnostics channel doc Issues and PRs related to the documentations.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants