Commit 66daabcc authored by Alexey Kozyatinskiy's avatar Alexey Kozyatinskiy Committed by Commit Bot

inspector: generate custom preview using native code

Full custom preview generation is moved to custom-preview file
including frontend part. New custom preview implementation returns
body getter function instead of bind function, formatter and config
objects. Body getter function calls formatter.body(object, config)
and returns json ML.

R=dgozman@chromium.org

Bug: chromium:595206
Cq-Include-Trybots: luci.chromium.try:linux_chromium_headless_rel;master.tryserver.blink:linux_trusty_blink_rel
Change-Id: I14ff3d8abb4a47d2bbc2e6eaa1835fc362ac7369
Reviewed-on: https://chromium-review.googlesource.com/c/1292686
Commit-Queue: Aleksey Kozyatinskiy <kozyatinskiy@chromium.org>
Reviewed-by: 's avatarDmitry Gozman <dgozman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#56872}
parent 7126530d
......@@ -103,6 +103,8 @@ v8_source_set("inspector") {
]
sources += get_target_outputs(":inspector_injected_script")
sources += [
"custom-preview.cc",
"custom-preview.h",
"injected-script.cc",
"injected-script.h",
"inspected-context.cc",
......
This diff is collapsed.
// 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.
#ifndef V8_INSPECTOR_CUSTOM_PREVIEW_H_
#define V8_INSPECTOR_CUSTOM_PREVIEW_H_
#include "src/inspector/protocol/Protocol.h"
#include "src/inspector/protocol/Runtime.h"
namespace v8_inspector {
const int kMaxCustomPreviewDepth = 20;
void generateCustomPreview(
int sessionId, const String16& groupName, v8::Local<v8::Context> context,
v8::Local<v8::Object> object, v8::MaybeLocal<v8::Value> config,
int maxDepth, std::unique_ptr<protocol::Runtime::CustomPreview>* preview);
} // namespace v8_inspector
#endif // V8_INSPECTOR_CUSTOM_PREVIEW_H_
......@@ -252,14 +252,13 @@ InjectedScript.prototype = {
* @param {?Array.<string>=} columnNames
* @param {boolean=} isTable
* @param {boolean=} doNotBind
* @param {*=} customObjectConfig
* @return {!RuntimeAgent.RemoteObject}
* @suppress {checkTypes}
*/
_wrapObject: function(object, objectGroupName, forceValueType, generatePreview, columnNames, isTable, doNotBind, customObjectConfig)
_wrapObject: function(object, objectGroupName, forceValueType, generatePreview, columnNames, isTable, doNotBind)
{
try {
return new InjectedScript.RemoteObject(object, objectGroupName, doNotBind, forceValueType, generatePreview, columnNames, isTable, undefined, customObjectConfig);
return new InjectedScript.RemoteObject(object, objectGroupName, doNotBind, forceValueType, generatePreview, columnNames, isTable, undefined);
} catch (e) {
try {
var description = injectedScript._describe(e);
......@@ -503,40 +502,6 @@ InjectedScript.prototype = {
return descriptors;
},
/**
* @param {string|undefined} objectGroupName
* @param {*} jsonMLObject
* @throws {string} error message
*/
_substituteObjectTagsInCustomPreview: function(objectGroupName, jsonMLObject)
{
var maxCustomPreviewRecursionDepth = 20;
this._customPreviewRecursionDepth = (this._customPreviewRecursionDepth || 0) + 1
try {
if (this._customPreviewRecursionDepth >= maxCustomPreviewRecursionDepth)
throw new Error("Too deep hierarchy of inlined custom previews");
if (!isArrayLike(jsonMLObject))
return;
if (jsonMLObject[0] === "object") {
var attributes = jsonMLObject[1];
var originObject = attributes["object"];
var config = attributes["config"];
if (typeof originObject === "undefined")
throw new Error("Illegal format: obligatory attribute \"object\" isn't specified");
jsonMLObject[1] = this._wrapObject(originObject, objectGroupName, false, false, null, false, false, config);
return;
}
for (var i = 0; i < jsonMLObject.length; ++i)
this._substituteObjectTagsInCustomPreview(objectGroupName, jsonMLObject[i]);
} finally {
this._customPreviewRecursionDepth--;
}
},
/**
* @param {*} object
* @return {boolean}
......@@ -699,14 +664,6 @@ InjectedScript.prototype = {
if (value === null)
return "" + value;
return this.isPrimitiveValue(value) ? toStringDescription(value) : (this._describe(value) || "");
},
/**
* @param {boolean} enabled
*/
setCustomObjectFormatterEnabled: function(enabled)
{
this._customObjectFormatterEnabled = enabled;
}
}
......@@ -726,9 +683,8 @@ var injectedScript = new InjectedScript();
* @param {?Array.<string>=} columnNames
* @param {boolean=} isTable
* @param {boolean=} skipEntriesPreview
* @param {*=} customObjectConfig
*/
InjectedScript.RemoteObject = function(object, objectGroupName, doNotBind, forceValueType, generatePreview, columnNames, isTable, skipEntriesPreview, customObjectConfig)
InjectedScript.RemoteObject = function(object, objectGroupName, doNotBind, forceValueType, generatePreview, columnNames, isTable, skipEntriesPreview)
{
this.type = typeof object;
if (this.type === "undefined" && injectedScript._isHTMLAllCollection(object))
......@@ -792,72 +748,9 @@ InjectedScript.RemoteObject = function(object, objectGroupName, doNotBind, force
else
this.preview = this._generatePreview(object, undefined, columnNames, isTable, skipEntriesPreview);
}
if (injectedScript._customObjectFormatterEnabled) {
var customPreview = this._customPreview(object, objectGroupName, customObjectConfig);
if (customPreview)
this.customPreview = customPreview;
}
}
InjectedScript.RemoteObject.prototype = {
/**
* @param {*} object
* @param {string=} objectGroupName
* @param {*=} customObjectConfig
* @return {?RuntimeAgent.CustomPreview}
*/
_customPreview: function(object, objectGroupName, customObjectConfig)
{
/**
* @param {!Error} error
*/
function logError(error)
{
// We use user code to generate custom output for object, we can use user code for reporting error too.
Promise.resolve().then(/* suppressBlacklist */ inspectedGlobalObject.console.error.bind(inspectedGlobalObject.console, "Custom Formatter Failed: " + error.message));
}
/**
* @param {*} object
* @param {*=} customObjectConfig
* @return {*}
*/
function wrap(object, customObjectConfig)
{
return injectedScript._wrapObject(object, objectGroupName, false, false, null, false, false, customObjectConfig);
}
try {
var formatters = inspectedGlobalObject["devtoolsFormatters"];
if (!formatters || !isArrayLike(formatters))
return null;
for (var i = 0; i < formatters.length; ++i) {
try {
var formatted = formatters[i].header(object, customObjectConfig);
if (!formatted)
continue;
var hasBody = formatters[i].hasBody(object, customObjectConfig);
injectedScript._substituteObjectTagsInCustomPreview(objectGroupName, formatted);
var formatterObjectId = injectedScript._bind(formatters[i], objectGroupName);
var bindRemoteObjectFunctionId = injectedScript._bind(wrap, objectGroupName);
var result = {header: JSON.stringify(formatted), hasBody: !!hasBody, formatterObjectId: formatterObjectId, bindRemoteObjectFunctionId: bindRemoteObjectFunctionId};
if (customObjectConfig)
result["configObjectId"] = injectedScript._bind(customObjectConfig, objectGroupName);
return result;
} catch (e) {
logError(e);
}
}
} catch (e) {
logError(e);
}
return null;
},
/**
* @return {!RuntimeAgent.ObjectPreview} preview
*/
......
......@@ -30,6 +30,7 @@
#include "src/inspector/injected-script.h"
#include "src/inspector/custom-preview.h"
#include "src/inspector/injected-script-source.h"
#include "src/inspector/inspected-context.h"
#include "src/inspector/protocol/Protocol.h"
......@@ -381,6 +382,16 @@ Response InjectedScript::wrapObject(
v8::Local<v8::Value> value, const String16& groupName, bool forceValueType,
bool generatePreview,
std::unique_ptr<protocol::Runtime::RemoteObject>* result) const {
return wrapObject(value, groupName, forceValueType, generatePreview,
v8::MaybeLocal<v8::Value>(), kMaxCustomPreviewDepth,
result);
}
Response InjectedScript::wrapObject(
v8::Local<v8::Value> value, const String16& groupName, bool forceValueType,
bool generatePreview, v8::MaybeLocal<v8::Value> customPreviewConfig,
int maxCustomPreviewDepth,
std::unique_ptr<protocol::Runtime::RemoteObject>* result) const {
v8::HandleScope handles(m_context->isolate());
v8::Local<v8::Value> wrappedObject;
v8::Local<v8::Context> context = m_context->context();
......@@ -395,6 +406,13 @@ Response InjectedScript::wrapObject(
*result =
protocol::Runtime::RemoteObject::fromValue(protocolValue.get(), &errors);
if (!result->get()) return Response::Error(errors.errors());
if (m_customPreviewEnabled && value->IsObject()) {
std::unique_ptr<protocol::Runtime::CustomPreview> customPreview;
generateCustomPreview(m_sessionId, groupName, m_context->context(),
value.As<v8::Object>(), customPreviewConfig,
maxCustomPreviewDepth, &customPreview);
if (customPreview) (*result)->setCustomPreview(std::move(customPreview));
}
return Response::OK();
}
......@@ -496,13 +514,7 @@ void InjectedScript::releaseObjectGroup(const String16& objectGroup) {
}
void InjectedScript::setCustomObjectFormatterEnabled(bool enabled) {
v8::HandleScope handles(m_context->isolate());
V8FunctionCall function(m_context->inspector(), m_context->context(),
v8Value(), "setCustomObjectFormatterEnabled");
function.appendArgument(enabled);
bool hadException = false;
function.call(hadException);
DCHECK(!hadException);
m_customPreviewEnabled = enabled;
}
v8::Local<v8::Value> InjectedScript::v8Value() const {
......
......@@ -85,6 +85,11 @@ class InjectedScript final {
v8::Local<v8::Value>, const String16& groupName, bool forceValueType,
bool generatePreview,
std::unique_ptr<protocol::Runtime::RemoteObject>* result) const;
Response wrapObject(
v8::Local<v8::Value>, const String16& groupName, bool forceValueType,
bool generatePreview, v8::MaybeLocal<v8::Value> customPreviewConfig,
int maxCustomPreviewDepth,
std::unique_ptr<protocol::Runtime::RemoteObject>* result) const;
std::unique_ptr<protocol::Runtime::RemoteObject> wrapTable(
v8::Local<v8::Value> table, v8::Local<v8::Value> columns) const;
......@@ -216,6 +221,7 @@ class InjectedScript final {
std::unordered_map<int, String16> m_idToObjectGroupName;
std::unordered_map<String16, std::vector<int>> m_nameToObjectGroup;
std::unordered_set<EvaluateCallback*> m_evaluateCallbacks;
bool m_customPreviewEnabled = false;
DISALLOW_COPY_AND_ASSIGN(InjectedScript);
};
......
......@@ -1911,22 +1911,12 @@
"properties": [
{
"name": "header",
"description": "The JSON-stringified result of formatter.header(object, config) call.\nIt contains json ML array that represents RemoteObject.",
"type": "string"
},
{
"name": "hasBody",
"type": "boolean"
},
{
"name": "formatterObjectId",
"$ref": "RemoteObjectId"
},
{
"name": "bindRemoteObjectFunctionId",
"$ref": "RemoteObjectId"
},
{
"name": "configObjectId",
"name": "bodyGetterId",
"description": "If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\nThe result value is json ML array.",
"optional": true,
"$ref": "RemoteObjectId"
}
......
......@@ -890,11 +890,13 @@ domain Runtime
experimental type CustomPreview extends object
properties
# The JSON-stringified result of formatter.header(object, config) call.
# It contains json ML array that represents RemoteObject.
string header
boolean hasBody
RemoteObjectId formatterObjectId
RemoteObjectId bindRemoteObjectFunctionId
optional RemoteObjectId configObjectId
# If formatter returns true as a result of formatter.hasBody call then bodyGetterId will
# contain RemoteObjectId for the function that returns result of formatter.body(object, config) call.
# The result value is json ML array.
optional RemoteObjectId bodyGetterId
# Object containing abbreviated remote object value.
experimental type ObjectPreview extends object
......
......@@ -36,7 +36,7 @@ InspectorTest.logMessage = function(originalMessage) {
const nonStableFields = new Set([
'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId',
'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId',
'formatterObjectId', 'debuggerId'
'formatterObjectId', 'debuggerId', 'bodyGetterId'
]);
const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields)));
if (message.id)
......
RemoteObject.CustomPreview
Dump custom previews..
{
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted by 1 ","a"]
}
{
id : <messageId>
result : {
result : {
type : object
value : [
[0] : span
[1] : {
}
[2] : Body formatted by 1
[3] : a
[4] : [
[0] : object
[1] : {
className : Object
description : Object
objectId : <objectId>
type : object
}
]
]
}
}
}
{
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted by 2 ","b"]
}
{
id : <messageId>
result : {
result : {
type : object
value : [
[0] : span
[1] : {
}
[2] : Body formatted by 2
[3] : b
]
}
}
}
{
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted by 1 ","c"]
}
{
id : <messageId>
result : {
result : {
type : object
value : [
[0] : span
[1] : {
}
[2] : Body formatted by 1
[3] : c
[4] : [
[0] : object
[1] : {
className : Object
description : Object
objectId : <objectId>
type : object
}
]
]
}
}
}
{
header : ["span",{},"Formatter with config ",["object",{"type":"object","className":"Object","description":"Object","objectId":"{\"injectedScriptId\":1,\"id\":10}","customPreview":{"header":"[\"span\",{},\"Header \",\"info: \",\"additional info\"]","bodyGetterId":"{\"injectedScriptId\":1,\"id\":11}"}}]]
}
Change formatters order and dump again..
{
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted by 1 ","a"]
}
{
id : <messageId>
result : {
result : {
type : object
value : [
[0] : span
[1] : {
}
[2] : Body formatted by 1
[3] : a
[4] : [
[0] : object
[1] : {
className : Object
description : Object
objectId : <objectId>
type : object
}
]
]
}
}
}
{
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted by 2 ","b"]
}
{
id : <messageId>
result : {
result : {
type : object
value : [
[0] : span
[1] : {
}
[2] : Body formatted by 2
[3] : b
]
}
}
}
{
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted by 2 ","c"]
}
{
id : <messageId>
result : {
result : {
type : object
value : [
[0] : span
[1] : {
}
[2] : Body formatted by 2
[3] : c
]
}
}
}
{
header : ["span",{},"Formatter with config ",["object",{"type":"object","className":"Object","description":"Object","objectId":"{\"injectedScriptId\":1,\"id\":21}","customPreview":{"header":"[\"span\",{},\"Header \",\"info: \",\"additional info\"]","bodyGetterId":"{\"injectedScriptId\":1,\"id\":22}"}}]]
}
Try to break custom preview..
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : Custom Formatter Failed: Uncaught 1
}
]
executionContextId : <executionContextId>
timestamp : <timestamp>
type : error
}
}
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : Custom Formatter Failed: Uncaught 1
}
]
executionContextId : <executionContextId>
timestamp : <timestamp>
type : error
}
}
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : Custom Formatter Failed: Uncaught 2
}
]
executionContextId : <executionContextId>
timestamp : <timestamp>
type : error
}
}
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : Custom Formatter Failed: Uncaught 3
}
]
executionContextId : <executionContextId>
timestamp : <timestamp>
type : error
}
}
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : Custom Formatter Failed: Uncaught 4
}
]
executionContextId : <executionContextId>
timestamp : <timestamp>
type : error
}
}
// 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.
const {session, contextGroup, Protocol} =
InspectorTest.start('RemoteObject.CustomPreview');
(async function test() {
contextGroup.addScript(`
var a = {name: 'a'};
var b = {name: 'b'};
var c = {name: 'c'};
a.formattableBy1 = true;
b.formattableBy2 = true;
c.formattableBy1 = true;
c.formattableBy2 = true;
var formatter1 = {
header: (x) => x.formattableBy1 ? ['span', {}, 'Header formatted by 1 ', x.name] : null,
hasBody: () => true,
body: (x) => ['span', {}, 'Body formatted by 1 ', x.name, ['object', {object: {}}]]
};
var formatter2 = {
header: (x) => x.formattableBy2 ? ['span', {}, 'Header formatted by 2 ', x.name] : null,
hasBody: (x) => true,
body: (x) => ['span', {}, 'Body formatted by 2 ', x.name]
};
var configTest = {};
var formatterWithConfig1 = {
header: function(x, config) {
if (x !== configTest || config)
return null;
return ['span', {}, 'Formatter with config ', ['object', {'object': x, 'config': {'info': 'additional info'}}]];
},
hasBody: (x) => false,
body: (x) => { throw 'Unreachable'; }
}
var formatterWithConfig2 = {
header: function(x, config) {
if (x !== configTest || !config)
return null;
return ['span', {}, 'Header ', 'info: ', config.info];
},
hasBody: (x, config) => config && config.info,
body: (x, config) => ['span', {}, 'body', 'info: ', config.info]
}
this.devtoolsFormatters = [formatter1, formatter2, formatterWithConfig1, formatterWithConfig2];
`);
Protocol.Runtime.enable();
Protocol.Runtime.setCustomObjectFormatterEnabled({enabled: true});
Protocol.Runtime.onConsoleAPICalled(m => InspectorTest.logMessage(m));
InspectorTest.log('Dump custom previews..');
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'a'}));
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'b'}));
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'c'}));
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'configTest'}));
InspectorTest.log('Change formatters order and dump again..');
await Protocol.Runtime.evaluate({
expression: 'this.devtoolsFormatters = [formatter2, formatter1, formatterWithConfig1, formatterWithConfig2]'
});
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'a'}));
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'b'}));
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'c'}));
await dumpCustomPreview(await Protocol.Runtime.evaluate({expression: 'configTest'}));
InspectorTest.log('Try to break custom preview..');
await Protocol.Runtime.evaluate({
expression: `Object.defineProperty(this, 'devtoolsFormatters', {
get: () => { throw 1; },
configurable: true
})`
});
Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
InspectorTest.logMessage(await Protocol.Runtime.onceConsoleAPICalled());
await Protocol.Runtime.evaluate({
expression: `Object.defineProperty(this, 'devtoolsFormatters', {
get: () => {
const arr = [1];
Object.defineProperty(arr, 0, { get: () => { throw 2; }});
return arr;
},
configurable: true
})`
});
Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
InspectorTest.logMessage(await Protocol.Runtime.onceConsoleAPICalled());
await Protocol.Runtime.evaluate({
expression: `Object.defineProperty(this, 'devtoolsFormatters', {
get: () => [{get header() { throw 3; }}],
configurable: true
})`
});
Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
InspectorTest.logMessage(await Protocol.Runtime.onceConsoleAPICalled());
await Protocol.Runtime.evaluate({
expression: `Object.defineProperty(this, 'devtoolsFormatters', {
get: () => [{header: () => { throw 4; }}],
configurable: true
})`
});
Protocol.Runtime.evaluate({ expression: '({})', generatePreview: true });
InspectorTest.logMessage(await Protocol.Runtime.onceConsoleAPICalled());
InspectorTest.completeTest();
})()
async function dumpCustomPreview(result) {
const { objectId, customPreview } = result.result.result;
InspectorTest.logMessage(customPreview);
if (customPreview.bodyGetterId) {
const body = await Protocol.Runtime.callFunctionOn({
objectId,
functionDeclaration: 'function(bodyGetter) { return bodyGetter.call(this); }',
arguments: [ { objectId: customPreview.bodyGetterId } ],
returnByValue: true
});
InspectorTest.logMessage(body);
}
}
......@@ -67,9 +67,7 @@ will reconnect..
[0] : {
className : Object
customPreview : {
bindRemoteObjectFunctionId : <bindRemoteObjectFunctionId>
formatterObjectId : <formatterObjectId>
hasBody : true
bodyGetterId : <bodyGetterId>
header : ["span",{},"Header formatted ",42]
}
description : Object
......
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