Skip to content

Commit

Permalink
src: add new underlying async context tracking
Browse files Browse the repository at this point in the history
Implements the core underlying bits of a refactored
AsyncLocalStorage implementation based on the same
mechanism that will be used for the standard AsyncContext
API. The implementation is based on the new continuation
preserved embedder data API in v8 and is designed to be
significantly faster and more efficient that the current
promise-hook based implementation.

This intentionally only adds the basic pieces, with the
intent of spreading the refactored implementation across
multiple PRs just to make things easier to review.

The basic idea is to implement AsyncLocalStorage like:

```js
const { internalBinding } = require('internal/test/binding');
const {
  get: get_,
  run: run_,
  runWithin: runWithin_,
  snapshot: snapshot_,
  enterWith: enterWith_,
} = internalBinding('async_context')

class AsyncLocalStorage {
  #key = Symbol();

  run(value, fn, ...args) {
    return run_(this.#key, value, () => fn(...args));
  }

  exit(fn, ...args) {
    return run_(this.#key, undefined, () => fn(...args));
  }

  getStore() {
    return get_(this.#key);
  }

  disable() {
    enterWith_(this.#key, undefined);
  }

  enterWith(value) {
    enterWith_(this.#key, value);
  }

  static snapshot() {
    const frame = snapshot_();
    return function(fn) {
      return runWithin_(frame, fn);
    };
  }

  static bind(fn) {
    return AsyncLocalStorage.snapshot().bind(undefined, fn);
  }
}
```
  • Loading branch information
jasnell committed Apr 29, 2024
1 parent 7c3dce0 commit add0970
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 0 deletions.
71 changes: 71 additions & 0 deletions lib/internal/async_context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';

const {
Symbol,
} = primordials;

const {
setAsyncContextFrameScopeCallback,
runWithin: runWithin_,
run: run_,
enter: enter_,
} = internalBinding('async_context');

const assert = require('internal/assert');

// Whenever the current frame is set, the C++ side will call our callback here
// to update the value of currentFrame. This is to avoid having to call into
// C++ every time we just want to cache ths shapshot. We'll see need to call
// down to C++ to enter the context or to set the context value.
const kNotSet = Symbol();
let currentFrame = kNotSet;
setAsyncContextFrameScopeCallback((frame) => {
currentFrame = frame;
});
assert(currentFrame !== kNotSet);

function isValidCurrentFrame() {
return (typeof currentFrame === 'object' && currentFrame !== null);
}

