diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index dcb0be8f7eb6ba..e16c22112141e2 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -14,13 +14,9 @@ // This file is compiled as if it's wrapped in a function with arguments // passed by node::LoadEnvironment() -/* global process, bootstrappers, loaderExports, triggerFatalException */ +/* global process, loaderExports, triggerFatalException */ /* global isMainThread */ -const { - _setupNextTick, - _setupPromises -} = bootstrappers; const { internalBinding, NativeModule } = loaderExports; const exceptionHandlerState = { captureFn: null }; @@ -105,8 +101,18 @@ function startup() { } NativeModule.require('internal/process/warning').setup(); - NativeModule.require('internal/process/next_tick').setup(_setupNextTick, - _setupPromises); + const { + nextTick, + runNextTicks + } = NativeModule.require('internal/process/next_tick').setup(); + + process.nextTick = nextTick; + // Used to emulate a tick manually in the JS land. + // A better name for this function would be `runNextTicks` but + // it has been exposed to the process object so we keep this legacy name + // TODO(joyeecheung): either remove it or make it public + process._tickCallback = runNextTicks; + const credentials = internalBinding('credentials'); if (credentials.implementsPosixCredentials) { process.getuid = credentials.getuid; diff --git a/lib/internal/process/next_tick.js b/lib/internal/process/next_tick.js index cb0274367ad4c5..00bba254718684 100644 --- a/lib/internal/process/next_tick.js +++ b/lib/internal/process/next_tick.js @@ -1,128 +1,138 @@ 'use strict'; -exports.setup = setupNextTick; - -function setupNextTick(_setupNextTick, _setupPromises) { - const { - getDefaultTriggerAsyncId, - newAsyncId, - initHooksExist, - destroyHooksExist, - emitInit, - emitBefore, - emitAfter, - emitDestroy, - symbols: { async_id_symbol, trigger_async_id_symbol } - } = require('internal/async_hooks'); - const emitPromiseRejectionWarnings = - require('internal/process/promises').setup(_setupPromises); - const { ERR_INVALID_CALLBACK } = require('internal/errors').codes; - const FixedQueue = require('internal/fixed_queue'); - - // tickInfo is used so that the C++ code in src/node.cc can - // have easy access to our nextTick state, and avoid unnecessary - // calls into JS land. - // runMicrotasks is used to run V8's micro task queue. - const [ - tickInfo, - runMicrotasks - ] = _setupNextTick(internalTickCallback); - - // *Must* match Environment::TickInfo::Fields in src/env.h. - const kHasScheduled = 0; - const kHasPromiseRejections = 1; - - const queue = new FixedQueue(); - - process.nextTick = nextTick; - // Needs to be accessible from beyond this scope. - process._tickCallback = _tickCallback; - - function _tickCallback() { - if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0) - runMicrotasks(); - if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0) - return; - - internalTickCallback(); - } - - function internalTickCallback() { - let tock; - do { - while (tock = queue.shift()) { - const asyncId = tock[async_id_symbol]; - emitBefore(asyncId, tock[trigger_async_id_symbol]); - // emitDestroy() places the async_id_symbol into an asynchronous queue - // that calls the destroy callback in the future. It's called before - // calling tock.callback so destroy will be called even if the callback - // throws an exception that is handled by 'uncaughtException' or a - // domain. - // TODO(trevnorris): This is a bit of a hack. It relies on the fact - // that nextTick() doesn't allow the event loop to proceed, but if - // any async hooks are enabled during the callback's execution then - // this tock's after hook will be called, but not its destroy hook. - if (destroyHooksExist()) - emitDestroy(asyncId); - - const callback = tock.callback; - if (tock.args === undefined) - callback(); - else - Reflect.apply(callback, undefined, tock.args); - - emitAfter(asyncId); - } - tickInfo[kHasScheduled] = 0; - runMicrotasks(); - } while (!queue.isEmpty() || emitPromiseRejectionWarnings()); - tickInfo[kHasPromiseRejections] = 0; - } +const { + // For easy access to the nextTick state in the C++ land, + // and to avoid unnecessary calls into JS land. + tickInfo, + // Used to run V8's micro task queue. + runMicrotasks, + setTickCallback, + initializePromiseRejectCallback +} = internalBinding('task_queue'); + +const { + promiseRejectHandler, + emitPromiseRejectionWarnings +} = require('internal/process/promises'); + +const { + getDefaultTriggerAsyncId, + newAsyncId, + initHooksExist, + destroyHooksExist, + emitInit, + emitBefore, + emitAfter, + emitDestroy, + symbols: { async_id_symbol, trigger_async_id_symbol } +} = require('internal/async_hooks'); +const { ERR_INVALID_CALLBACK } = require('internal/errors').codes; +const FixedQueue = require('internal/fixed_queue'); + +// *Must* match Environment::TickInfo::Fields in src/env.h. +const kHasScheduled = 0; +const kHasPromiseRejections = 1; + +const queue = new FixedQueue(); + +function runNextTicks() { + if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0) + runMicrotasks(); + if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0) + return; + + internalTickCallback(); +} - class TickObject { - constructor(callback, args, triggerAsyncId) { - // This must be set to null first to avoid function tracking - // on the hidden class, revisit in V8 versions after 6.2 - this.callback = null; - this.callback = callback; - this.args = args; - - const asyncId = newAsyncId(); - this[async_id_symbol] = asyncId; - this[trigger_async_id_symbol] = triggerAsyncId; - - if (initHooksExist()) { - emitInit(asyncId, - 'TickObject', - triggerAsyncId, - this); - } +function internalTickCallback() { + let tock; + do { + while (tock = queue.shift()) { + const asyncId = tock[async_id_symbol]; + emitBefore(asyncId, tock[trigger_async_id_symbol]); + // emitDestroy() places the async_id_symbol into an asynchronous queue + // that calls the destroy callback in the future. It's called before + // calling tock.callback so destroy will be called even if the callback + // throws an exception that is handled by 'uncaughtException' or a + // domain. + // TODO(trevnorris): This is a bit of a hack. It relies on the fact + // that nextTick() doesn't allow the event loop to proceed, but if + // any async hooks are enabled during the callback's execution then + // this tock's after hook will be called, but not its destroy hook. + if (destroyHooksExist()) + emitDestroy(asyncId); + + const callback = tock.callback; + if (tock.args === undefined) + callback(); + else + Reflect.apply(callback, undefined, tock.args); + + emitAfter(asyncId); } - } + tickInfo[kHasScheduled] = 0; + runMicrotasks(); + } while (!queue.isEmpty() || emitPromiseRejectionWarnings()); + tickInfo[kHasPromiseRejections] = 0; +} - // `nextTick()` will not enqueue any callback when the process is about to - // exit since the callback would not have a chance to be executed. - function nextTick(callback) { - if (typeof callback !== 'function') - throw new ERR_INVALID_CALLBACK(); - - if (process._exiting) - return; - - var args; - switch (arguments.length) { - case 1: break; - case 2: args = [arguments[1]]; break; - case 3: args = [arguments[1], arguments[2]]; break; - case 4: args = [arguments[1], arguments[2], arguments[3]]; break; - default: - args = new Array(arguments.length - 1); - for (var i = 1; i < arguments.length; i++) - args[i - 1] = arguments[i]; +class TickObject { + constructor(callback, args, triggerAsyncId) { + // This must be set to null first to avoid function tracking + // on the hidden class, revisit in V8 versions after 6.2 + this.callback = null; + this.callback = callback; + this.args = args; + + const asyncId = newAsyncId(); + this[async_id_symbol] = asyncId; + this[trigger_async_id_symbol] = triggerAsyncId; + + if (initHooksExist()) { + emitInit(asyncId, + 'TickObject', + triggerAsyncId, + this); } + } +} - if (queue.isEmpty()) - tickInfo[kHasScheduled] = 1; - queue.push(new TickObject(callback, args, getDefaultTriggerAsyncId())); +// `nextTick()` will not enqueue any callback when the process is about to +// exit since the callback would not have a chance to be executed. +function nextTick(callback) { + if (typeof callback !== 'function') + throw new ERR_INVALID_CALLBACK(); + + if (process._exiting) + return; + + var args; + switch (arguments.length) { + case 1: break; + case 2: args = [arguments[1]]; break; + case 3: args = [arguments[1], arguments[2]]; break; + case 4: args = [arguments[1], arguments[2], arguments[3]]; break; + default: + args = new Array(arguments.length - 1); + for (var i = 1; i < arguments.length; i++) + args[i - 1] = arguments[i]; } + + if (queue.isEmpty()) + tickInfo[kHasScheduled] = 1; + queue.push(new TickObject(callback, args, getDefaultTriggerAsyncId())); } + +// TODO(joyeecheung): make this a factory class so that node.js can +// control the side effects caused by the initializers. +exports.setup = function() { + // Initializes the per-isolate promise rejection callback which + // will call the handler being passed into this function. + initializePromiseRejectCallback(promiseRejectHandler); + // Sets the callback to be run in every tick. + setTickCallback(internalTickCallback); + return { + nextTick, + runNextTicks + }; +}; diff --git a/lib/internal/process/promises.js b/lib/internal/process/promises.js index 31c96293b341b9..df4e8188fe92b4 100644 --- a/lib/internal/process/promises.js +++ b/lib/internal/process/promises.js @@ -1,21 +1,16 @@ 'use strict'; const { safeToString } = internalBinding('util'); +const { + promiseRejectEvents +} = internalBinding('task_queue'); const maybeUnhandledPromises = new WeakMap(); const pendingUnhandledRejections = []; const asyncHandledRejections = []; -const promiseRejectEvents = {}; let lastPromiseId = 0; -exports.setup = setupPromises; - -function setupPromises(_setupPromises) { - _setupPromises(handler, promiseRejectEvents); - return emitPromiseRejectionWarnings; -} - -function handler(type, promise, reason) { +function promiseRejectHandler(type, promise, reason) { switch (type) { case promiseRejectEvents.kPromiseRejectWithNoHandler: return unhandledRejection(promise, reason); @@ -124,3 +119,8 @@ function emitPromiseRejectionWarnings() { } return maybeScheduledTicks || pendingUnhandledRejections.length !== 0; } + +module.exports = { + promiseRejectHandler, + emitPromiseRejectionWarnings +}; diff --git a/node.gyp b/node.gyp index b337de0a422be6..8560c1d512ca14 100644 --- a/node.gyp +++ b/node.gyp @@ -325,7 +325,6 @@ 'sources': [ 'src/async_wrap.cc', - 'src/bootstrapper.cc', 'src/callback_scope.cc', 'src/cares_wrap.cc', 'src/connect_wrap.cc', @@ -371,6 +370,8 @@ 'src/node_process.cc', 'src/node_serdes.cc', 'src/node_stat_watcher.cc', + 'src/node_symbols.cc', + 'src/node_task_queue.cc', 'src/node_trace_events.cc', 'src/node_types.cc', 'src/node_url.cc', diff --git a/src/env.h b/src/env.h index 934c160197d7a4..c999b258c5cafc 100644 --- a/src/env.h +++ b/src/env.h @@ -358,7 +358,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(performance_entry_template, v8::Function) \ V(pipe_constructor_template, v8::FunctionTemplate) \ V(process_object, v8::Object) \ - V(promise_handler_function, v8::Function) \ + V(promise_reject_callback, v8::Function) \ V(promise_wrap_template, v8::ObjectTemplate) \ V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \ V(script_context_constructor_template, v8::FunctionTemplate) \ @@ -373,7 +373,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(tty_constructor_template, v8::FunctionTemplate) \ V(udp_constructor_function, v8::Function) \ V(url_constructor_function, v8::Function) \ - V(write_wrap_template, v8::ObjectTemplate) \ + V(write_wrap_template, v8::ObjectTemplate) class Environment; diff --git a/src/node.cc b/src/node.cc index 9bd2ac9ff6f920..8f554363c0fb1a 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1200,20 +1200,14 @@ void LoadEnvironment(Environment* env) { return; } - // Bootstrap Node.js - Local bootstrapper = Object::New(env->isolate()); - SetupBootstrapObject(env, bootstrapper); - // process, bootstrappers, loaderExports, triggerFatalException std::vector> node_params = { env->process_string(), - FIXED_ONE_BYTE_STRING(isolate, "bootstrappers"), FIXED_ONE_BYTE_STRING(isolate, "loaderExports"), FIXED_ONE_BYTE_STRING(isolate, "triggerFatalException"), FIXED_ONE_BYTE_STRING(isolate, "isMainThread")}; std::vector> node_args = { process, - bootstrapper, loader_exports.ToLocalChecked(), env->NewFunctionTemplate(FatalException) ->GetFunction(context) diff --git a/src/node_binding.cc b/src/node_binding.cc index 46ae5f6840f658..cf47b3058de538 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -52,6 +52,7 @@ V(stream_wrap) \ V(string_decoder) \ V(symbols) \ + V(task_queue) \ V(tcp_wrap) \ V(timers) \ V(trace_events) \ diff --git a/src/node_internals.h b/src/node_internals.h index 99498a6218d35a..897905b5a76a69 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -185,8 +185,6 @@ v8::Maybe ProcessEmitDeprecationWarning(Environment* env, const char* warning, const char* deprecation_code); -void SetupBootstrapObject(Environment* env, - v8::Local bootstrapper); void SetupProcessObject(Environment* env, const std::vector& args, const std::vector& exec_args); diff --git a/src/node_symbols.cc b/src/node_symbols.cc new file mode 100644 index 00000000000000..36289b9f4148ae --- /dev/null +++ b/src/node_symbols.cc @@ -0,0 +1,29 @@ +#include "env-inl.h" +#include "node_binding.h" + +namespace node { + +using v8::Context; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace symbols { + +static void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); +#define V(PropertyName, StringValue) \ + target \ + ->Set(env->context(), env->PropertyName()->Name(), env->PropertyName()) \ + .FromJust(); + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V +} + +} // namespace symbols +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(symbols, node::symbols::Initialize) diff --git a/src/bootstrapper.cc b/src/node_task_queue.cc similarity index 54% rename from src/bootstrapper.cc rename to src/node_task_queue.cc index b46ba5badab226..f65f420081d6e8 100644 --- a/src/bootstrapper.cc +++ b/src/node_task_queue.cc @@ -1,5 +1,5 @@ -#include "node.h" #include "env-inl.h" +#include "node.h" #include "node_internals.h" #include "v8.h" @@ -23,36 +23,21 @@ using v8::Object; using v8::Promise; using v8::PromiseRejectEvent; using v8::PromiseRejectMessage; -using v8::String; using v8::Value; -void RunMicrotasks(const FunctionCallbackInfo& args) { +namespace task_queue { + +static void RunMicrotasks(const FunctionCallbackInfo& args) { args.GetIsolate()->RunMicrotasks(); } -void SetupNextTick(const FunctionCallbackInfo& args) { +static void SetTickCallback(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); - Local context = env->context(); - CHECK(args[0]->IsFunction()); - env->set_tick_callback_function(args[0].As()); - - Local run_microtasks_fn = - env->NewFunctionTemplate(RunMicrotasks)->GetFunction(context) - .ToLocalChecked(); - run_microtasks_fn->SetName(FIXED_ONE_BYTE_STRING(isolate, "runMicrotasks")); - - Local ret[] = { - env->tick_info()->fields().GetJSArray(), - run_microtasks_fn - }; - - args.GetReturnValue().Set(Array::New(isolate, ret, arraysize(ret))); } -void PromiseRejectCallback(PromiseRejectMessage message) { +static void PromiseRejectCallback(PromiseRejectMessage message) { static std::atomic unhandledRejections{0}; static std::atomic rejectionsHandledAfter{0}; @@ -64,7 +49,7 @@ void PromiseRejectCallback(PromiseRejectMessage message) { if (env == nullptr) return; - Local callback = env->promise_handler_function(); + Local callback = env->promise_reject_callback(); Local value; Local type = Number::New(env->isolate(), event); @@ -104,53 +89,48 @@ void PromiseRejectCallback(PromiseRejectMessage message) { env->tick_info()->promise_rejections_toggle_on(); } -void SetupPromises(const FunctionCallbackInfo& args) { +static void InitializePromiseRejectCallback( + const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); CHECK(args[0]->IsFunction()); - CHECK(args[1]->IsObject()); - - Local constants = args[1].As(); - - NODE_DEFINE_CONSTANT(constants, kPromiseRejectWithNoHandler); - NODE_DEFINE_CONSTANT(constants, kPromiseHandlerAddedAfterReject); - NODE_DEFINE_CONSTANT(constants, kPromiseResolveAfterResolved); - NODE_DEFINE_CONSTANT(constants, kPromiseRejectAfterResolved); + // TODO(joyeecheung): this may be moved to somewhere earlier in the bootstrap + // to make sure it's only called once isolate->SetPromiseRejectCallback(PromiseRejectCallback); - env->set_promise_handler_function(args[0].As()); -} -#define BOOTSTRAP_METHOD(name, fn) env->SetMethod(bootstrapper, #name, fn) - -// The Bootstrapper object is an ephemeral object that is used only during -// the bootstrap process of the Node.js environment. A reference to the -// bootstrap object must not be kept around after the bootstrap process -// completes so that it can be gc'd as soon as possible. -void SetupBootstrapObject(Environment* env, - Local bootstrapper) { - BOOTSTRAP_METHOD(_setupNextTick, SetupNextTick); - BOOTSTRAP_METHOD(_setupPromises, SetupPromises); + env->set_promise_reject_callback(args[0].As()); } -#undef BOOTSTRAP_METHOD - -namespace symbols { -void Initialize(Local target, - Local unused, - Local context, - void* priv) { +static void Initialize(Local target, + Local unused, + Local context, + void* priv) { Environment* env = Environment::GetCurrent(context); -#define V(PropertyName, StringValue) \ - target->Set(env->context(), \ - env->PropertyName()->Name(), \ - env->PropertyName()).FromJust(); - PER_ISOLATE_SYMBOL_PROPERTIES(V) -#undef V + Isolate* isolate = env->isolate(); + + env->SetMethod(target, "setTickCallback", SetTickCallback); + env->SetMethod(target, "runMicrotasks", RunMicrotasks); + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(isolate, "tickInfo"), + env->tick_info()->fields().GetJSArray()).FromJust(); + + Local events = Object::New(isolate); + NODE_DEFINE_CONSTANT(events, kPromiseRejectWithNoHandler); + NODE_DEFINE_CONSTANT(events, kPromiseHandlerAddedAfterReject); + NODE_DEFINE_CONSTANT(events, kPromiseResolveAfterResolved); + NODE_DEFINE_CONSTANT(events, kPromiseRejectAfterResolved); + + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(isolate, "promiseRejectEvents"), + events).FromJust(); + env->SetMethod(target, + "initializePromiseRejectCallback", + InitializePromiseRejectCallback); } -} // namespace symbols +} // namespace task_queue } // namespace node -NODE_MODULE_CONTEXT_AWARE_INTERNAL(symbols, node::symbols::Initialize) +NODE_MODULE_CONTEXT_AWARE_INTERNAL(task_queue, node::task_queue::Initialize) diff --git a/test/message/events_unhandled_error_nexttick.out b/test/message/events_unhandled_error_nexttick.out index 1c0ed6df9341fe..aa52367ba09499 100644 --- a/test/message/events_unhandled_error_nexttick.out +++ b/test/message/events_unhandled_error_nexttick.out @@ -16,7 +16,7 @@ Error Emitted 'error' event at: at process.nextTick (*events_unhandled_error_nexttick.js:*:*) at internalTickCallback (internal/process/next_tick.js:*:*) - at process._tickCallback (internal/process/next_tick.js:*:*) + at process.runNextTicks [as _tickCallback] (internal/process/next_tick.js:*:*) at Function.Module.runMain (internal/modules/cjs/loader.js:*:*) at executeUserCode (internal/bootstrap/node.js:*:*) at startExecution (internal/bootstrap/node.js:*:*) diff --git a/test/message/nexttick_throw.out b/test/message/nexttick_throw.out index 2bf69e8146130a..1fee9075fe5078 100644 --- a/test/message/nexttick_throw.out +++ b/test/message/nexttick_throw.out @@ -5,7 +5,7 @@ ReferenceError: undefined_reference_error_maker is not defined at *test*message*nexttick_throw.js:*:* at internalTickCallback (internal/process/next_tick.js:*:*) - at process._tickCallback (internal/process/next_tick.js:*:*) + at process.runNextTicks [as _tickCallback] (internal/process/next_tick.js:*:*) at Function.Module.runMain (internal/modules/cjs/loader.js:*:*) at executeUserCode (internal/bootstrap/node.js:*:*) at startExecution (internal/bootstrap/node.js:*:*) diff --git a/test/message/unhandled_promise_trace_warnings.out b/test/message/unhandled_promise_trace_warnings.out index 2187ee1e85b8f8..0b2ef999414a3f 100644 --- a/test/message/unhandled_promise_trace_warnings.out +++ b/test/message/unhandled_promise_trace_warnings.out @@ -42,7 +42,7 @@ at * (node:*) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1) at handledRejection (internal/process/promises.js:*) - at handler (internal/process/promises.js:*) + at promiseRejectHandler (internal/process/promises.js:*) at Promise.then * at Promise.catch * at Immediate.setImmediate (*test*message*unhandled_promise_trace_warnings.js:*) diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 3f67144f5fb2ce..ee4864e09ba1d5 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -9,7 +9,7 @@ const common = require('../common'); const assert = require('assert'); const isMainThread = common.isMainThread; -const kMaxModuleCount = isMainThread ? 61 : 83; +const kMaxModuleCount = isMainThread ? 62 : 84; assert(list.length <= kMaxModuleCount, `Total length: ${list.length}\n` + list.join('\n')