Bug 917963 - Implement break on first script statement. r=davidwalsh

Differential Revision: https://phabricator.services.mozilla.com/D85198
This commit is contained in:
Logan Smyth 2020-08-04 18:46:48 +00:00
Родитель db7d4269ad
Коммит 06838cf425
5 изменённых файлов: 135 добавлений и 12 удалений

Просмотреть файл

@ -20,6 +20,7 @@ add_task(async function() {
"event.xhr.load",
"timer.timeout.set",
"timer.timeout.fire",
"script.source.firstStatement",
]);
invokeInTab("clickHandler");
@ -41,6 +42,11 @@ add_task(async function() {
assertPauseLocation(dbg, 28);
await resume(dbg);
invokeInTab("evalHandler");
await waitForPaused(dbg);
assertPauseLocation(dbg, 2, "http://example.com/eval-test.js");
await resume(dbg);
// Test that we don't pause on event breakpoints when source is blackboxed.
await clickElement(dbg, "blackbox");
await waitForDispatch(dbg, "BLACKBOX");
@ -59,10 +65,10 @@ add_task(async function() {
await waitForDispatch(dbg, "BLACKBOX");
});
function assertPauseLocation(dbg, line) {
function assertPauseLocation(dbg, line, url = "event-breakpoints.js") {
const { location } = dbg.selectors.getVisibleSelectedFrame();
const source = findSource(dbg, "event-breakpoints.js");
const source = findSource(dbg, url);
is(location.sourceId, source.id, `correct sourceId`);
is(location.line, line, `correct line`);

Просмотреть файл

@ -12,6 +12,7 @@
<button id="click-button">Run Click Handler</button>
<button id="xhr-button">Run XHR Handler</button>
<button id="timer-button">Run Timer Handler</button>
<button id="eval-button">Run Eval</button>
<div id="click-target" style="margin: 50px; background-color: green;">
Click Target
</div>

Просмотреть файл

@ -29,3 +29,11 @@ function timerHandler() {
}, 50);
console.log("timer set");
}
document.getElementById("eval-button").onmousedown = evalHandler;
function evalHandler() {
eval(`
console.log("eval ran");
//# sourceURL=http://example.com/eval-test.js
`);
}

Просмотреть файл

@ -21,6 +21,8 @@ const { threadSpec } = require("devtools/shared/specs/thread");
const {
getAvailableEventBreakpoints,
eventBreakpointForNotification,
eventsRequireNotifications,
firstStatementBreakpointId,
makeEventBreakpointMessage,
} = require("devtools/server/actors/utils/event-breakpoints");
const {
@ -215,6 +217,7 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
this._parent.on("will-navigate", this._onWillNavigate);
this._parent.on("navigate", this._onNavigate);
this._firstStatementBreakpoint = null;
this._debuggerNotificationObserver = new DebuggerNotificationObserver();
if (Services.obs) {
@ -617,18 +620,92 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
setActiveEventBreakpoints: function(ids) {
this._activeEventBreakpoints = new Set(ids);
if (this._activeEventBreakpoints.size === 0) {
this._debuggerNotificationObserver.removeListener(
this._eventBreakpointListener
);
} else {
if (eventsRequireNotifications(ids)) {
this._debuggerNotificationObserver.addListener(
this._eventBreakpointListener
);
} else {
this._debuggerNotificationObserver.removeListener(
this._eventBreakpointListener
);
}
if (this._activeEventBreakpoints.has(firstStatementBreakpointId())) {
this._ensureFirstStatementBreakpointInitialized();
this._firstStatementBreakpoint.hit = frame =>
this._pauseAndRespondEventBreakpoint(
frame,
firstStatementBreakpointId()
);
} else if (this._firstStatementBreakpoint) {
// Disabling the breakpoint disables the feature as much as we need it
// to. We do not bother removing breakpoints from the scripts themselves
// here because the breakpoints will be a no-op if `hit` is `null`, and
// if we wanted to remove them, we'd need a way to iterate through them
// all, which would require us to hold strong references to them, which
// just isn't needed. Plus, if the user disables and then re-enables the
// feature again later, the breakpoints will still be there to work.
this._firstStatementBreakpoint.hit = null;
}
},
_ensureFirstStatementBreakpointInitialized() {
if (this._firstStatementBreakpoint) {
return;
}
this._firstStatementBreakpoint = { hit: null };
for (const script of this.dbg.findScripts()) {
this._maybeTrackFirstStatementBreakpoint(script);
}
},
_maybeTrackFirstStatementBreakpointForNewGlobal(global) {
if (this._firstStatementBreakpoint) {
for (const script of this.dbg.findScripts({ global })) {
this._maybeTrackFirstStatementBreakpoint(script);
}
}
},
_maybeTrackFirstStatementBreakpoint(script) {
if (
// If the feature is not enabled yet, there is nothing to do.
!this._firstStatementBreakpoint ||
// WASM files don't have a first statement.
script.format !== "js" ||
// All "top-level" scripts are non-functions, whether that's because
// the script is a module, a global script, or an eval or what.
script.isFunction
) {
return;
}
const bps = script.getPossibleBreakpoints();
// Scripts aren't guaranteed to have a step start if for instance the
// file contains only function declarations, so in that case we try to
// fall back to whatever we can find.
let meta = bps.find(bp => bp.isStepStart) || bps[0];
if (!meta) {
// We've tried to avoid using `getAllColumnOffsets()` because the set of
// locations included in this list is very under-defined, but for this
// usecase it's not the end of the world. Maybe one day we could have an
// "onEnterFrame" that was scoped to a specific script to avoid this.
meta = script.getAllColumnOffsets()[0];
}
if (!meta) {
// Not certain that this is actually possible, but including for sanity
// so that we don't throw unexpectedly.
return;
}
script.setBreakpoint(meta.offset, this._firstStatementBreakpoint);
},
_onNewDebuggee(global) {
this._maybeTrackFirstStatementBreakpointForNewGlobal(global);
try {
this._debuggerNotificationObserver.connect(global.unsafeDereference());
} catch (e) {}
@ -1949,16 +2026,15 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
},
/**
* A function that the engine calls when a new script has been loaded into the
* scope of the specified debuggee global.
* A function that the engine calls when a new script has been loaded.
*
* @param script Debugger.Script
* The source script that has been loaded into a debuggee compartment.
* @param global Debugger.Object
* A Debugger.Object instance whose referent is the global object.
*/
onNewScript: function(script, global) {
onNewScript: function(script) {
this._addSource(script.source);
this._maybeTrackFirstStatementBreakpoint(script);
},
/**

Просмотреть файл

@ -79,6 +79,13 @@ function animationEvent(operation, name, notificationType) {
};
}
const SCRIPT_FIRST_STATEMENT_BREAKPOINT = {
id: "script.source.firstStatement",
type: "script",
name: "Script First Statement",
message: "Script First Statement",
};
const AVAILABLE_BREAKPOINTS = [
{
name: "Animation",
@ -251,6 +258,10 @@ const AVAILABLE_BREAKPOINTS = [
generalEvent("pointer", "lostpointercapture"),
],
},
{
name: "Script",
items: [SCRIPT_FIRST_STATEMENT_BREAKPOINT],
},
{
name: "Timer",
items: [
@ -365,6 +376,8 @@ for (const eventBP of FLAT_EVENTS) {
}
byEventType[eventType] = eventBP.id;
}
} else if (eventBP.type === "script") {
// Nothing to do.
} else {
throw new Error("Unknown type: " + eventBP.type);
}
@ -430,6 +443,25 @@ function makeEventBreakpointMessage(id) {
return EVENTS_BY_ID[id].message;
}
exports.firstStatementBreakpointId = firstStatementBreakpointId;
function firstStatementBreakpointId() {
return SCRIPT_FIRST_STATEMENT_BREAKPOINT.id;
}
exports.eventsRequireNotifications = eventsRequireNotifications;
function eventsRequireNotifications(ids) {
for (const id of ids) {
const eventBreakpoint = EVENTS_BY_ID[id];
// Script events are implemented directly in the server and do not require
// notifications from Gecko, so there is no need to watch for them.
if (eventBreakpoint && eventBreakpoint.type !== "script") {
return true;
}
}
return false;
}
exports.getAvailableEventBreakpoints = getAvailableEventBreakpoints;
function getAvailableEventBreakpoints() {
const available = [];