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

lib: rewrite AsyncLocalStorage without async_hooks #48528

Merged
merged 1 commit into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ function runInAsyncScopes(resourceCount, cb, i = 0) {

function main({ n, resourceCount }) {
const store = new AsyncLocalStorage();
runInAsyncScopes(resourceCount, () => {
bench.start();
runBenchmark(store, n);
bench.end(n);
store.run({}, () => {
runInAsyncScopes(resourceCount, () => {
bench.start();
runBenchmark(store, n);
bench.end(n);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { AsyncLocalStorage } = require('async_hooks');
* - AsyncLocalStorage1.getStore()
*/
const bench = common.createBenchmark(main, {
sotrageCount: [1, 10, 100],
storageCount: [1, 10, 100],
n: [1e4],
});

Expand All @@ -34,8 +34,8 @@ function runStores(stores, value, cb, idx = 0) {
}
}

function main({ n, sotrageCount }) {
const stores = new Array(sotrageCount).fill(0).map(() => new AsyncLocalStorage());
function main({ n, storageCount }) {
const stores = new Array(storageCount).fill(0).map(() => new AsyncLocalStorage());
const contextValue = {};

runStores(stores, contextValue, () => {
Expand Down
16 changes: 16 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,21 @@ and `"` are usable.
It is possible to run code containing inline types by passing
[`--experimental-strip-types`][].

### `--experimental-async-context-frame`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Enables the use of AsyncLocalStorage backed by AsyncContextFrame rather than
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Enables the use of AsyncLocalStorage backed by AsyncContextFrame rather than
Enables the use of `AsyncLocalStorage` backed by `AsyncContextFrame` rather than

maybe even convert it to links.

the default implementation which relies on async\_hooks. This new model is
implemented very differently and so could have differences in how context data
flows within the application. As such, it is presently recommended to be sure
your application behaviour is unaffected by this change before using it in
production.

### `--experimental-default-type=type`

<!-- YAML
Expand Down Expand Up @@ -2905,6 +2920,7 @@ one is included in the list below.
* `--enable-network-family-autoselection`
* `--enable-source-maps`
* `--experimental-abortcontroller`
* `--experimental-async-context-frame`
* `--experimental-default-type`
* `--experimental-detect-module`
* `--experimental-eventsource`
Expand Down
119 changes: 14 additions & 105 deletions lib/async_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const {
NumberIsSafeInteger,
ObjectDefineProperties,
ObjectFreeze,
ObjectIs,
ReflectApply,
Symbol,
} = primordials;
Expand All @@ -30,6 +29,8 @@ const {
} = require('internal/validators');
const internal_async_hooks = require('internal/async_hooks');

const AsyncContextFrame = require('internal/async_context_frame');

// Get functions
// For userland AsyncResources, make sure to emit a destroy event when the
// resource gets gced.
Expand Down Expand Up @@ -158,6 +159,7 @@ function createHook(fns) {
// Embedder API //

const destroyedSymbol = Symbol('destroyed');
const contextFrameSymbol = Symbol('context_frame');

class AsyncResource {
constructor(type, opts = kEmptyObject) {
Expand All @@ -177,6 +179,8 @@ class AsyncResource {
throw new ERR_INVALID_ASYNC_ID('triggerAsyncId', triggerAsyncId);
}

this[contextFrameSymbol] = AsyncContextFrame.current();

const asyncId = newAsyncId();
this[async_id_symbol] = asyncId;
this[trigger_async_id_symbol] = triggerAsyncId;
Expand All @@ -201,12 +205,12 @@ class AsyncResource {
const asyncId = this[async_id_symbol];
emitBefore(asyncId, this[trigger_async_id_symbol], this);

const contextFrame = this[contextFrameSymbol];
const prior = AsyncContextFrame.exchange(contextFrame);
try {
const ret =
ReflectApply(fn, thisArg, args);

return ret;
return ReflectApply(fn, thisArg, args);
} finally {
AsyncContextFrame.set(prior);
if (hasAsyncIdStack())
emitAfter(asyncId);
}
Expand Down Expand Up @@ -270,110 +274,15 @@ class AsyncResource {
}
}

const storageList = [];
const storageHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
const currentResource = executionAsyncResource();
// Value of currentResource is always a non null object
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource, type);
}
},
});

class AsyncLocalStorage {
constructor() {
this.kResourceStore = Symbol('kResourceStore');
this.enabled = false;
}

static bind(fn) {
return AsyncResource.bind(fn);
}

static snapshot() {
return AsyncLocalStorage.bind((cb, ...args) => cb(...args));
}

disable() {
if (this.enabled) {
this.enabled = false;
// If this.enabled, the instance must be in storageList
ArrayPrototypeSplice(storageList,
ArrayPrototypeIndexOf(storageList, this), 1);
if (storageList.length === 0) {
storageHook.disable();
}
}
}

_enable() {
if (!this.enabled) {
this.enabled = true;
ArrayPrototypePush(storageList, this);
storageHook.enable();
}
}

// Propagate the context from a parent resource to a child one
_propagate(resource, triggerResource, type) {
const store = triggerResource[this.kResourceStore];
if (this.enabled) {
resource[this.kResourceStore] = store;
}
}

enterWith(store) {
this._enable();
const resource = executionAsyncResource();
resource[this.kResourceStore] = store;
}

run(store, callback, ...args) {
// Avoid creation of an AsyncResource if store is already active
if (ObjectIs(store, this.getStore())) {
return ReflectApply(callback, null, args);
}

this._enable();

const resource = executionAsyncResource();
const oldStore = resource[this.kResourceStore];

resource[this.kResourceStore] = store;

try {
return ReflectApply(callback, null, args);
} finally {
resource[this.kResourceStore] = oldStore;
}
}

exit(callback, ...args) {
if (!this.enabled) {
return ReflectApply(callback, null, args);
}
this.disable();
try {
return ReflectApply(callback, null, args);
} finally {
this._enable();
}
}

getStore() {
if (this.enabled) {
const resource = executionAsyncResource();
return resource[this.kResourceStore];
}
}
}

// Placing all exports down here because the exported classes won't export
// otherwise.
module.exports = {
// Public API
AsyncLocalStorage,
get AsyncLocalStorage() {
return AsyncContextFrame.enabled ?
require('internal/async_local_storage/native') :
require('internal/async_local_storage/async_hooks');
},
createHook,
executionAsyncId,
triggerAsyncId,
Expand Down
50 changes: 50 additions & 0 deletions lib/internal/async_context_frame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

const {
getContinuationPreservedEmbedderData,
setContinuationPreservedEmbedderData,
} = internalBinding('async_context_frame');

let enabled_;

class AsyncContextFrame extends Map {
constructor(store, data) {
super(AsyncContextFrame.current());
this.set(store, data);
}

static get enabled() {
enabled_ ??= require('internal/options')
.getOptionValue('--experimental-async-context-frame');
return enabled_;
}

static current() {
if (this.enabled) {
return getContinuationPreservedEmbedderData();
}
}

static set(frame) {
if (this.enabled) {
setContinuationPreservedEmbedderData(frame);
}
}

static exchange(frame) {
const prior = this.current();
this.set(frame);
return prior;
}

static disable(store) {
const frame = this.current();
frame?.disable(store);
}

disable(store) {
this.delete(store);
}
}

module.exports = AsyncContextFrame;
Loading
Loading