Bug 1096739 - Clean up ThreadActor.prototype._setBreakpoint. r=ejpbruel,past

This commit is contained in:
Nick Fitzgerald 2014-11-13 10:07:18 -08:00
parent 884af473bd
commit e94753d0c8
4 changed files with 282 additions and 161 deletions

View File

@ -5,7 +5,9 @@ function secondCall() {
// This comment is useful: ☺
eval("debugger;");
function foo() {}
if (true) {
if (x) {
foo();
}
}
var x = true;

View File

@ -176,6 +176,27 @@ BreakpointStore.prototype = {
}
},
/**
* Move the breakpoint to the new location.
*
* @param Object aBreakpoint
* The breakpoint being moved. See `addBreakpoint` for a description of
* its expected properties.
* @param Object aNewLocation
* The location to move the breakpoint to. Properties:
* - line
* - column (optional; omission implies whole line breakpoint)
*/
moveBreakpoint: function (aBreakpoint, aNewLocation) {
const existingBreakpoint = this.getBreakpoint(aBreakpoint);
this.removeBreakpoint(existingBreakpoint);
const { line, column } = aNewLocation;
existingBreakpoint.line = line;
existingBreakpoint.column = column;
this.addBreakpoint(existingBreakpoint);
},
/**
* Get a breakpoint from the breakpoint store. Will throw an error if the
* breakpoint is not found.
@ -1426,12 +1447,132 @@ ThreadActor.prototype = {
return this._setBreakpoint(aLocation);
},
/**
* Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint
* is being set contains no code, then the breakpoint will slide down to the
* next line that has runnable code. In this case the server breakpoint cache
* will be updated, so callers that iterate over the breakpoint cache should
* take that into account.
* Get or create the BreakpointActor for the breakpoint at the given location.
*
* NB: This will override a pre-existing BreakpointActor's condition with
* the given the location's condition.
*
* @param Object location
* The breakpoint location. See BreakpointStore.prototype.addBreakpoint
* for more information.
* @returns BreakpointActor
*/
_getOrCreateBreakpointActor: function (location) {
let actor;
const storedBp = this.breakpointStore.getBreakpoint(location);
if (storedBp.actor) {
actor = storedBp.actor;
actor.condition = location.condition;
return actor;
}
storedBp.actor = actor = new BreakpointActor(this, {
url: location.url,
line: location.line,
column: location.column,
condition: location.condition
});
this.threadLifetimePool.addActor(actor);
return actor;
},
/**
* Set breakpoints at the offsets closest to our target location's column.
*
* @param Array scripts
* The set of Debugger.Script instances to consider.
* @param Object location
* The target location.
* @param BreakpointActor actor
* The BreakpointActor to handle hitting the breakpoints we set.
* @returns Object
* The RDP response.
*/
_setBreakpointAtColumn: function (scripts, location, actor) {
// Debugger.Script -> array of offset mappings
const scriptsAndOffsetMappings = new Map();
for (let script of scripts) {
this._findClosestOffsetMappings(location, script, scriptsAndOffsetMappings);
}
for (let [script, mappings] of scriptsAndOffsetMappings) {
for (let offsetMapping of mappings) {
script.setBreakpoint(offsetMapping.offset, actor);
}
actor.addScript(script, this);
}
return {
actor: actor.actorID
};
},
/**
* Find the scripts which contain offsets that are an entry point to the given
* line.
*
* @param Array scripts
* The set of Debugger.Scripts to consider.
* @param Number line
* The line we are searching for entry points into.
* @returns Array of objects of the form { script, offsets } where:
* - script is a Debugger.Script
* - offsets is an array of offsets that are entry points into the
* given line.
*/
_findEntryPointsForLine: function (scripts, line) {
const entryPoints = [];
for (let script of scripts) {
const offsets = script.getLineOffsets(line);
if (offsets.length) {
entryPoints.push({ script, offsets });
}
}
return entryPoints;
},
/**
* Find the first line that is associated with bytecode offsets, and is
* greater than or equal to the given start line.
*
* @param Array scripts
* The set of Debugger.Script instances to consider.
* @param Number startLine
* The target line.
* @return Object|null
* If we can't find a line matching our constraints, return
* null. Otherwise, return an object of the form:
* {
* line: Number,
* entryPoints: [
* { script: Debugger.Script, offsets: [offset, ...] },
* ...
* ]
* }
*/
_findNextLineWithOffsets: function (scripts, startLine) {
const maxLine = Math.max(...scripts.map(s => s.startLine + s.lineCount));
for (let line = startLine; line < maxLine; line++) {
const entryPoints = this._findEntryPointsForLine(scripts, line);
if (entryPoints.length) {
return { line, entryPoints };
}
}
return null;
},
/**
* Set a breakpoint using the Debugger API. If the line on which the
* breakpoint is being set contains no code, then the breakpoint will slide
* down to the next line that has runnable code. In this case the server
* breakpoint cache will be updated, so callers that iterate over the
* breakpoint cache should take that into account.
*
* @param object aLocation
* The location of the breakpoint (in the generated source, if source
@ -1441,170 +1582,134 @@ ThreadActor.prototype = {
* nowhere else.
*/
_setBreakpoint: function (aLocation, aOnlyThisScript=null) {
let location = {
const location = {
url: aLocation.url,
line: aLocation.line,
column: aLocation.column,
condition: aLocation.condition
};
let actor;
let storedBp = this.breakpointStore.getBreakpoint(location);
if (storedBp.actor) {
actor = storedBp.actor;
actor.condition = location.condition;
} else {
storedBp.actor = actor = new BreakpointActor(this, {
url: location.url,
line: location.line,
column: location.column,
condition: location.condition
});
this.threadLifetimePool.addActor(actor);
}
// Find all scripts matching the given location
let scripts = this.dbg.findScripts(location);
if (scripts.length == 0) {
// Since we did not find any scripts to set the breakpoint on now, return
// early. When a new script that matches this breakpoint location is
// introduced, the breakpoint actor will already be in the breakpoint store
// and will be set at that time.
return {
actor: actor.actorID
};
}
/**
* For each script, if the given line has at least one entry point, set a
* breakpoint on the bytecode offets for each of them.
*/
// Debugger.Script -> array of offset mappings
let scriptsAndOffsetMappings = new Map();
for (let script of scripts) {
this._findClosestOffsetMappings(location,
script,
scriptsAndOffsetMappings);
}
if (scriptsAndOffsetMappings.size > 0) {
for (let [script, mappings] of scriptsAndOffsetMappings) {
if (aOnlyThisScript && script !== aOnlyThisScript) {
continue;
}
for (let offsetMapping of mappings) {
script.setBreakpoint(offsetMapping.offset, actor);
}
actor.addScript(script, this);
}
return {
actor: actor.actorID
};
}
/**
* If we get here, no breakpoint was set. This is because the given line
* has no entry points, for example because it is empty. As a fallback
* strategy, we try to set the breakpoint on the smallest line greater
* than or equal to the given line that as at least one entry point.
*/
// Find all innermost scripts matching the given location
scripts = this.dbg.findScripts({
url: aLocation.url,
line: aLocation.line,
innermost: true
const actor = location.actor = this._getOrCreateBreakpointActor(location);
const scripts = this.dbg.findScripts({
url: location.url,
// Although we will automatically slide the breakpoint down to the first
// line with code when the requested line doesn't have any, we want to
// restrict the sliding to within functions that contain the requested
// line.
line: location.line
});
/**
* For each innermost script, look for the smallest line greater than or
* equal to the given line that has one or more entry points. If found, set
* a breakpoint on the bytecode offset for each of its entry points.
*/
let actualLocation;
let found = false;
for (let script of scripts) {
let offsets = script.getAllOffsets();
for (let line = location.line; line < offsets.length; ++line) {
if (offsets[line]) {
if (!aOnlyThisScript || script === aOnlyThisScript) {
for (let offset of offsets[line]) {
script.setBreakpoint(offset, actor);
}
actor.addScript(script, this);
}
if (!actualLocation) {
actualLocation = {
url: location.url,
line: line
};
}
found = true;
break;
}
}
if (scripts.length === 0) {
// Since we did not find any scripts to set the breakpoint on now, return
// early. When a new script that matches this breakpoint location is
// introduced, the breakpoint actor will already be in the breakpoint
// store and the breakpoint will be set at that time. This is similar to
// GDB's "pending" breakpoints for shared libraries that aren't loaded
// yet.
return {
actor: actor.actorID
};
}
if (found) {
let existingBp = this.breakpointStore.hasBreakpoint(actualLocation);
if (location.column) {
return this._setBreakpointAtColumn(scripts, location, actor);
}
if (existingBp && existingBp.actor) {
/**
* We already have a breakpoint actor for the actual location, so actor
* we created earlier is now redundant. Delete it, update the breakpoint
* store, and return the actor for the actual location.
*/
// Select the first line that has offsets, and is greater than or equal to
// the requested line. Set breakpoints on each of the offsets that is an
// entry point to our selected line.
const result = this._findNextLineWithOffsets(scripts, location.line);
if (!result) {
return {
error: "noCodeAtLineColumn",
actor: actor.actorID
};
}
const { line, entryPoints } = result;
const actualLocation = line !== location.line
? { url: location.url, line }
: undefined;
if (actualLocation) {
// Check whether we already have a breakpoint actor for the actual
// location. If we do have an existing actor, then the actor we created
// above is redundant and must be destroyed. If we do not have an existing
// actor, we need to update the breakpoint store with the new location.
const existingBreakpoint = this.breakpointStore.hasBreakpoint(actualLocation);
if (existingBreakpoint && existingBreakpoint.actor) {
actor.onDelete();
this.breakpointStore.removeBreakpoint(location);
return {
actor: existingBp.actor.actorID,
actualLocation: actualLocation
actor: existingBreakpoint.actor.actorID,
actualLocation
};
} else {
actor.location = actualLocation;
this.breakpointStore.moveBreakpoint(location, actualLocation);
}
/**
* We don't have a breakpoint actor for the actual location yet. Instead
* or creating a new actor, reuse the actor we created earlier, and update
* the breakpoint store.
*/
actor.location = actualLocation;
this.breakpointStore.addBreakpoint({
actor: actor,
url: actualLocation.url,
line: actualLocation.line,
column: actualLocation.column
});
this.breakpointStore.removeBreakpoint(location);
return {
actor: actor.actorID,
actualLocation: actualLocation
};
}
/**
* If we get here, no line matching the given line was found, so just fail
* epically.
*/
this._setBreakpointOnEntryPoints(
actor,
aOnlyThisScript
? entryPoints.filter(o => o.script === aOnlyThisScript)
: entryPoints
);
return {
error: "noCodeAtLineColumn",
actor: actor.actorID
actor: actor.actorID,
actualLocation
};
},
/**
* Set breakpoints on all the given entry points with the given
* BreakpointActor as the handler.
*
* @param BreakpointActor actor
* The actor handling the breakpoint hits.
* @param Array entryPoints
* An array of objects of the form `{ script, offsets }`.
*/
_setBreakpointOnEntryPoints: function (actor, entryPoints) {
for (let { script, offsets } of entryPoints) {
for (let offset of offsets) {
script.setBreakpoint(offset, actor);
}
actor.addScript(script, this);
}
},
/**
* Find all of the offset mappings associated with `aScript` that are closest
* to `aTargetLocation`. If new offset mappings are found that are closer to
* `aTargetOffset` than the existing offset mappings inside
* `aScriptsAndOffsetMappings`, we empty that map and only consider the
* closest offset mappings. If there is no column in `aTargetLocation`, we add
* all offset mappings that are on the given line.
* closest offset mappings.
*
* In many cases, but not all, this method finds only one closest offset.
* Consider the following case, where multiple offsets will be found:
*
* 0 1 2 3
* 0123456789012345678901234567890
* +-------------------------------
* 1|function f() {
* 2| return g() + h();
* 3|}
*
* The Debugger reports three offsets on line 2 upon which we could set a
* breakpoint: the `return` statement at column 2, the call expression `g()`
* at column 9, and the call expression `h()` at column 15. (Careful readers
* will note that complete source location information isn't saved by
* SpiderMonkey's frontend, and we don't get an offset associated specifically
* with the `+` operation.)
*
* If our target location is line 2 column 12, the offset for the call to `g`
* is 3 columns to the left and the offset for the call to `h` is 3 columns to
* the right. Because they are equally close, we will return both offsets to
* have breakpoints set upon them.
*
* @param Object aTargetLocation
* An object of the form { url, line[, column] }.
@ -1617,21 +1722,6 @@ ThreadActor.prototype = {
_findClosestOffsetMappings: function (aTargetLocation,
aScript,
aScriptsAndOffsetMappings) {
// If we are given a column, we will try and break only at that location,
// otherwise we will break anytime we get on that line.
if (aTargetLocation.column == null) {
let offsetMappings = aScript.getLineOffsets(aTargetLocation.line)
.map(o => ({
line: aTargetLocation.line,
offset: o
}));
if (offsetMappings.length) {
aScriptsAndOffsetMappings.set(aScript, offsetMappings);
}
return;
}
let offsetMappings = aScript.getAllColumnOffsets()
.filter(({ lineNumber }) => lineNumber === aTargetLocation.line);

View File

@ -16,10 +16,10 @@ Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
function run_test()
{
initTestTracerServer();
gDebuggee = addTestGlobal("test-tracer-actor");
gDebuggee = addTestGlobal("test-breakpoints");
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestThread(gClient, "test-tracer-actor", testBreakpoint);
attachTestThread(gClient, "test-breakpoints", testBreakpoint);
});
do_test_pending();
}

View File

@ -16,6 +16,7 @@ function run_test()
test_remove_breakpoint();
test_find_breakpoints();
test_duplicate_breakpoints();
test_move_breakpoint();
}
function test_has_breakpoint() {
@ -180,3 +181,31 @@ function test_duplicate_breakpoints() {
do_check_eq(bpStore.size, 1, "We should have only 1 whole line breakpoint");
bpStore.removeBreakpoint(location);
}
function test_move_breakpoint() {
let bpStore = new BreakpointStore();
let oldLocation = {
url: "http://example.com/foo.js",
line: 10
};
let newLocation = {
url: "http://example.com/foo.js",
line: 12
};
bpStore.addBreakpoint(oldLocation);
bpStore.moveBreakpoint(oldLocation, newLocation);
equal(bpStore.size, 1, "Moving a breakpoint maintains the correct size.");
let bp = bpStore.getBreakpoint(newLocation);
ok(bp, "We should be able to get a breakpoint at the new location.");
equal(bp.line, newLocation.line,
"We should get the moved line.");
equal(bpStore.hasBreakpoint({ url: "http://example.com/foo.js", line: 10 }),
null,
"And we shouldn't be able to get any BP at the old location.");
}