class AsyncLocalStorage {
#key = Symbol();

run(value, fn, ...args) {
return run_(this.#key, value, () => fn(...args));
}

exit(fn, ...args) {
return run_(this.#key, undefined, () => fn(...args));
}

getStore() {
return currentFrame[this.#key];
}

disable() {
if (isValidCurrentFrame()) {
currentFrame[this.#key] = undefined;
}
}

enterWith(value) {
currentFrame ??= enter_();
assert(isValidCurrentFrame());
currentFrame[this.#key] = value;
}

static snapshot() {
return function(fn) {
return runWithin_(currentFrame, fn);
};
}

static bind(fn) {
return AsyncLocalStorage.snapshot().bind(undefined, fn);
}
}

module.exports = {
AsyncLocalStorage,
};
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
'src/api/exceptions.cc',
'src/api/hooks.cc',
'src/api/utils.cc',
'src/async_context.cc',
'src/async_wrap.cc',
'src/base_object.cc',
'src/cares_wrap.cc',
Expand Down
180 changes: 180 additions & 0 deletions src/async_context.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "v8-function-callback.h"
#include "v8-function.h"

namespace node {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Local;
using v8::Object;
using v8::Symbol;
using v8::Undefined;
using v8::Value;

namespace async_context_frame {

namespace {
Local<Value> CopyCurrentOrCreate(Environment* env,
Local<Symbol> key,
Local<Value> value) {
auto current = env->isolate()->GetContinuationPreservedEmbedderData();
CHECK(!current.IsEmpty());
Local<Object> frame;
if (!current->IsObject()) {
CHECK(current->IsUndefined());
v8::Local<v8::Name> name = key;
return Object::New(
env->isolate(), v8::Null(env->isolate()), &name, &value, 1);
}
CHECK(current->IsObject());
frame = current.As<Object>()->Clone();
USE(frame->Set(env->context(), key, value));
return frame;
}

class Scope final {
public:
Scope(Environment* env, Local<Value> frame)
: env_(env),
prev_(env->isolate()->GetContinuationPreservedEmbedderData()) {
CHECK(!frame.IsEmpty());
CHECK(prev_->IsUndefined() || prev_->IsObject());
Debug(env,
DebugCategory::ASYNC_CONTEXT,
"Entering AsyncContextFrame::Scope\n");
if (frame->IsObject()) {
env_->isolate()->SetContinuationPreservedEmbedderData(frame);
Notify(env, frame);
} else {
CHECK(frame->IsUndefined());
env_->isolate()->SetContinuationPreservedEmbedderData(
Undefined(env->isolate()));
Notify(env, Undefined(env->isolate()));
}
}
~Scope() {
Debug(env_,
DebugCategory::ASYNC_CONTEXT,
"Leaving AsyncContextFrame::Scope\n");
env_->isolate()->SetContinuationPreservedEmbedderData(prev_);
}

static void Notify(Environment* env, v8::Local<v8::Value> frame) {
if (!env->async_context_frame_scope().IsEmpty()) {
USE(env->async_context_frame_scope()->Call(
env->context(), Undefined(env->isolate()), 1, &frame));
}
}

Scope(const Scope&) = delete;
Scope(Scope&&) = delete;
Scope& operator=(const Scope&) = delete;
Scope& operator=(Scope&&) = delete;

void* operator new(size_t) = delete;
void* operator new[](size_t) = delete;
void operator delete(void*) = delete;
void operator delete[](void*) = delete;

private:
Environment* env_;
Local<Value> prev_;
};
} // namespace

// ============================================================================

void RunWithin(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
if (!args[0]->IsObject() && !args[0]->IsUndefined()) {
THROW_ERR_INVALID_ARG_TYPE(env,
"first argument must be an object or undefined");
return;
}
if (!args[1]->IsFunction()) {
THROW_ERR_INVALID_ARG_TYPE(env, "second argument must be a function");
return;
}
Scope scope(env, args[0]);
Local<Value> ret;
if (args[1]
.As<Function>()
->Call(env->context(), Undefined(env->isolate()), 0, nullptr)
.ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}

void Run(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
if (!args[0]->IsSymbol()) {
THROW_ERR_INVALID_ARG_TYPE(env, "first argument must be a symbol");
}
if (!args[2]->IsFunction()) {
THROW_ERR_INVALID_ARG_TYPE(env, "second argument must be a function");
return;
}

auto key = args[0].As<Symbol>();
auto function = args[2].As<Function>();
Scope scope(env, CopyCurrentOrCreate(env, key, args[1]));
Local<Value> ret;
if (function->Call(env->context(), Undefined(env->isolate()), 0, nullptr)
.ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}

void Enter(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
auto frame = env->isolate()->GetContinuationPreservedEmbedderData();
// If this is being called, then we should be in the empty frame,
// so frame should be undefined.
CHECK(frame->IsUndefined());
auto obj = v8::Object::New(
env->isolate(), v8::Null(env->isolate()), nullptr, nullptr, 0);
env->isolate()->SetContinuationPreservedEmbedderData(obj);
args.GetReturnValue().Set(obj);
}

void SetAsyncContextFrameScopeCallback(
const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
CHECK(args[0]->IsFunction());
CHECK(env->async_context_frame_scope().IsEmpty());
env->set_async_context_frame_scope(args[0].As<Function>());
Scope::Notify(env, env->isolate()->GetContinuationPreservedEmbedderData());
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(SetAsyncContextFrameScopeCallback);
registry->Register(RunWithin);
registry->Register(Run);
registry->Register(Enter);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
SetMethod(context,
target,
"setAsyncContextFrameScopeCallback",
SetAsyncContextFrameScopeCallback);
SetMethod(context, target, "runWithin", RunWithin);
SetMethod(context, target, "run", Run);
SetMethod(context, target, "enter", Enter);
}

} // namespace async_context_frame
} // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(async_context,
node::async_context_frame::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(
async_context, node::async_context_frame::RegisterExternalReferences)
1 change: 1 addition & 0 deletions src/debug_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str);
// from a provider type to a debug category.
#define DEBUG_CATEGORY_NAMES(V) \
NODE_ASYNC_PROVIDER_TYPES(V) \
V(ASYNC_CONTEXT) \
V(COMPILE_CACHE) \
V(DIAGNOSTICS) \
V(HUGEPAGES) \
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@
V(async_hooks_destroy_function, v8::Function) \
V(async_hooks_init_function, v8::Function) \
V(async_hooks_promise_resolve_function, v8::Function) \
V(async_context_frame_scope, v8::Function) \
V(buffer_prototype_object, v8::Object) \
V(crypto_key_object_constructor, v8::Function) \
V(crypto_key_object_private_constructor, v8::Function) \
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
// The binding IDs that start with 'internal_only' are not exposed to the user
// land even from internal/test/binding module under --expose-internals.
#define NODE_BUILTIN_STANDARD_BINDINGS(V) \
V(async_context) \
V(async_wrap) \
V(blob) \
V(block_list) \
Expand Down
1 change: 1 addition & 0 deletions src/node_external_reference.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class ExternalReferenceRegistry {
};

#define EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \
V(async_context) \
V(async_wrap) \
V(binding) \
V(blob) \
Expand Down

0 comments on commit add0970

Please sign in to comment.