Commit 49c4ac77 authored by Alexey Kozyatinskiy's avatar Alexey Kozyatinskiy Committed by Commit Bot

[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: 's avatarDmitry Gozman <dgozman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53462}
parent bbfe7f32
......@@ -2872,9 +2872,51 @@
"name": "terminateExecution",
"description": "Terminate current or next JavaScript execution.\nWill cancel the termination when the outer-most script execution ends.",
"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": [
{
"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",
"description": "Issued when console API was called.",
......
......@@ -1322,6 +1322,29 @@ domain Runtime
# Will cancel the termination when the outer-most script execution ends.
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.
event consoleAPICalled
parameters
......
......@@ -206,6 +206,7 @@ void V8InspectorImpl::contextCreated(const V8ContextInfo& info) {
(*contextById)[contextId].reset(context);
forEachSession(
info.contextGroupId, [&context](V8InspectorSessionImpl* session) {
session->runtimeAgent()->addBindings(context);
session->runtimeAgent()->reportExecutionContextCreated(context);
});
}
......
......@@ -54,6 +54,7 @@ namespace V8RuntimeAgentImplState {
static const char customObjectFormatterEnabled[] =
"customObjectFormatterEnabled";
static const char runtimeEnabled[] = "runtimeEnabled";
static const char bindings[] = "bindings";
};
using protocol::Runtime::RemoteObject;
......@@ -639,6 +640,86 @@ void V8RuntimeAgentImpl::terminateExecution(
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() {
if (!m_state->booleanProperty(V8RuntimeAgentImplState::runtimeEnabled, false))
return;
......@@ -647,6 +728,10 @@ void V8RuntimeAgentImpl::restore() {
if (m_state->booleanProperty(
V8RuntimeAgentImplState::customObjectFormatterEnabled, false))
m_session->setCustomObjectFormatterEnabled(true);
m_inspector->forEachContext(
m_session->contextGroupId(),
[this](InspectedContext* context) { addBindings(context); });
}
Response V8RuntimeAgentImpl::enable() {
......@@ -669,6 +754,7 @@ Response V8RuntimeAgentImpl::disable() {
if (!m_enabled) return Response::OK();
m_enabled = false;
m_state->setBoolean(V8RuntimeAgentImplState::runtimeEnabled, false);
m_state->remove(V8RuntimeAgentImplState::bindings);
m_inspector->disableStackCapturingIfNeeded();
m_session->setCustomObjectFormatterEnabled(false);
reset();
......
......@@ -110,6 +110,10 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
void terminateExecution(
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 reportExecutionContextCreated(InspectedContext*);
void reportExecutionContextDestroyed(InspectedContext*);
......@@ -121,6 +125,11 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
private:
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;
protocol::DictionaryValue* m_state;
protocol::Runtime::Frontend m_frontend;
......
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..
// 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};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment