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

fix: let Node.js perform microtask checkpoint in the main process #24131

Merged
merged 7 commits into from Jun 17, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions filenames.gni
Expand Up @@ -525,6 +525,8 @@ filenames = {
"shell/common/gin_helper/function_template_extensions.h",
"shell/common/gin_helper/locker.cc",
"shell/common/gin_helper/locker.h",
"shell/common/gin_helper/microtasks_scope.cc",
"shell/common/gin_helper/microtasks_scope.h",
"shell/common/gin_helper/object_template_builder.cc",
"shell/common/gin_helper/object_template_builder.h",
"shell/common/gin_helper/persistent_dictionary.cc",
Expand Down
2 changes: 0 additions & 2 deletions shell/browser/api/electron_api_url_loader.cc
Expand Up @@ -135,8 +135,6 @@ class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
data_producer_ = std::make_unique<mojo::DataPipeProducer>(std::move(pipe));

v8::HandleScope handle_scope(isolate_);
v8::MicrotasksScope script_scope(isolate_,
v8::MicrotasksScope::kRunMicrotasks);
auto maybe_wrapper = GetWrapper(isolate_);
v8::Local<v8::Value> wrapper;
if (!maybe_wrapper.ToLocal(&wrapper)) {
Expand Down
1 change: 1 addition & 0 deletions shell/browser/electron_browser_main_parts.h
Expand Up @@ -90,6 +90,7 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {

Browser* browser() { return browser_.get(); }
BrowserProcessImpl* browser_process() { return fake_browser_process_.get(); }
NodeEnvironment* node_env() { return node_env_.get(); }

protected:
// content::BrowserMainParts:
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/javascript_environment.h
Expand Up @@ -57,6 +57,8 @@ class NodeEnvironment {
explicit NodeEnvironment(node::Environment* env);
~NodeEnvironment();

node::Environment* env() { return env_; }

private:
node::Environment* env_;

Expand Down
20 changes: 19 additions & 1 deletion shell/browser/microtasks_runner.cc
Expand Up @@ -4,6 +4,10 @@
// found in the LICENSE file.

#include "shell/browser/microtasks_runner.h"

#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/node_includes.h"
#include "v8/include/v8.h"

namespace electron {
Expand All @@ -15,7 +19,21 @@ void MicrotasksRunner::WillProcessTask(const base::PendingTask& pending_task,

void MicrotasksRunner::DidProcessTask(const base::PendingTask& pending_task) {
v8::Isolate::Scope scope(isolate_);
v8::MicrotasksScope::PerformCheckpoint(isolate_);
// In the browser process we follow Node.js microtask policy of kExplicit
// and let the MicrotaskRunner which is a task observer for chromium UI thread
// scheduler run the micotask checkpoint. This worked fine because Node.js
// also runs microtasks through its task queue, but after
// https://github.com/electron/electron/issues/20013 Node.js now performs its
// own microtask checkpoint and it may happen is some situations that there is
// contention for performing checkpoint between Node.js and chromium, ending
// up Node.js dealying its callbacks. To fix this, now we always lets Node.js
// handle the checkpoint in the browser process.
{
auto* node_env = electron::ElectronBrowserMainParts::Get()->node_env();
node::InternalCallbackScope microtasks_scope(
node_env->env(), v8::Local<v8::Object>(), {0, 0},
node::InternalCallbackScope::kAllowEmptyResource);
}
}

} // namespace electron
4 changes: 2 additions & 2 deletions shell/common/api/electron_bindings.cc
Expand Up @@ -25,6 +25,7 @@
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/locker.h"
#include "shell/common/gin_helper/microtasks_scope.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/heap_snapshot.h"
#include "shell/common/node_includes.h"
Expand Down Expand Up @@ -271,8 +272,7 @@ void ElectronBindings::DidReceiveMemoryDump(
v8::Isolate* isolate = promise.isolate();
gin_helper::Locker locker(isolate);
v8::HandleScope handle_scope(isolate);
v8::MicrotasksScope script_scope(isolate,
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate, true);
v8::Context::Scope context_scope(
v8::Local<v8::Context>::New(isolate, context));

Expand Down
11 changes: 4 additions & 7 deletions shell/common/gin_helper/callback.h
Expand Up @@ -13,7 +13,7 @@
#include "shell/common/gin_converters/std_converter.h"
#include "shell/common/gin_helper/function_template.h"
#include "shell/common/gin_helper/locker.h"

#include "shell/common/gin_helper/microtasks_scope.h"
// Implements safe convertions between JS functions and base::Callback.

namespace gin_helper {
Expand Down Expand Up @@ -48,8 +48,7 @@ struct V8FunctionInvoker<v8::Local<v8::Value>(ArgTypes...)> {
v8::EscapableHandleScope handle_scope(isolate);
if (!function.IsAlive())
return v8::Null(isolate);
v8::MicrotasksScope script_scope(isolate,
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate, true);
v8::Local<v8::Function> holder = function.NewHandle(isolate);
v8::Local<v8::Context> context = holder->CreationContext();
v8::Context::Scope context_scope(context);
Expand All @@ -73,8 +72,7 @@ struct V8FunctionInvoker<void(ArgTypes...)> {
v8::HandleScope handle_scope(isolate);
if (!function.IsAlive())
return;
v8::MicrotasksScope script_scope(isolate,
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate, true);
v8::Local<v8::Function> holder = function.NewHandle(isolate);
v8::Local<v8::Context> context = holder->CreationContext();
v8::Context::Scope context_scope(context);
Expand All @@ -97,8 +95,7 @@ struct V8FunctionInvoker<ReturnType(ArgTypes...)> {
ReturnType ret = ReturnType();
if (!function.IsAlive())
return ret;
v8::MicrotasksScope script_scope(isolate,
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate, true);
v8::Local<v8::Function> holder = function.NewHandle(isolate);
v8::Local<v8::Context> context = holder->CreationContext();
v8::Context::Scope context_scope(context);
Expand Down
4 changes: 2 additions & 2 deletions shell/common/gin_helper/event_emitter_caller.cc
Expand Up @@ -5,6 +5,7 @@
#include "shell/common/gin_helper/event_emitter_caller.h"

#include "shell/common/gin_helper/locker.h"
#include "shell/common/gin_helper/microtasks_scope.h"
#include "shell/common/node_includes.h"

namespace gin_helper {
Expand All @@ -16,8 +17,7 @@ v8::Local<v8::Value> CallMethodWithArgs(v8::Isolate* isolate,
const char* method,
ValueVector* args) {
// Perform microtask checkpoint after running JavaScript.
v8::MicrotasksScope script_scope(isolate,
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate, true);
// Use node::MakeCallback to call the callback, and it will also run pending
// tasks in Node.js.
v8::MaybeLocal<v8::Value> ret = node::MakeCallback(
Expand Down
7 changes: 3 additions & 4 deletions shell/common/gin_helper/function_template.h
Expand Up @@ -14,6 +14,7 @@
#include "shell/common/gin_helper/arguments.h"
#include "shell/common/gin_helper/destroyable.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/microtasks_scope.h"

// This file is forked from gin/function_template.h with 2 differences:
// 1. Support for additional types of arguments.
Expand Down Expand Up @@ -214,8 +215,7 @@ class Invoker<IndicesHolder<indices...>, ArgTypes...>

template <typename ReturnType>
void DispatchToCallback(base::Callback<ReturnType(ArgTypes...)> callback) {
v8::MicrotasksScope script_scope(args_->isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(args_->isolate(), true);
args_->Return(
callback.Run(std::move(ArgumentHolder<indices, ArgTypes>::value)...));
}
Expand All @@ -224,8 +224,7 @@ class Invoker<IndicesHolder<indices...>, ArgTypes...>
// expression to foo. As a result, we must specialize the case of Callbacks
// that have the void return type.
void DispatchToCallback(base::Callback<void(ArgTypes...)> callback) {
v8::MicrotasksScope script_scope(args_->isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(args_->isolate(), true);
callback.Run(std::move(ArgumentHolder<indices, ArgTypes>::value)...);
}

Expand Down
24 changes: 24 additions & 0 deletions shell/common/gin_helper/microtasks_scope.cc
@@ -0,0 +1,24 @@
// Copyright (c) 2020 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/common/gin_helper/microtasks_scope.h"

#include "shell/common/gin_helper/locker.h"

namespace gin_helper {

MicrotasksScope::MicrotasksScope(v8::Isolate* isolate,
bool ignore_browser_checkpoint) {
if (Locker::IsBrowserProcess()) {
if (!ignore_browser_checkpoint)
v8::MicrotasksScope::PerformCheckpoint(isolate);
} else {
v8_microtasks_scope_ = std::make_unique<v8::MicrotasksScope>(
isolate, v8::MicrotasksScope::kRunMicrotasks);
}
}

MicrotasksScope::~MicrotasksScope() = default;

} // namespace gin_helper
31 changes: 31 additions & 0 deletions shell/common/gin_helper/microtasks_scope.h
@@ -0,0 +1,31 @@
// Copyright (c) 2020 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#ifndef SHELL_COMMON_GIN_HELPER_MICROTASKS_SCOPE_H_
#define SHELL_COMMON_GIN_HELPER_MICROTASKS_SCOPE_H_

#include <memory>

#include "base/macros.h"
#include "v8/include/v8.h"

namespace gin_helper {

// In the browser process runs v8::MicrotasksScope::PerformCheckpoint
// In the render process creates a v8::MicrotasksScope.
class MicrotasksScope {
public:
explicit MicrotasksScope(v8::Isolate* isolate,
bool ignore_browser_checkpoint = false);
~MicrotasksScope();

private:
std::unique_ptr<v8::MicrotasksScope> v8_microtasks_scope_;

DISALLOW_COPY_AND_ASSIGN(MicrotasksScope);
};

} // namespace gin_helper

#endif // SHELL_COMMON_GIN_HELPER_MICROTASKS_SCOPE_H_
12 changes: 4 additions & 8 deletions shell/common/gin_helper/promise.cc
Expand Up @@ -26,8 +26,7 @@ PromiseBase& PromiseBase::operator=(PromiseBase&&) = default;
v8::Maybe<bool> PromiseBase::Reject() {
gin_helper::Locker locker(isolate());
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate());
v8::Context::Scope context_scope(GetContext());

return GetInner()->Reject(GetContext(), v8::Undefined(isolate()));
Expand All @@ -36,8 +35,7 @@ v8::Maybe<bool> PromiseBase::Reject() {
v8::Maybe<bool> PromiseBase::Reject(v8::Local<v8::Value> except) {
gin_helper::Locker locker(isolate());
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate());
v8::Context::Scope context_scope(GetContext());

return GetInner()->Reject(GetContext(), except);
Expand All @@ -46,8 +44,7 @@ v8::Maybe<bool> PromiseBase::Reject(v8::Local<v8::Value> except) {
v8::Maybe<bool> PromiseBase::RejectWithErrorMessage(base::StringPiece message) {
gin_helper::Locker locker(isolate());
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate());
v8::Context::Scope context_scope(GetContext());

v8::Local<v8::Value> error =
Expand Down Expand Up @@ -90,8 +87,7 @@ v8::Local<v8::Promise> Promise<void>::ResolvedPromise(v8::Isolate* isolate) {
v8::Maybe<bool> Promise<void>::Resolve() {
gin_helper::Locker locker(isolate());
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate());
v8::Context::Scope context_scope(GetContext());

return GetInner()->Resolve(GetContext(), v8::Undefined(isolate()));
Expand Down
4 changes: 2 additions & 2 deletions shell/common/gin_helper/promise.h
Expand Up @@ -16,6 +16,7 @@
#include "content/public/browser/browser_thread.h"
#include "shell/common/gin_converters/std_converter.h"
#include "shell/common/gin_helper/locker.h"
#include "shell/common/gin_helper/microtasks_scope.h"

namespace gin_helper {

Expand Down Expand Up @@ -110,8 +111,7 @@ class Promise : public PromiseBase {
v8::Maybe<bool> Resolve(const RT& value) {
gin_helper::Locker locker(isolate());
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(isolate());
v8::Context::Scope context_scope(GetContext());

return GetInner()->Resolve(GetContext(),
Expand Down
4 changes: 2 additions & 2 deletions shell/common/node_bindings.cc
Expand Up @@ -29,6 +29,7 @@
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "shell/common/gin_helper/locker.h"
#include "shell/common/gin_helper/microtasks_scope.h"
#include "shell/common/mac/main_application_bundle.h"
#include "shell/common/node_includes.h"

Expand Down Expand Up @@ -475,8 +476,7 @@ void NodeBindings::UvRunOnce() {
v8::Context::Scope context_scope(env->context());

// Perform microtask checkpoint after running JavaScript.
v8::MicrotasksScope script_scope(env->isolate(),
v8::MicrotasksScope::kRunMicrotasks);
gin_helper::MicrotasksScope microtasks_scope(env->isolate());

if (browser_env_ != BrowserEnvironment::BROWSER)
TRACE_EVENT_BEGIN0("devtools.timeline", "FunctionCall");
Expand Down
2 changes: 2 additions & 0 deletions spec-main/api-session-spec.ts
Expand Up @@ -288,6 +288,8 @@ describe('session module', () => {
const { item, itemUrl, itemFilename } = await downloadPrevented;
expect(itemUrl).to.equal(url);
expect(itemFilename).to.equal('mockFile.txt');
// Delay till the next tick.
await new Promise(resolve => setImmediate(() => resolve()));
expect(() => item.getURL()).to.throw('DownloadItem used after being destroyed');
});
});
Expand Down
36 changes: 29 additions & 7 deletions spec-main/api-web-contents-spec.ts
Expand Up @@ -597,28 +597,50 @@ describe('webContents module', () => {
// On Mac, zooming isn't done with the mouse wheel.
ifdescribe(process.platform !== 'darwin')('zoom-changed', () => {
afterEach(closeAllWindows);
it('is emitted with the correct zooming info', async () => {
it('is emitted with the correct zoom-in info', async () => {
const w = new BrowserWindow({ show: false });
await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'));

const testZoomChanged = async ({ zoomingIn }: { zoomingIn: boolean }) => {
const testZoomChanged = async () => {
w.webContents.sendInputEvent({
type: 'mouseWheel',
x: 300,
y: 300,
deltaX: 0,
deltaY: zoomingIn ? 1 : -1,
deltaY: 1,
wheelTicksX: 0,
wheelTicksY: zoomingIn ? 1 : -1,
wheelTicksY: 1,
modifiers: ['control', 'meta']
});

const [, zoomDirection] = await emittedOnce(w.webContents, 'zoom-changed');
expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out');
expect(zoomDirection).to.equal('in');
};

await testZoomChanged({ zoomingIn: true });
await testZoomChanged({ zoomingIn: false });
await testZoomChanged();
});

it('is emitted with the correct zoom-out info', async () => {
const w = new BrowserWindow({ show: false });
await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'));

const testZoomChanged = async () => {
w.webContents.sendInputEvent({
type: 'mouseWheel',
x: 300,
y: 300,
deltaX: 0,
deltaY: -1,
wheelTicksX: 0,
wheelTicksY: -1,
modifiers: ['control', 'meta']
});

const [, zoomDirection] = await emittedOnce(w.webContents, 'zoom-changed');
expect(zoomDirection).to.equal('out');
};

await testZoomChanged();
});
});

Expand Down
14 changes: 14 additions & 0 deletions spec-main/node-spec.ts
Expand Up @@ -323,4 +323,18 @@ describe('node feature', () => {
done();
});
});

it('performs microtask checkpoint correctly', (done) => {
const f3 = async () => {
return new Promise((resolve, reject) => {
reject(new Error('oops'));
});
};

process.once('unhandledRejection', () => done('catch block is delayed to next tick'));

setTimeout(() => {
f3().catch(() => done());
});
});
});