From 5cb11a17cdd1762f52ef5e46a5c963e33c5d5b06 Mon Sep 17 00:00:00 2001 From: Alexey Kozyatinskiy Date: Thu, 31 May 2018 13:42:07 -0700 Subject: [PATCH] Reland "[inspector] added Runtime.installBinding method" This is a reland of 49c4ac7753de637d2baba09811bd7fbbcb01fe7c 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 > Reviewed-by: Dmitry Gozman > 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 Commit-Queue: Aleksey Kozyatinskiy Cr-Commit-Position: refs/heads/master@{#53479} --- src/inspector/js_protocol.json | 42 ++++++++ src/inspector/js_protocol.pdl | 23 +++++ src/inspector/v8-inspector-impl.cc | 1 + src/inspector/v8-runtime-agent-impl.cc | 86 ++++++++++++++++ src/inspector/v8-runtime-agent-impl.h | 9 ++ .../runtime/add-binding-expected.txt | 99 +++++++++++++++++++ test/inspector/runtime/add-binding.js | 82 +++++++++++++++ 7 files changed, 342 insertions(+) create mode 100644 test/inspector/runtime/add-binding-expected.txt create mode 100644 test/inspector/runtime/add-binding.js diff --git a/src/inspector/js_protocol.json b/src/inspector/js_protocol.json index 0d5b800b42..facd2a27a6 100644 --- a/src/inspector/js_protocol.json +++ b/src/inspector/js_protocol.json @@ -2884,9 +2884,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.", diff --git a/src/inspector/js_protocol.pdl b/src/inspector/js_protocol.pdl index 290530281e..23feed452c 100644 --- a/src/inspector/js_protocol.pdl +++ b/src/inspector/js_protocol.pdl @@ -1330,6 +1330,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 diff --git a/src/inspector/v8-inspector-impl.cc b/src/inspector/v8-inspector-impl.cc index 0eb87da94b..6272e4b4b2 100644 --- a/src/inspector/v8-inspector-impl.cc +++ b/src/inspector/v8-inspector-impl.cc @@ -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); }); } diff --git a/src/inspector/v8-runtime-agent-impl.cc b/src/inspector/v8-runtime-agent-impl.cc index 531f707d62..60f1de1553 100644 --- a/src/inspector/v8-runtime-agent-impl.cc +++ b/src/inspector/v8-runtime-agent-impl.cc @@ -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& 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(v8::debug::GetInspector(isolate)); + int contextId = InspectedContext::contextId(isolate->GetCurrentContext()); + int contextGroupId = inspector->contextGroupId(contextId); + + String16 name = toProtocolString(v8::Local::Cast(info.Data())); + String16 payload = toProtocolString(v8::Local::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 localContext = context->context(); + v8::Local global = localContext->Global(); + v8::Local v8Name = toV8String(m_inspector->isolate(), name); + v8::Local functionValue; + v8::MicrotasksScope microtasks(m_inspector->isolate(), + v8::MicrotasksScope::kDoNotRunMicrotasks); + if (v8::Function::New(localContext, bindingCallback, v8Name) + .ToLocal(&functionValue)) { + v8::Maybe 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(); diff --git a/src/inspector/v8-runtime-agent-impl.h b/src/inspector/v8-runtime-agent-impl.h index 420092e72a..9ed3bd750d 100644 --- a/src/inspector/v8-runtime-agent-impl.h +++ b/src/inspector/v8-runtime-agent-impl.h @@ -110,6 +110,10 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend { void terminateExecution( std::unique_ptr 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& 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; diff --git a/test/inspector/runtime/add-binding-expected.txt b/test/inspector/runtime/add-binding-expected.txt new file mode 100644 index 0000000000..94d5ed4e0a --- /dev/null +++ b/test/inspector/runtime/add-binding-expected.txt @@ -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 : + name : send + payload : payload + } +} + +Add binding inside session2.. +Call binding.. +binding called in session1 +{ + method : Runtime.bindingCalled + params : { + executionContextId : + name : send + payload : payload + } +} +binding called in session2 +{ + method : Runtime.bindingCalled + params : { + executionContextId : + name : send + payload : payload + } +} + +Disable agent inside session1.. +Call binding.. +binding called in session2 +{ + method : Runtime.bindingCalled + params : { + 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 : + 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 : + name : send + payload : payload + } +} + +Running test: testRemoveBinding + +Add binding inside session.. +Call binding.. +binding called in session1 +{ + method : Runtime.bindingCalled + params : { + executionContextId : + name : send + payload : payload + } +} +Remove binding inside session.. +Call binding.. diff --git a/test/inspector/runtime/add-binding.js b/test/inspector/runtime/add-binding.js new file mode 100644 index 0000000000..78e8d00be7 --- /dev/null +++ b/test/inspector/runtime/add-binding.js @@ -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}; +}