Reland "[inspector] added Runtime.installBinding method"

This is a reland of 49c4ac7753

Original change's description:
> [inspector] added Runtime.installBinding method
>
> A lot of different clients use console.debug as a message channel from
> page to protocol client. console.debug is a little slow and not
> designed for this use case.
>
> This CL introduces new method: Runtime.installBinding. This method
> installs binding function by given name on global object on each
> inspected context including any context created later.
> Binding function takes exactly one string argument. Each time when
> binding function is called, Runtime.bindingCalled notification is
> triggered and includes passed payload.
>
> Binding function survives page reload and reinstalled right after
> console object is setup. So installed binding can be used inside
> script added by Page.addScriptToEvaluateOnNewDocument so client may do
> something like:
> Runtime.installBinding({name: 'send'});
> Page.addScriptToEvaluateOnNewDocument({source: 'console.debug = send'});
> .. navigate page ..
>
> In microbenchmark this function is ~4.6 times faster then
> console.debug.
>
> R=lushnikov@chromium.org,pfeldman@chromium.org
>
> Bug: none
> Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel
> Change-Id: I3e0e231dde9d45116709d248f6e9e7ec7037e8e3
> Reviewed-on: https://chromium-review.googlesource.com/1077662
> Commit-Queue: Aleksey Kozyatinskiy <kozyatinskiy@chromium.org>
> Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#53462}

TBR=dgozman@chromium.org

Bug: none
Change-Id: I58d053581a86f15338dea621498058b7b75c7c85
Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel
Reviewed-on: https://chromium-review.googlesource.com/1081833
Reviewed-by: Aleksey Kozyatinskiy <kozyatinskiy@chromium.org>
Commit-Queue: Aleksey Kozyatinskiy <kozyatinskiy@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53479}
This commit is contained in:
Alexey Kozyatinskiy 2018-05-31 13:42:07 -07:00 committed by Commit Bot
parent 785bd43b7e
commit 5cb11a17cd
7 changed files with 342 additions and 0 deletions

View File

@ -2884,9 +2884,51 @@
"name": "terminateExecution", "name": "terminateExecution",
"description": "Terminate current or next JavaScript execution.\nWill cancel the termination when the outer-most script execution ends.", "description": "Terminate current or next JavaScript execution.\nWill cancel the termination when the outer-most script execution ends.",
"experimental": true "experimental": true
},
{
"name": "addBinding",
"description": "Adds binding with the given name on the global objects of all inspected\ncontexts, including those created later. Bindings survive reloads.\nBinding function takes exactly one argument, this argument should be string,\nin case of any other input, function throws an exception.\nEach binding function call produces Runtime.bindingCalled notification.",
"experimental": true,
"parameters": [
{
"name": "name",
"type": "string"
}
]
},
{
"name": "removeBinding",
"description": "This method does not remove binding function from global object but\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.",
"experimental": true,
"parameters": [
{
"name": "name",
"type": "string"
}
]
} }
], ],
"events": [ "events": [
{
"name": "bindingCalled",
"description": "Notification is issued every time when binding is called.",
"experimental": true,
"parameters": [
{
"name": "name",
"type": "string"
},
{
"name": "payload",
"type": "string"
},
{
"name": "executionContextId",
"description": "Identifier of the context where the call was made.",
"$ref": "ExecutionContextId"
}
]
},
{ {
"name": "consoleAPICalled", "name": "consoleAPICalled",
"description": "Issued when console API was called.", "description": "Issued when console API was called.",

View File

@ -1330,6 +1330,29 @@ domain Runtime
# Will cancel the termination when the outer-most script execution ends. # Will cancel the termination when the outer-most script execution ends.
experimental command terminateExecution experimental command terminateExecution
# Adds binding with the given name on the global objects of all inspected
# contexts, including those created later. Bindings survive reloads.
# Binding function takes exactly one argument, this argument should be string,
# in case of any other input, function throws an exception.
# Each binding function call produces Runtime.bindingCalled notification.
experimental command addBinding
parameters
string name
# This method does not remove binding function from global object but
# unsubscribes current runtime agent from Runtime.bindingCalled notifications.
experimental command removeBinding
parameters
string name
# Notification is issued every time when binding is called.
experimental event bindingCalled
parameters
string name
string payload
# Identifier of the context where the call was made.
ExecutionContextId executionContextId
# Issued when console API was called. # Issued when console API was called.
event consoleAPICalled event consoleAPICalled
parameters parameters

View File

@ -206,6 +206,7 @@ void V8InspectorImpl::contextCreated(const V8ContextInfo& info) {
(*contextById)[contextId].reset(context); (*contextById)[contextId].reset(context);
forEachSession( forEachSession(
info.contextGroupId, [&context](V8InspectorSessionImpl* session) { info.contextGroupId, [&context](V8InspectorSessionImpl* session) {
session->runtimeAgent()->addBindings(context);
session->runtimeAgent()->reportExecutionContextCreated(context); session->runtimeAgent()->reportExecutionContextCreated(context);
}); });
} }

View File

@ -54,6 +54,7 @@ namespace V8RuntimeAgentImplState {
static const char customObjectFormatterEnabled[] = static const char customObjectFormatterEnabled[] =
"customObjectFormatterEnabled"; "customObjectFormatterEnabled";
static const char runtimeEnabled[] = "runtimeEnabled"; static const char runtimeEnabled[] = "runtimeEnabled";
static const char bindings[] = "bindings";
}; };
using protocol::Runtime::RemoteObject; using protocol::Runtime::RemoteObject;
@ -639,6 +640,86 @@ void V8RuntimeAgentImpl::terminateExecution(
m_inspector->debugger()->terminateExecution(std::move(callback)); m_inspector->debugger()->terminateExecution(std::move(callback));
} }
Response V8RuntimeAgentImpl::addBinding(const String16& name) {
if (!m_state->getObject(V8RuntimeAgentImplState::bindings)) {
m_state->setObject(V8RuntimeAgentImplState::bindings,
protocol::DictionaryValue::create());
}
protocol::DictionaryValue* bindings =
m_state->getObject(V8RuntimeAgentImplState::bindings);
bindings->setBoolean(name, true);
m_inspector->forEachContext(
m_session->contextGroupId(),
[&name, this](InspectedContext* context) { addBinding(context, name); });
return Response::OK();
}
void V8RuntimeAgentImpl::bindingCallback(
const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Isolate* isolate = info.GetIsolate();
if (info.Length() != 1 || !info[0]->IsString()) {
info.GetIsolate()->ThrowException(toV8String(
isolate, "Invalid arguments: should be exactly one string."));
return;
}
V8InspectorImpl* inspector =
static_cast<V8InspectorImpl*>(v8::debug::GetInspector(isolate));
int contextId = InspectedContext::contextId(isolate->GetCurrentContext());
int contextGroupId = inspector->contextGroupId(contextId);
String16 name = toProtocolString(v8::Local<v8::String>::Cast(info.Data()));
String16 payload = toProtocolString(v8::Local<v8::String>::Cast(info[0]));
inspector->forEachSession(
contextGroupId,
[&name, &payload, &contextId](V8InspectorSessionImpl* session) {
session->runtimeAgent()->bindingCalled(name, payload, contextId);
});
}
void V8RuntimeAgentImpl::addBinding(InspectedContext* context,
const String16& name) {
v8::HandleScope handles(m_inspector->isolate());
v8::Local<v8::Context> localContext = context->context();
v8::Local<v8::Object> global = localContext->Global();
v8::Local<v8::String> v8Name = toV8String(m_inspector->isolate(), name);
v8::Local<v8::Value> functionValue;
v8::MicrotasksScope microtasks(m_inspector->isolate(),
v8::MicrotasksScope::kDoNotRunMicrotasks);
if (v8::Function::New(localContext, bindingCallback, v8Name)
.ToLocal(&functionValue)) {
v8::Maybe<bool> success = global->Set(localContext, v8Name, functionValue);
USE(success);
}
}
Response V8RuntimeAgentImpl::removeBinding(const String16& name) {
protocol::DictionaryValue* bindings =
m_state->getObject(V8RuntimeAgentImplState::bindings);
if (!bindings) return Response::OK();
bindings->remove(name);
return Response::OK();
}
void V8RuntimeAgentImpl::bindingCalled(const String16& name,
const String16& payload,
int executionContextId) {
protocol::DictionaryValue* bindings =
m_state->getObject(V8RuntimeAgentImplState::bindings);
if (!bindings || !bindings->booleanProperty(name, false)) return;
m_frontend.bindingCalled(name, payload, executionContextId);
}
void V8RuntimeAgentImpl::addBindings(InspectedContext* context) {
if (!m_enabled) return;
protocol::DictionaryValue* bindings =
m_state->getObject(V8RuntimeAgentImplState::bindings);
if (!bindings) return;
for (size_t i = 0; i < bindings->size(); ++i) {
addBinding(context, bindings->at(i).first);
}
}
void V8RuntimeAgentImpl::restore() { void V8RuntimeAgentImpl::restore() {
if (!m_state->booleanProperty(V8RuntimeAgentImplState::runtimeEnabled, false)) if (!m_state->booleanProperty(V8RuntimeAgentImplState::runtimeEnabled, false))
return; return;
@ -647,6 +728,10 @@ void V8RuntimeAgentImpl::restore() {
if (m_state->booleanProperty( if (m_state->booleanProperty(
V8RuntimeAgentImplState::customObjectFormatterEnabled, false)) V8RuntimeAgentImplState::customObjectFormatterEnabled, false))
m_session->setCustomObjectFormatterEnabled(true); m_session->setCustomObjectFormatterEnabled(true);
m_inspector->forEachContext(
m_session->contextGroupId(),
[this](InspectedContext* context) { addBindings(context); });
} }
Response V8RuntimeAgentImpl::enable() { Response V8RuntimeAgentImpl::enable() {
@ -669,6 +754,7 @@ Response V8RuntimeAgentImpl::disable() {
if (!m_enabled) return Response::OK(); if (!m_enabled) return Response::OK();
m_enabled = false; m_enabled = false;
m_state->setBoolean(V8RuntimeAgentImplState::runtimeEnabled, false); m_state->setBoolean(V8RuntimeAgentImplState::runtimeEnabled, false);
m_state->remove(V8RuntimeAgentImplState::bindings);
m_inspector->disableStackCapturingIfNeeded(); m_inspector->disableStackCapturingIfNeeded();
m_session->setCustomObjectFormatterEnabled(false); m_session->setCustomObjectFormatterEnabled(false);
reset(); reset();

View File

@ -110,6 +110,10 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
void terminateExecution( void terminateExecution(
std::unique_ptr<TerminateExecutionCallback> callback) override; std::unique_ptr<TerminateExecutionCallback> callback) override;
Response addBinding(const String16& name) override;
Response removeBinding(const String16& name) override;
void addBindings(InspectedContext* context);
void reset(); void reset();
void reportExecutionContextCreated(InspectedContext*); void reportExecutionContextCreated(InspectedContext*);
void reportExecutionContextDestroyed(InspectedContext*); void reportExecutionContextDestroyed(InspectedContext*);
@ -121,6 +125,11 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
private: private:
bool reportMessage(V8ConsoleMessage*, bool generatePreview); bool reportMessage(V8ConsoleMessage*, bool generatePreview);
static void bindingCallback(const v8::FunctionCallbackInfo<v8::Value>& args);
void bindingCalled(const String16& name, const String16& payload,
int executionContextId);
void addBinding(InspectedContext* context, const String16& name);
V8InspectorSessionImpl* m_session; V8InspectorSessionImpl* m_session;
protocol::DictionaryValue* m_state; protocol::DictionaryValue* m_state;
protocol::Runtime::Frontend m_frontend; protocol::Runtime::Frontend m_frontend;

View File

@ -0,0 +1,99 @@
Test for Runtime.addBinding.
Running test: testBasic
Add binding inside session1..
Call binding..
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
Add binding inside session2..
Call binding..
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
binding called in session2
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
Disable agent inside session1..
Call binding..
binding called in session2
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
Disable agent inside session2..
Call binding..
Enable agent inside session1..
Call binding..
Running test: testReconnect
Add binding inside session..
Reconnect..
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
Running test: testBindingOverrides
Add send function on global object..
Add binding inside session..
Call binding..
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
Running test: testRemoveBinding
Add binding inside session..
Call binding..
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : send
payload : payload
}
}
Remove binding inside session..
Call binding..

View File

@ -0,0 +1,82 @@
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
InspectorTest.log('Test for Runtime.addBinding.');
InspectorTest.runAsyncTestSuite([
async function testBasic() {
const {contextGroup, sessions: [session1, session2]} = setupSessions(2);
InspectorTest.log('\nAdd binding inside session1..');
session1.Protocol.Runtime.addBinding({name: 'send'});
InspectorTest.log('Call binding..');
await session1.Protocol.Runtime.evaluate({expression: `send('payload')`});
InspectorTest.log('\nAdd binding inside session2..');
session2.Protocol.Runtime.addBinding({name: 'send'});
InspectorTest.log('Call binding..');
await session2.Protocol.Runtime.evaluate({expression: `send('payload')`});
InspectorTest.log('\nDisable agent inside session1..');
session1.Protocol.Runtime.disable();
InspectorTest.log('Call binding..');
await session2.Protocol.Runtime.evaluate({expression: `send('payload')`});
InspectorTest.log('\nDisable agent inside session2..');
session2.Protocol.Runtime.disable();
InspectorTest.log('Call binding..');
await session2.Protocol.Runtime.evaluate({expression: `send('payload')`});
InspectorTest.log('\nEnable agent inside session1..');
session1.Protocol.Runtime.enable();
InspectorTest.log('Call binding..');
await session2.Protocol.Runtime.evaluate({expression: `send('payload')`});
},
async function testReconnect() {
const {contextGroup, sessions: [session]} = setupSessions(1);
InspectorTest.log('\nAdd binding inside session..');
await session.Protocol.Runtime.addBinding({name: 'send'});
InspectorTest.log('Reconnect..');
session.reconnect();
await session.Protocol.Runtime.evaluate({expression: `send('payload')`});
},
async function testBindingOverrides() {
const {contextGroup, sessions: [session]} = setupSessions(1);
InspectorTest.log('\nAdd send function on global object..');
session.Protocol.Runtime.evaluate({expression: 'send = () => 42'});
InspectorTest.log('Add binding inside session..');
session.Protocol.Runtime.addBinding({name: 'send'});
InspectorTest.log('Call binding..');
await session.Protocol.Runtime.evaluate({expression: `send('payload')`});
},
async function testRemoveBinding() {
const {contextGroup, sessions: [session]} = setupSessions(1);
InspectorTest.log('\nAdd binding inside session..');
session.Protocol.Runtime.addBinding({name: 'send'});
InspectorTest.log('Call binding..');
await session.Protocol.Runtime.evaluate({expression: `send('payload')`});
InspectorTest.log('Remove binding inside session..');
session.Protocol.Runtime.removeBinding({name: 'send'});
InspectorTest.log('Call binding..');
await session.Protocol.Runtime.evaluate({expression: `send('payload')`});
}
]);
function setupSessions(num) {
const contextGroup = new InspectorTest.ContextGroup();
const sessions = [];
for (let i = 0; i < num; ++i) {
const session = contextGroup.connect();
sessions.push(session);
session.Protocol.Runtime.enable();
session.Protocol.Runtime.onBindingCalled(msg => {
InspectorTest.log(`binding called in session${i + 1}`);
InspectorTest.logMessage(msg);
});
}
return {contextGroup, sessions};
}