885a5669aa
Sometimes we need to capture stack trace on one debugger and use it later as a parent stack on another debugger (e.g. worker.postMessage). This CL includes following addition to our protocol and v8-inspector.h: - added Runtime.StackTraceId, this id represents stack trace captured on debugger with given id, - protocol client can fetch Runtime.StackTrace by Runtime.StacKTraceId using Debugger.getStackTrace method, - externalParent field is added to Debugger.paused event, it may contain external parent stack trace, - V8Inspector::storeCurrentStackTrace captures current stack trace and returns V8StackTraceId for embedder this id can be used as argument for V8Inspector::externalAsyncTaskStarted and V8Inspector::externalAsyncTaskFinished method. Any async stack trace captured between these calls will get passed external stack trace as external parent. These methods are designed to be called on different debuggers. If async task is scheduled and started on one debugger user should continue to use asyncTask* API, - Debugger.enable methods returns unique debuggerId. TBR=dgozman@chromium.org,jgruber@chromium.org Bug: chromium:778796 Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel;master.tryserver.chromium.linux:linux_chromium_rel_ng Change-Id: I2c1a2b2e30ed69ccb61d10f08686f4edb09f50e4 Reviewed-on: https://chromium-review.googlesource.com/786274 Commit-Queue: Aleksey Kozyatinskiy <kozyatinskiy@chromium.org> Reviewed-by: Aleksey Kozyatinskiy <kozyatinskiy@chromium.org> Cr-Commit-Position: refs/heads/master@{#49591}
412 lines
14 KiB
JavaScript
412 lines
14 KiB
JavaScript
// Copyright 2016 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 = {};
|
|
InspectorTest._dumpInspectorProtocolMessages = false;
|
|
InspectorTest._commandsForLogging = new Set();
|
|
InspectorTest._sessions = new Set();
|
|
|
|
InspectorTest.log = utils.print.bind(utils);
|
|
InspectorTest.quitImmediately = utils.quit.bind(utils);
|
|
|
|
InspectorTest.logProtocolCommandCalls = function(command) {
|
|
InspectorTest._commandsForLogging.add(command);
|
|
}
|
|
|
|
InspectorTest.completeTest = function() {
|
|
var promises = [];
|
|
for (var session of InspectorTest._sessions)
|
|
promises.push(session.Protocol.Debugger.disable());
|
|
Promise.all(promises).then(() => utils.quit());
|
|
}
|
|
|
|
InspectorTest.waitForPendingTasks = function() {
|
|
var promises = [];
|
|
for (var session of InspectorTest._sessions)
|
|
promises.push(session.Protocol.Runtime.evaluate({ expression: "new Promise(r => setTimeout(r, 0))//# sourceURL=wait-for-pending-tasks.js", awaitPromise: true }));
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
InspectorTest.startDumpingProtocolMessages = function() {
|
|
InspectorTest._dumpInspectorProtocolMessages = true;
|
|
}
|
|
|
|
InspectorTest.logMessage = function(originalMessage) {
|
|
var message = JSON.parse(JSON.stringify(originalMessage));
|
|
if (message.id)
|
|
message.id = "<messageId>";
|
|
|
|
const nonStableFields = new Set([
|
|
'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId',
|
|
'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId',
|
|
'formatterObjectId', 'debuggerId'
|
|
]);
|
|
var objects = [ message ];
|
|
while (objects.length) {
|
|
var object = objects.shift();
|
|
for (var key in object) {
|
|
if (nonStableFields.has(key))
|
|
object[key] = `<${key}>`;
|
|
else if (typeof object[key] === "string" && object[key].match(/\d+:\d+:\d+:\d+/))
|
|
object[key] = object[key].substring(0, object[key].lastIndexOf(':')) + ":<scriptId>";
|
|
else if (typeof object[key] === "object")
|
|
objects.push(object[key]);
|
|
}
|
|
}
|
|
|
|
InspectorTest.logObject(message);
|
|
return originalMessage;
|
|
}
|
|
|
|
InspectorTest.logObject = function(object, title) {
|
|
var lines = [];
|
|
|
|
function dumpValue(value, prefix, prefixWithName) {
|
|
if (typeof value === "object" && value !== null) {
|
|
if (value instanceof Array)
|
|
dumpItems(value, prefix, prefixWithName);
|
|
else
|
|
dumpProperties(value, prefix, prefixWithName);
|
|
} else {
|
|
lines.push(prefixWithName + String(value).replace(/\n/g, " "));
|
|
}
|
|
}
|
|
|
|
function dumpProperties(object, prefix, firstLinePrefix) {
|
|
prefix = prefix || "";
|
|
firstLinePrefix = firstLinePrefix || prefix;
|
|
lines.push(firstLinePrefix + "{");
|
|
|
|
var propertyNames = Object.keys(object);
|
|
propertyNames.sort();
|
|
for (var i = 0; i < propertyNames.length; ++i) {
|
|
var name = propertyNames[i];
|
|
if (!object.hasOwnProperty(name))
|
|
continue;
|
|
var prefixWithName = " " + prefix + name + " : ";
|
|
dumpValue(object[name], " " + prefix, prefixWithName);
|
|
}
|
|
lines.push(prefix + "}");
|
|
}
|
|
|
|
function dumpItems(object, prefix, firstLinePrefix) {
|
|
prefix = prefix || "";
|
|
firstLinePrefix = firstLinePrefix || prefix;
|
|
lines.push(firstLinePrefix + "[");
|
|
for (var i = 0; i < object.length; ++i)
|
|
dumpValue(object[i], " " + prefix, " " + prefix + "[" + i + "] : ");
|
|
lines.push(prefix + "]");
|
|
}
|
|
|
|
dumpValue(object, "", title || "");
|
|
InspectorTest.log(lines.join("\n"));
|
|
}
|
|
|
|
InspectorTest.ContextGroup = class {
|
|
constructor() {
|
|
this.id = utils.createContextGroup();
|
|
}
|
|
|
|
schedulePauseOnNextStatement(reason, details) {
|
|
utils.schedulePauseOnNextStatement(this.id, reason, details);
|
|
}
|
|
|
|
cancelPauseOnNextStatement() {
|
|
utils.cancelPauseOnNextStatement(this.id);
|
|
}
|
|
|
|
addScript(string, lineOffset, columnOffset, url) {
|
|
utils.compileAndRunWithOrigin(this.id, string, url || '', lineOffset || 0, columnOffset || 0, false);
|
|
}
|
|
|
|
addModule(string, url, lineOffset, columnOffset) {
|
|
utils.compileAndRunWithOrigin(this.id, string, url, lineOffset || 0, columnOffset || 0, true);
|
|
}
|
|
|
|
loadScript(fileName) {
|
|
this.addScript(utils.read(fileName));
|
|
}
|
|
|
|
connect() {
|
|
return new InspectorTest.Session(this);
|
|
}
|
|
|
|
setupInjectedScriptEnvironment(session) {
|
|
let scriptSource = '';
|
|
// First define all getters on Object.prototype.
|
|
let injectedScriptSource = utils.read('src/inspector/injected-script-source.js');
|
|
let getterRegex = /\.[a-zA-Z0-9]+/g;
|
|
let match;
|
|
let getters = new Set();
|
|
while (match = getterRegex.exec(injectedScriptSource)) {
|
|
getters.add(match[0].substr(1));
|
|
}
|
|
scriptSource += `(function installSettersAndGetters() {
|
|
let defineProperty = Object.defineProperty;
|
|
let ObjectPrototype = Object.prototype;
|
|
let ArrayPrototype = Array.prototype;
|
|
defineProperty(ArrayPrototype, 0, {
|
|
set() { debugger; throw 42; }, get() { debugger; throw 42; },
|
|
__proto__: null
|
|
});`,
|
|
scriptSource += Array.from(getters).map(getter => `
|
|
defineProperty(ObjectPrototype, '${getter}', {
|
|
set() { debugger; throw 42; }, get() { debugger; throw 42; },
|
|
__proto__: null
|
|
});
|
|
`).join('\n') + '})();';
|
|
this.addScript(scriptSource);
|
|
|
|
if (session) {
|
|
InspectorTest.log('WARNING: setupInjectedScriptEnvironment with debug flag for debugging only and should not be landed.');
|
|
InspectorTest.log('WARNING: run test with --expose-inspector-scripts flag to get more details.');
|
|
InspectorTest.log('WARNING: you can additionally comment rjsmin in xxd.py to get unminified injected-script-source.js.');
|
|
session.setupScriptMap();
|
|
session.Protocol.Debugger.enable();
|
|
session.Protocol.Debugger.onPaused(message => {
|
|
let callFrames = message.params.callFrames;
|
|
session.logSourceLocations(callFrames.map(frame => frame.location));
|
|
})
|
|
}
|
|
}
|
|
};
|
|
|
|
InspectorTest.Session = class {
|
|
constructor(contextGroup) {
|
|
this.contextGroup = contextGroup;
|
|
this._dispatchTable = new Map();
|
|
this._eventHandlers = new Map();
|
|
this._requestId = 0;
|
|
this.Protocol = this._setupProtocol();
|
|
InspectorTest._sessions.add(this);
|
|
this.id = utils.connectSession(contextGroup.id, '', this._dispatchMessage.bind(this));
|
|
}
|
|
|
|
disconnect() {
|
|
InspectorTest._sessions.delete(this);
|
|
utils.disconnectSession(this.id);
|
|
}
|
|
|
|
reconnect() {
|
|
var state = utils.disconnectSession(this.id);
|
|
this.id = utils.connectSession(this.contextGroup.id, state, this._dispatchMessage.bind(this));
|
|
}
|
|
|
|
async addInspectedObject(serializable) {
|
|
return this.Protocol.Runtime.evaluate({expression: `inspector.addInspectedObject(${this.id}, ${JSON.stringify(serializable)})`});
|
|
}
|
|
|
|
sendRawCommand(requestId, command, handler) {
|
|
if (InspectorTest._dumpInspectorProtocolMessages)
|
|
utils.print("frontend: " + command);
|
|
this._dispatchTable.set(requestId, handler);
|
|
utils.sendMessageToBackend(this.id, command);
|
|
}
|
|
|
|
setupScriptMap() {
|
|
if (this._scriptMap)
|
|
return;
|
|
this._scriptMap = new Map();
|
|
}
|
|
|
|
logCallFrames(callFrames) {
|
|
for (var frame of callFrames) {
|
|
var functionName = frame.functionName || '(anonymous)';
|
|
var url = frame.url ? frame.url : this._scriptMap.get(frame.location.scriptId).url;
|
|
var lineNumber = frame.location ? frame.location.lineNumber : frame.lineNumber;
|
|
var columnNumber = frame.location ? frame.location.columnNumber : frame.columnNumber;
|
|
InspectorTest.log(`${functionName} (${url}:${lineNumber}:${columnNumber})`);
|
|
}
|
|
}
|
|
|
|
logSourceLocation(location, forceSourceRequest) {
|
|
var scriptId = location.scriptId;
|
|
if (!this._scriptMap || !this._scriptMap.has(scriptId)) {
|
|
InspectorTest.log("setupScriptMap should be called before Protocol.Debugger.enable.");
|
|
InspectorTest.completeTest();
|
|
}
|
|
var script = this._scriptMap.get(scriptId);
|
|
if (!script.scriptSource || forceSourceRequest) {
|
|
return this.Protocol.Debugger.getScriptSource({ scriptId })
|
|
.then(message => script.scriptSource = message.result.scriptSource)
|
|
.then(dumpSourceWithLocation);
|
|
}
|
|
return Promise.resolve().then(dumpSourceWithLocation);
|
|
|
|
function dumpSourceWithLocation() {
|
|
var lines = script.scriptSource.split('\n');
|
|
var line = lines[location.lineNumber];
|
|
line = line.slice(0, location.columnNumber) + '#' + (line.slice(location.columnNumber) || '');
|
|
lines[location.lineNumber] = line;
|
|
lines = lines.filter(line => line.indexOf('//# sourceURL=') === -1);
|
|
InspectorTest.log(lines.slice(Math.max(location.lineNumber - 1, 0), location.lineNumber + 2).join('\n'));
|
|
InspectorTest.log('');
|
|
}
|
|
}
|
|
|
|
logSourceLocations(locations) {
|
|
if (locations.length == 0) return Promise.resolve();
|
|
return this.logSourceLocation(locations[0]).then(() => this.logSourceLocations(locations.splice(1)));
|
|
}
|
|
|
|
async logBreakLocations(inputLocations) {
|
|
let locations = inputLocations.slice();
|
|
let scriptId = locations[0].scriptId;
|
|
let script = this._scriptMap.get(scriptId);
|
|
if (!script.scriptSource) {
|
|
let message = await this.Protocol.Debugger.getScriptSource({scriptId});
|
|
script.scriptSource = message.result.scriptSource;
|
|
}
|
|
let lines = script.scriptSource.split('\n');
|
|
locations = locations.sort((loc1, loc2) => {
|
|
if (loc2.lineNumber !== loc1.lineNumber) return loc2.lineNumber - loc1.lineNumber;
|
|
return loc2.columnNumber - loc1.columnNumber;
|
|
});
|
|
for (let location of locations) {
|
|
let line = lines[location.lineNumber];
|
|
line = line.slice(0, location.columnNumber) + locationMark(location.type) + line.slice(location.columnNumber);
|
|
lines[location.lineNumber] = line;
|
|
}
|
|
lines = lines.filter(line => line.indexOf('//# sourceURL=') === -1);
|
|
InspectorTest.log(lines.join('\n') + '\n');
|
|
return inputLocations;
|
|
|
|
function locationMark(type) {
|
|
if (type === 'return') return '|R|';
|
|
if (type === 'call') return '|C|';
|
|
if (type === 'debuggerStatement') return '|D|';
|
|
return '|_|';
|
|
}
|
|
}
|
|
|
|
async logTypeProfile(typeProfile, source) {
|
|
let entries = typeProfile.entries;
|
|
|
|
// Sort in reverse order so we can replace entries without invalidating
|
|
// the other offsets.
|
|
entries = entries.sort((a, b) => b.offset - a.offset);
|
|
|
|
for (let entry of entries) {
|
|
source = source.slice(0, entry.offset) + typeAnnotation(entry.types) +
|
|
source.slice(entry.offset);
|
|
}
|
|
InspectorTest.log(source);
|
|
return typeProfile;
|
|
|
|
function typeAnnotation(types) {
|
|
return `/*${types.map(t => t.name).join(', ')}*/`;
|
|
}
|
|
}
|
|
|
|
logAsyncStackTrace(asyncStackTrace) {
|
|
while (asyncStackTrace) {
|
|
if (asyncStackTrace.promiseCreationFrame) {
|
|
var frame = asyncStackTrace.promiseCreationFrame;
|
|
InspectorTest.log(`-- ${asyncStackTrace.description} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})--`);
|
|
} else {
|
|
InspectorTest.log(`-- ${asyncStackTrace.description} --`);
|
|
}
|
|
this.logCallFrames(asyncStackTrace.callFrames);
|
|
asyncStackTrace = asyncStackTrace.parent;
|
|
}
|
|
}
|
|
|
|
_sendCommandPromise(method, params) {
|
|
if (InspectorTest._commandsForLogging.has(method))
|
|
utils.print(method + ' called');
|
|
var requestId = ++this._requestId;
|
|
var messageObject = { "id": requestId, "method": method, "params": params };
|
|
return new Promise(fulfill => this.sendRawCommand(requestId, JSON.stringify(messageObject), fulfill));
|
|
}
|
|
|
|
_setupProtocol() {
|
|
return new Proxy({}, { get: (target, agentName, receiver) => new Proxy({}, {
|
|
get: (target, methodName, receiver) => {
|
|
const eventPattern = /^on(ce)?([A-Z][A-Za-z0-9]+)/;
|
|
var match = eventPattern.exec(methodName);
|
|
if (!match)
|
|
return args => this._sendCommandPromise(`${agentName}.${methodName}`, args || {});
|
|
var eventName = match[2];
|
|
eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
|
|
if (match[1])
|
|
return () => this._waitForEventPromise(`${agentName}.${eventName}`);
|
|
return listener => this._eventHandlers.set(`${agentName}.${eventName}`, listener);
|
|
}
|
|
})});
|
|
}
|
|
|
|
_dispatchMessage(messageString) {
|
|
var messageObject = JSON.parse(messageString);
|
|
if (InspectorTest._dumpInspectorProtocolMessages)
|
|
utils.print("backend: " + JSON.stringify(messageObject));
|
|
try {
|
|
var messageId = messageObject["id"];
|
|
if (typeof messageId === "number") {
|
|
var handler = this._dispatchTable.get(messageId);
|
|
if (handler) {
|
|
handler(messageObject);
|
|
this._dispatchTable.delete(messageId);
|
|
}
|
|
} else {
|
|
var eventName = messageObject["method"];
|
|
var eventHandler = this._eventHandlers.get(eventName);
|
|
if (this._scriptMap && eventName === "Debugger.scriptParsed")
|
|
this._scriptMap.set(messageObject.params.scriptId, JSON.parse(JSON.stringify(messageObject.params)));
|
|
if (eventName === "Debugger.scriptParsed" && messageObject.params.url === "wait-for-pending-tasks.js")
|
|
return;
|
|
if (eventHandler)
|
|
eventHandler(messageObject);
|
|
}
|
|
} catch (e) {
|
|
InspectorTest.log("Exception when dispatching message: " + e + "\n" + e.stack + "\n message = " + JSON.stringify(messageObject, null, 2));
|
|
InspectorTest.completeTest();
|
|
}
|
|
};
|
|
|
|
_waitForEventPromise(eventName) {
|
|
return new Promise(fulfill => {
|
|
this._eventHandlers.set(eventName, result => {
|
|
delete this._eventHandlers.delete(eventName);
|
|
fulfill(result);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
InspectorTest.runTestSuite = function(testSuite) {
|
|
function nextTest() {
|
|
if (!testSuite.length) {
|
|
InspectorTest.completeTest();
|
|
return;
|
|
}
|
|
var fun = testSuite.shift();
|
|
InspectorTest.log("\nRunning test: " + fun.name);
|
|
fun(nextTest);
|
|
}
|
|
nextTest();
|
|
}
|
|
|
|
InspectorTest.runAsyncTestSuite = async function(testSuite) {
|
|
for (var test of testSuite) {
|
|
InspectorTest.log("\nRunning test: " + test.name);
|
|
try {
|
|
await test();
|
|
} catch (e) {
|
|
utils.print(e.stack);
|
|
}
|
|
}
|
|
InspectorTest.completeTest();
|
|
}
|
|
|
|
InspectorTest.start = function(description) {
|
|
try {
|
|
InspectorTest.log(description);
|
|
var contextGroup = new InspectorTest.ContextGroup();
|
|
var session = contextGroup.connect();
|
|
return { session: session, contextGroup: contextGroup, Protocol: session.Protocol };
|
|
} catch (e) {
|
|
utils.print(e.stack);
|
|
}
|
|
}
|