Merge inbound to mozilla-central. a=merge

This commit is contained in:
Gurzau Raul 2019-07-29 12:53:35 +03:00
Родитель f7d0d30b65 8e2cb113e0
Коммит 1eb05019f4
28 изменённых файлов: 2219 добавлений и 823 удалений

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

@ -503,6 +503,10 @@
transition-duration: 100ms;
}
.webreplay-player .message.uncached {
opacity: 0.5;
}
.webreplay-player .recording .message.highlighted {
background-color: var(--recording-marker-background-hover);
}
@ -648,3 +652,10 @@
.webreplay-player #overlay .tick:hover ~ .tick {
opacity: 0.5;
}
.webreplay-player .unscanned {
position: absolute;
height: 100%;
background: #000000;
opacity: 0.1;
}

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

@ -60,6 +60,11 @@ ChromeUtils.defineModuleGetter(
"pointPrecedes",
"resource://devtools/shared/execution-point-utils.js"
);
ChromeUtils.defineModuleGetter(
this,
"pointEquals",
"resource://devtools/shared/execution-point-utils.js"
);
const { UPDATE_REQUEST } = require("devtools/client/netmonitor/src/constants");
@ -189,6 +194,22 @@ function addMessage(newMessage, state, filtersState, prefsState, uiState) {
state.hasExecutionPoints = true;
}
// When replaying, we might get two messages with the same execution point and
// logpoint ID. In this case the first message is provisional and should be
// removed.
const removedIds = [];
if (newMessage.logpointId) {
const existingMessage = [...state.messagesById.values()].find(existing => {
return (
existing.logpointId == newMessage.logpointId &&
pointEquals(existing.executionPoint, newMessage.executionPoint)
);
});
if (existingMessage) {
removedIds.push(existingMessage.id);
}
}
// Check if the current message could be placed in a Warning Group.
// This needs to be done before setting the new message in messagesById so we have a
// proper message.
@ -331,7 +352,7 @@ function addMessage(newMessage, state, filtersState, prefsState, uiState) {
state.networkMessagesUpdateById[newMessage.actor] = newMessage;
}
return state;
return removeMessagesFromState(state, removedIds);
}
/* eslint-enable complexity */

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

@ -107,6 +107,8 @@ class WebReplayPlayer extends Component {
paused: false,
messages: [],
highlightedMessage: null,
unscannedRegions: [],
cachedPoints: [],
start: 0,
end: 1,
};
@ -200,7 +202,12 @@ class WebReplayPlayer extends Component {
}
onProgress(packet) {
const { recording, executionPoint } = packet;
const {
recording,
executionPoint,
unscannedRegions,
cachedPoints,
} = packet;
log(`progress: ${recording ? "rec" : "play"} ${executionPoint.progress}`);
if (this.state.seeking) {
@ -212,7 +219,12 @@ class WebReplayPlayer extends Component {
return;
}
const newState = { recording, executionPoint };
const newState = {
recording,
executionPoint,
unscannedRegions,
cachedPoints,
};
if (recording) {
newState.recordingEndpoint = executionPoint;
}
@ -453,7 +465,12 @@ class WebReplayPlayer extends Component {
}
renderMessage(message, index) {
const { messages, executionPoint, highlightedMessage } = this.state;
const {
messages,
executionPoint,
highlightedMessage,
cachedPoints,
} = this.state;
const offset = this.getVisibleOffset(message.executionPoint);
const previousMessage = messages[index - 1];
@ -477,11 +494,16 @@ class WebReplayPlayer extends Component {
const isHighlighted = highlightedMessage == message.id;
const uncached =
message.executionPoint &&
!cachedPoints.includes(message.executionPoint.progress);
return dom.a({
className: classname("message", {
overlayed: isOverlayed,
future: isFuture,
highlighted: isHighlighted,
uncached,
}),
style: {
left: `${Math.max(offset - markerWidth / 2, 0)}px`,
@ -525,6 +547,37 @@ class WebReplayPlayer extends Component {
});
}
renderUnscannedRegions() {
return this.state.unscannedRegions.map(
this.renderUnscannedRegion.bind(this)
);
}
renderUnscannedRegion({ start, end }) {
let startOffset = this.getVisibleOffset({ progress: start });
let endOffset = this.getVisibleOffset({ progress: end });
if (startOffset > this.overlayWidth || endOffset < 0) {
return null;
}
if (startOffset < 0) {
startOffset = 0;
}
if (endOffset > this.overlayWidth) {
endOffset = this.overlayWidth;
}
return dom.span({
className: classname("unscanned"),
style: {
left: `${startOffset}px`,
width: `${endOffset - startOffset}px`,
},
});
}
render() {
const percent = this.getVisiblePercent(this.state.executionPoint);
const recording = this.isRecording();
@ -560,7 +613,8 @@ class WebReplayPlayer extends Component {
style: { left: `${percent}%`, width: `${100 - percent}%` },
}),
...this.renderMessages(),
...this.renderTicks()
...this.renderTicks(),
...this.renderUnscannedRegions()
)
)
)

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

@ -17,6 +17,7 @@ support-files =
examples/doc_rr_continuous.html
examples/doc_rr_logs.html
examples/doc_rr_error.html
examples/doc_control_flow.html
examples/doc_inspector_basic.html
examples/doc_inspector_styles.html
examples/styles.css
@ -26,6 +27,7 @@ support-files =
[browser_dbg_rr_breakpoints-03.js]
[browser_dbg_rr_breakpoints-04.js]
[browser_dbg_rr_breakpoints-05.js]
[browser_dbg_rr_breakpoints-06.js]
[browser_dbg_rr_record.js]
[browser_dbg_rr_stepping-01.js]
[browser_dbg_rr_stepping-02.js]

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

@ -0,0 +1,49 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable no-undef */
"use strict";
// Test hitting breakpoints when using tricky control flow constructs:
// catch, finally, generators, and async/await.
add_task(async function() {
const dbg = await attachRecordingDebugger("doc_control_flow.html", {
waitForRecording: true,
});
const { threadFront, tab, toolbox } = dbg;
const breakpoints = [];
await rewindToBreakpoint(10);
await resumeToBreakpoint(12);
await resumeToBreakpoint(18);
await resumeToBreakpoint(20);
await resumeToBreakpoint(32);
await resumeToBreakpoint(27);
await resumeToLine(threadFront, 32);
await resumeToLine(threadFront, 27);
await resumeToBreakpoint(42);
await resumeToBreakpoint(44);
await resumeToBreakpoint(50);
await resumeToBreakpoint(54);
for (const bp of breakpoints) {
await threadFront.removeBreakpoint(bp);
}
await toolbox.closeToolbox();
await gBrowser.removeTab(tab);
async function rewindToBreakpoint(line) {
const bp = await setBreakpoint(threadFront, "doc_control_flow.html", line);
breakpoints.push(bp);
await rewindToLine(threadFront, line);
}
async function resumeToBreakpoint(line) {
const bp = await setBreakpoint(threadFront, "doc_control_flow.html", line);
breakpoints.push(bp);
await resumeToLine(threadFront, line);
}
});

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

@ -0,0 +1,104 @@
<html lang="en" dir="ltr">
<body>
<div id="maindiv" style="padding-top:50px">Hello World!</div>
</body>
<script>
function trycatch() {
try {
throwError();
} catch (e) {
updateText("catch");
}
updateText("afterCatch");
startNextCallback();
}
function tryfinally() {
try {
throwError();
} finally {
updateText("finally");
startNextCallback();
}
}
function generator() {
for (const v of inner()) {
updateText(`generated ${v}`);
}
function *inner() {
for (const v of [1,2]) {
updateText(`yield ${v}`);
yield v;
}
}
startNextCallback();
}
async function asyncer() {
await timer();
updateText("after timer 1");
await timer();
updateText("after timer 2");
startNextCallback();
}
async function asyncerThrow() {
await timer();
updateText("after throw timer 1");
try {
await timerThrow();
} catch (e) {
updateText("after throw timer 2");
startNextCallback();
throw e;
}
}
const callbacks = [
trycatch,
tryfinally,
generator,
asyncer,
asyncerThrow,
];
startNextCallback();
// Helpers
function startNextCallback() {
if (callbacks.length) {
const callback = callbacks.shift();
window.setTimeout(() => {
try { callback() } catch (e) {}
});
} else {
const cpmm = SpecialPowers.Services.cpmm;
cpmm.sendAsyncMessage("RecordingFinished");
}
}
function updateText(text) {
document.getElementById("maindiv").innerHTML = text;
}
function throwError() {
throw new Error();
}
function timer() {
return new Promise(resolve => {
setTimeout(resolve);
});
}
function timerThrow() {
return new Promise((resolve, reject) => {
setTimeout(reject);
});
}
</script>
</html>

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

@ -166,3 +166,8 @@ PromiseTestUtils.whitelistRejectionsGlobally(/NS_ERROR_NOT_INITIALIZED/);
PromiseTestUtils.whitelistRejectionsGlobally(
/Current thread has paused or resumed/
);
// When running the full test suite, long delays can occur early on in tests,
// before child processes have even been spawned. Allow a longer timeout to
// avoid failures from this.
requestLongerTimeout(120);

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

@ -46,7 +46,7 @@ function BreakpointActor(threadActor, location) {
BreakpointActor.prototype = {
setOptions(options) {
for (const [script, offsets] of this.scripts) {
this._updateOptionsForScript(script, offsets, options);
this._newOffsetsOrOptions(script, offsets, this.options, options);
}
this.options = options;
@ -71,11 +71,7 @@ BreakpointActor.prototype = {
*/
addScript: function(script, offsets) {
this.scripts.set(script, offsets.concat(this.scripts.get(offsets) || []));
for (const offset of offsets) {
script.setBreakpoint(offset, this);
}
this._updateOptionsForScript(script, offsets, this.options);
this._newOffsetsOrOptions(script, offsets, null, this.options);
},
/**
@ -88,12 +84,20 @@ BreakpointActor.prototype = {
this.scripts.clear();
},
// Update any state affected by changing options on a script this breakpoint
// is associated with.
_updateOptionsForScript(script, offsets, options) {
/**
* Called on changes to this breakpoint's script offsets or options.
*/
_newOffsetsOrOptions(script, offsets, oldOptions, options) {
// When replaying, logging breakpoints are handled using an API to get logged
// messages from throughout the recording.
if (this.threadActor.dbg.replaying && options.logValue) {
if (
oldOptions &&
oldOptions.logValue == options.logValue &&
oldOptions.condition == options.condition
) {
return;
}
for (const offset of offsets) {
const { lineNumber, columnNumber } = script.getOffsetLocation(offset);
script.replayVirtualConsoleLog(
@ -113,6 +117,15 @@ BreakpointActor.prototype = {
}
);
}
return;
}
// In all other cases, this is used as a script breakpoint handler.
// Clear any existing handler first in case this is called multiple times
// after options change.
for (const offset of offsets) {
script.clearBreakpoint(this, offset);
script.setBreakpoint(offset, this);
}
},
@ -202,12 +215,6 @@ BreakpointActor.prototype = {
const reason = { type: "breakpoint", actors: [this.actorID] };
const { condition, logValue } = this.options || {};
// When replaying, breakpoints with log values are handled via
// _updateOptionsForScript.
if (logValue && this.threadActor.dbg.replaying) {
return undefined;
}
if (condition) {
const { result, message } = this.checkCondition(frame, condition);

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -103,8 +103,7 @@ ReplayDebugger.prototype = {
canRewind: RecordReplayControl.canRewind,
replayCurrentExecutionPoint() {
this._ensurePaused();
return this._control.pausePoint();
return this._control.lastPausePoint();
},
replayRecordingEndpoint() {
@ -115,6 +114,14 @@ ReplayDebugger.prototype = {
return this._control.childIsRecording();
},
replayUnscannedRegions() {
return this._control.unscannedRegions();
},
replayCachedPoints() {
return this._control.cachedPoints();
},
addDebuggee() {},
removeAllDebuggees() {},
@ -158,18 +165,6 @@ ReplayDebugger.prototype = {
return this._processResponse(request, response);
},
// Update graphics according to the current state of the child process. This
// should be done anytime we pause and allow the user to interact with the
// debugger.
_repaint() {
const rv = this._sendRequestAllowDiverge({ type: "repaint" }, {});
if ("width" in rv && "height" in rv) {
RecordReplayControl.hadRepaint(rv.width, rv.height);
} else {
RecordReplayControl.hadRepaintFailure();
}
},
getDebuggees() {
return [];
},
@ -310,10 +305,10 @@ ReplayDebugger.prototype = {
}
},
// This hook is called whenever we switch between recording and replaying
// child processes.
_onSwitchChild() {
// The position change handler listens to changes to the current child.
// This hook is called whenever control state changes which affects something
// the position change handler listens to (more than just position changes,
// alas).
_callOnPositionChange() {
if (this.replayingOnPositionChange) {
this.replayingOnPositionChange();
}
@ -329,7 +324,7 @@ ReplayDebugger.prototype = {
this._direction = Direction.NONE;
// Update graphics according to the current state of the child.
this._repaint();
this._control.repaint();
// If breakpoint handlers for the pause haven't been called yet, don't
// call them at all.
@ -362,7 +357,6 @@ ReplayDebugger.prototype = {
_performResume() {
this._ensurePaused();
assert(!this._threadPauseCount);
if (this._resumeCallback && !this._threadPauseCount) {
const callback = this._resumeCallback;
this._invalidateAfterUnpause();
@ -387,7 +381,7 @@ ReplayDebugger.prototype = {
return;
}
const pauseData = this._sendRequestAllowDiverge({ type: "pauseData" });
const pauseData = this._control.getPauseData();
if (!pauseData.frames) {
return;
}
@ -423,6 +417,7 @@ ReplayDebugger.prototype = {
},
_virtualConsoleLog(position, text, condition, callback) {
dumpv(`AddLogpoint ${JSON.stringify(position)} ${text} ${condition}`);
this._control.addLogpoint({ position, text, condition, callback });
},
@ -431,7 +426,7 @@ ReplayDebugger.prototype = {
/////////////////////////////////////////////////////////
_setBreakpoint(handler, position, data) {
dumpv("AddBreakpoint " + JSON.stringify(position));
dumpv(`AddBreakpoint ${JSON.stringify(position)}`);
this._control.addBreakpoint(position);
this._breakpoints.push({ handler, position, data });
},
@ -709,6 +704,7 @@ ReplayDebugger.prototype = {
},
set onEnterFrame(handler) {
this._breakpointKindSetter("EnterFrame", handler, () => {
this._capturePauseData();
handler.call(this, this.getNewestFrame());
});
},
@ -880,9 +876,7 @@ function ReplayDebuggerFrame(dbg, data) {
this._dbg = dbg;
this._data = data;
if (this._data.arguments) {
this._data.arguments = this._data.arguments.map(a =>
this._dbg._convertValue(a)
);
this._arguments = this._data.arguments.map(a => this._dbg._convertValue(a));
}
}
@ -916,7 +910,8 @@ ReplayDebuggerFrame.prototype = {
return this._data.offset;
},
get arguments() {
return this._data.arguments;
assert(this._data);
return this._arguments;
},
get live() {
return true;

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

@ -45,7 +45,7 @@ function getCanvas(window) {
return window.middlemanCanvas;
}
function updateWindowCanvas(window, buffer, width, height, hadFailure) {
function updateWindowCanvas(window, buffer, width, height) {
// Make sure the window has a canvas filling the screen.
const canvas = getCanvas(window);
@ -67,29 +67,39 @@ function updateWindowCanvas(window, buffer, width, height, hadFailure) {
const imageData = cx.getImageData(0, 0, width, height);
imageData.data.set(graphicsData);
cx.putImageData(imageData, 0, 0);
}
// Indicate to the user when repainting failed and we are showing old painted
// graphics instead of the most up-to-date graphics.
if (hadFailure) {
cx.fillStyle = "red";
cx.font = "48px serif";
cx.fillText("PAINT FAILURE", 10, 50);
}
function clearWindowCanvas(window) {
const canvas = getCanvas(window);
const cx = canvas.getContext("2d");
cx.clearRect(0, 0, canvas.width, canvas.height);
}
// Entry point for when we have some new graphics data from the child process
// to draw.
// eslint-disable-next-line no-unused-vars
function UpdateCanvas(buffer, width, height, hadFailure) {
function UpdateCanvas(buffer, width, height) {
try {
// Paint to all windows we can find. Hopefully there is only one.
for (const window of Services.ww.getWindowEnumerator()) {
updateWindowCanvas(window, buffer, width, height, hadFailure);
updateWindowCanvas(window, buffer, width, height);
}
} catch (e) {
dump("Middleman Graphics UpdateCanvas Exception: " + e + "\n");
dump(`Middleman Graphics UpdateCanvas Exception: ${e}\n`);
}
}
// eslint-disable-next-line no-unused-vars
var EXPORTED_SYMBOLS = ["UpdateCanvas"];
function ClearCanvas() {
try {
for (const window of Services.ww.getWindowEnumerator()) {
clearWindowCanvas(window);
}
} catch (e) {
dump(`Middleman Graphics ClearCanvas Exception: ${e}\n`);
}
}
// eslint-disable-next-line no-unused-vars
var EXPORTED_SYMBOLS = ["UpdateCanvas", "ClearCanvas"];

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

@ -35,6 +35,7 @@ const sandbox = Cu.Sandbox(
Cu.evalInSandbox(
"Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
"Components.utils.import('resource://gre/modules/Services.jsm');" +
"Components.utils.import('resource://devtools/shared/execution-point-utils.js');" +
"addDebuggerToGlobal(this);",
sandbox
);
@ -44,15 +45,22 @@ const {
Services,
InspectorUtils,
CSSRule,
pointPrecedes,
pointEquals,
findClosestPoint,
} = sandbox;
const dbg = new Debugger();
const firstGlobal = dbg.makeGlobalObjectReference(sandbox);
const gFirstGlobal = dbg.makeGlobalObjectReference(sandbox);
const gAllGlobals = [];
// We are interested in debugging all globals in the process.
dbg.onNewGlobalObject = function(global) {
try {
dbg.addDebuggee(global);
gAllGlobals.push(global);
scanningOnNewGlobal(global);
} catch (e) {
// Ignore errors related to adding a same-compartment debuggee.
// See bug 1523755.
@ -181,7 +189,8 @@ const gScripts = new IdMap();
const gNewScripts = [];
function addScript(script) {
gScripts.add(script);
const id = gScripts.add(script);
script.setInstrumentationId(id);
script.getChildScripts().forEach(addScript);
}
@ -204,6 +213,11 @@ function considerScript(script) {
return RecordReplayControl.shouldUpdateProgressCounter(script.url);
}
function setEmptyInstrumentationId(script) {
script.setInstrumentationId(0);
script.getChildScripts().foreach(setEmptyInstrumentationId);
}
dbg.onNewScript = function(script) {
if (RecordReplayControl.areThreadEventsDisallowed()) {
// This script is part of an eval on behalf of the debugger.
@ -211,17 +225,13 @@ dbg.onNewScript = function(script) {
}
if (!considerScript(script)) {
setEmptyInstrumentationId(script);
return;
}
addScript(script);
addScriptSource(script.source);
// Each onNewScript call advances the progress counter, to preserve the
// ProgressCounter invariant when onNewScript is called multiple times
// without executing any scripts.
RecordReplayControl.advanceProgressCounter();
if (gManifest.kind == "resume") {
gNewScripts.push(getScriptData(gScripts.getId(script)));
}
@ -372,36 +382,225 @@ function NewTimeWarpTarget() {
// Recording Scanning
///////////////////////////////////////////////////////////////////////////////
const gScannedScripts = new Set();
// The recording is scanned using the Debugger's instrumentation API. We need to
// accumulate the execution points at which every breakpoint site is hit, and
// use instrumentation both to invoke a callback at those breakpoint site hits
// and to efficiently update the frame depth when no generators/async frames
// or exception unwinds occur. In the latter case we fallback on the Debugger
// API to make sure we maintain the correct frame depth.
//
// The Debugger API can also straightforwardly provide this information,
// by setting EnterFrame/OnPop hooks and breakpoints on all appropriate sites
// in content scripts. Unfortunately, this is extremely slow: setting a single
// breakpoint in a script prevents it from being Ion compiled and causes it to
// run several times slower than the normal baseline code. If the page being
// debugged has much JS, scanning it will be extremely slow compared to the
// normal execution speed, and many replaying processes will be needed to keep
// scan data up to date.
function startScanningScript(script) {
const id = gScripts.getId(script);
const offsets = script.getPossibleBreakpointOffsets();
let lastFrame = null,
lastFrameIndex = 0;
for (const offset of offsets) {
const handler = {
hit(frame) {
let frameIndex;
if (frame == lastFrame) {
frameIndex = lastFrameIndex;
} else {
lastFrame = frame;
lastFrameIndex = frameIndex = countScriptFrames() - 1;
}
RecordReplayControl.addScriptHit(id, offset, frameIndex);
},
};
script.setBreakpoint(offset, handler);
function scanningOnNewGlobal(global) {
global.setInstrumentation(
global.makeDebuggeeNativeFunction(
RecordReplayControl.instrumentationCallback
),
["main", "entry", "breakpoint", "exit"]
);
if (RecordReplayControl.isScanningScripts()) {
global.setInstrumentationActive(true);
}
}
// eslint-disable-next-line no-unused-vars
function ScriptResumeFrame(script) {
// At frame resumption points, sync the frame depth. These won't be hit very
// often, and handling them is tricky when e.g. catching exceptions thrown by
// an await, which could be either a resumption or a continuation of an
// existing frame.
RecordReplayControl.setFrameDepth(countScriptFrames() - 1);
RecordReplayControl.onResumeFrame("", script);
}
function startScanningAllScripts() {
if (RecordReplayControl.isScanningScripts()) {
return;
}
RecordReplayControl.setScanningScripts(true);
for (const global of gAllGlobals) {
global.setInstrumentationActive(true);
}
// The onExceptionUnwind hook gets called anytime an error needs to be handled
// for a frame. If there are try/catch or try/finally blocks in the script
// then the hook might be called multiple times, and the frame might finish
// normally. To avoid dealing with this complexity we just add an onPop hook
// to any frame that has had exceptions unwound in it, to make sure the frame
// index is set correctly when it finally unwinds.
dbg.onExceptionUnwind = frame => {
if (considerScript(frame.script)) {
frame.onPop = () => {
const script = gScripts.getId(frame.script);
RecordReplayControl.setFrameDepth(countScriptFrames());
RecordReplayControl.onExitFrame("", script);
};
}
};
}
function stopScanningAllScripts() {
if (!RecordReplayControl.isScanningScripts()) {
return;
}
RecordReplayControl.setScanningScripts(false);
for (const global of gAllGlobals) {
global.setInstrumentationActive(false);
}
dbg.onExceptionUnwind = undefined;
}
///////////////////////////////////////////////////////////////////////////////
// Scanning Queries
///////////////////////////////////////////////////////////////////////////////
function findScriptHits(position, startpoint, endpoint) {
const { kind, script, offset, frameIndex: bpFrameIndex } = position;
const hits = [];
for (let checkpoint = startpoint; checkpoint < endpoint; checkpoint++) {
const allHits = RecordReplayControl.findScriptHits(
checkpoint,
script,
offset
);
for (const { progress, frameIndex } of allHits) {
switch (kind) {
case "OnStep":
if (bpFrameIndex != frameIndex) {
continue;
}
// FALLTHROUGH
case "Break":
hits.push({
checkpoint,
progress,
position: { kind: "OnStep", script, offset, frameIndex },
});
}
}
}
return hits;
}
function findAllScriptHits(script, frameIndex, offsets, startpoint, endpoint) {
const allHits = [];
for (const offset of offsets) {
const position = {
kind: "OnStep",
script,
offset,
frameIndex,
};
const hits = findScriptHits(position, startpoint, endpoint);
allHits.push(...hits);
}
return allHits;
}
function findChangeFrames(checkpoint, which, kind, frameIndex, maybeScript) {
const hits = RecordReplayControl.findChangeFrames(checkpoint, which);
return hits
.filter(
hit =>
hit.frameIndex == frameIndex &&
(!maybeScript || hit.script == maybeScript)
)
.map(({ script, progress }) => ({
checkpoint,
progress,
position: { kind, script, frameIndex },
}));
}
function findFrameSteps({ targetPoint, breakpointOffsets }) {
const {
checkpoint,
position: { script, frameIndex: targetIndex },
} = targetPoint;
// Find the entry point of the frame whose steps contain |targetPoint|.
let entryPoint;
if (targetPoint.position.kind == "EnterFrame") {
entryPoint = targetPoint;
} else {
const entryHits = [
...findChangeFrames(checkpoint, 0, "EnterFrame", targetIndex, script),
...findChangeFrames(checkpoint, 2, "EnterFrame", targetIndex, script),
];
// Find the last frame entry or resume for the frame's script preceding the
// target point. Since frames do not span checkpoints the hit must be in the
// range we are searching.
entryPoint = findClosestPoint(
entryHits,
targetPoint,
/* before */ true,
/* inclusive */ true
);
assert(entryPoint);
}
// Find the exit point of the frame.
const exitHits = findChangeFrames(
checkpoint,
1,
"OnPop",
targetIndex,
script
);
const exitPoint = findClosestPoint(
exitHits,
targetPoint,
/* before */ false,
/* inclusive */ true
);
// The steps in the frame are the hits in the script which have the right
// frame index and happen between the entry and exit points. Any EnterFrame
// points for immediate callees of the frame are also included.
const breakpointHits = findAllScriptHits(
script,
targetIndex,
breakpointOffsets,
checkpoint,
checkpoint + 1
);
const enterFrameHits = findChangeFrames(
checkpoint,
0,
"EnterFrame",
targetIndex + 1
);
const steps = breakpointHits.concat(enterFrameHits).filter(point => {
return pointPrecedes(entryPoint, point) && pointPrecedes(point, exitPoint);
});
steps.push(entryPoint, exitPoint);
steps.sort((pointA, pointB) => {
return pointPrecedes(pointB, pointA);
});
return steps;
}
///////////////////////////////////////////////////////////////////////////////
// Position Handler State
///////////////////////////////////////////////////////////////////////////////
// Position kinds we are expected to hit.
let gPositionHandlerKinds = Object.create(null);
// Whether EnterFrame positions should be hit.
let gHasEnterFrameHandler = false;
// Handlers we tried to install but couldn't due to a script not existing.
// Breakpoints requested by the middleman --- which are preserved when
@ -421,7 +620,7 @@ function clearPositionHandlers() {
dbg.clearAllBreakpoints();
dbg.onEnterFrame = undefined;
gPositionHandlerKinds = Object.create(null);
gHasEnterFrameHandler = false;
gPendingPcHandlers.length = 0;
gInstalledPcHandlers.length = 0;
gOnPopFilters.length = 0;
@ -434,14 +633,6 @@ function installPendingHandlers() {
pending.forEach(ensurePositionHandler);
}
// Hit a position with the specified kind if we are expected to. This is for
// use with position kinds that have no script/offset/frameIndex information.
function hitGlobalHandler(kind, frame) {
if (gPositionHandlerKinds[kind]) {
positionHit({ kind }, frame);
}
}
// The completion state of any frame that is being popped.
let gPopFrameResult = null;
@ -457,7 +648,14 @@ function onPopFrame(completion) {
function onEnterFrame(frame) {
if (considerScript(frame.script)) {
hitGlobalHandler("EnterFrame", frame);
if (gHasEnterFrameHandler) {
ensurePositionHandler({
kind: "OnStep",
script: gScripts.getId(frame.script),
frameIndex: countScriptFrames() - 1,
offset: frame.script.mainOffset,
});
}
gOnPopFilters.forEach(filter => {
if (filter(frame)) {
@ -481,8 +679,6 @@ function addOnPopFilter(filter) {
}
function ensurePositionHandler(position) {
gPositionHandlerKinds[position.kind] = true;
switch (position.kind) {
case "Break":
case "OnStep":
@ -496,6 +692,12 @@ function ensurePositionHandler(position) {
gPendingPcHandlers.push(position);
return;
}
// Make sure the script is delazified and has been instrumented before
// we try to operate on it, so that we can compute the appropriate offsets
// to use. Accessing mainOffset here is a hack but ensures the script is
// not lazy.
debugScript.mainOffset;
}
const match = function({ script, offset }) {
@ -511,6 +713,14 @@ function ensurePositionHandler(position) {
debugScript.setBreakpoint(position.offset, {
hit(frame) {
if (position.offset == debugScript.mainOffset) {
positionHit({
kind: "EnterFrame",
script: position.script,
frameIndex: countScriptFrames() - 1,
});
}
positionHit(
{
kind: "OnStep",
@ -529,6 +739,7 @@ function ensurePositionHandler(position) {
break;
case "EnterFrame":
dbg.onEnterFrame = onEnterFrame;
gHasEnterFrameHandler = true;
break;
}
}
@ -630,7 +841,7 @@ function makeDebuggeeValue(value) {
// Sometimes the global which Cu.getGlobalForObject finds has
// isInvisibleToDebugger set. Wrap the object into the first global we
// found in this case.
return firstGlobal.makeDebuggeeValue(value);
return gFirstGlobal.makeDebuggeeValue(value);
}
}
return value;
@ -688,43 +899,13 @@ const gManifestStartHandlers = {
},
findHits({ position, startpoint, endpoint }) {
const { kind, script, offset, frameIndex: bpFrameIndex } = position;
const hits = [];
const allHits = RecordReplayControl.findScriptHits(script, offset);
for (const { checkpoint, progress, frameIndex } of allHits) {
if (checkpoint >= startpoint && checkpoint < endpoint) {
switch (kind) {
case "OnStep":
if (bpFrameIndex != frameIndex) {
continue;
}
// FALLTHROUGH
case "Break":
hits.push({
checkpoint,
progress,
position: { kind: "OnStep", script, offset, frameIndex },
});
}
}
}
RecordReplayControl.manifestFinished(hits);
RecordReplayControl.manifestFinished(
findScriptHits(position, startpoint, endpoint)
);
},
findFrameSteps({ entryPoint }) {
assert(entryPoint.position.kind == "EnterFrame");
const frameIndex = countScriptFrames() - 1;
const script = getFrameData(frameIndex).script;
const offsets = gScripts.getObject(script).getPossibleBreakpointOffsets();
for (const offset of offsets) {
ensurePositionHandler({ kind: "OnStep", script, offset, frameIndex });
}
ensurePositionHandler({ kind: "EnterFrame" });
ensurePositionHandler({ kind: "OnPop", script, frameIndex });
gFrameSteps = [entryPoint];
gFrameStepsFrameIndex = frameIndex;
RecordReplayControl.resumeExecution();
findFrameSteps(info) {
RecordReplayControl.manifestFinished(findFrameSteps(info));
},
flushRecording() {
@ -752,6 +933,13 @@ const gManifestStartHandlers = {
RecordReplayControl.manifestFinished();
},
getPauseData() {
divergeFromRecording();
const data = getPauseData();
data.paintData = RecordReplayControl.repaint();
RecordReplayControl.manifestFinished(data);
},
hitLogpoint({ text, condition }) {
divergeFromRecording();
@ -766,7 +954,11 @@ const gManifestStartHandlers = {
const rv = frame.eval(text);
const converted = convertCompletionValue(rv, { snapshot: true });
RecordReplayControl.manifestFinished({ result: converted });
const data = getPauseData();
data.paintData = RecordReplayControl.repaint();
RecordReplayControl.manifestFinished({ result: converted, data });
},
};
@ -788,7 +980,7 @@ function ManifestStart(manifest) {
// eslint-disable-next-line no-unused-vars
function BeforeCheckpoint() {
clearPositionHandlers();
gScannedScripts.clear();
stopScanningAllScripts();
}
const FirstCheckpointId = 1;
@ -865,12 +1057,7 @@ const gManifestPrepareAfterCheckpointHandlers = {
},
scanRecording() {
dbg.onEnterFrame = frame => {
if (considerScript(frame.script) && !gScannedScripts.has(frame.script)) {
startScanningScript(frame.script);
gScannedScripts.add(frame.script);
}
};
startScanningAllScripts();
},
};
@ -878,10 +1065,7 @@ function processManifestAfterCheckpoint(point, restoredCheckpoint) {
// After rewinding gManifest won't be correct, so we always mark the current
// manifest as finished and rely on the middleman to give us a new one.
if (restoredCheckpoint) {
RecordReplayControl.manifestFinished({
restoredCheckpoint,
point: currentExecutionPoint(),
});
RecordReplayControl.manifestFinished({ restoredCheckpoint, point });
}
if (!gManifest) {
@ -911,11 +1095,6 @@ function AfterCheckpoint(id, restoredCheckpoint) {
}
}
// In the findFrameSteps manifest, all steps that have been found.
let gFrameSteps = null;
let gFrameStepsFrameIndex = 0;
// Handlers that run after reaching a position watched by ensurePositionHandler.
// This must be specified for any manifest that uses ensurePositionHandler.
const gManifestPositionHandlers = {
@ -929,35 +1108,11 @@ const gManifestPositionHandlers = {
},
runToPoint({ endpoint }, point) {
if (
point.progress == endpoint.progress &&
point.position.frameIndex == endpoint.position.frameIndex
) {
if (pointEquals(point, endpoint)) {
clearPositionHandlers();
RecordReplayControl.manifestFinished({ point });
}
},
findFrameSteps(_, point) {
switch (point.position.kind) {
case "OnStep":
gFrameSteps.push(point);
break;
case "EnterFrame":
if (countScriptFrames() == gFrameStepsFrameIndex + 2) {
gFrameSteps.push(point);
}
break;
case "OnPop":
gFrameSteps.push(point);
clearPositionHandlers();
RecordReplayControl.manifestFinished({
point,
frameSteps: gFrameSteps,
});
break;
}
},
};
function positionHit(position, frame) {
@ -986,7 +1141,6 @@ function getScriptData(id) {
displayName: script.displayName,
url: script.url,
format: script.format,
firstBreakpointOffset: script.getPossibleBreakpointOffsets()[0],
};
}
@ -1023,6 +1177,7 @@ function getFrameData(index) {
}
}
const script = gScripts.getId(frame.script);
return {
index,
type: frame.type,
@ -1031,7 +1186,7 @@ function getFrameData(index) {
generator: frame.generator,
constructing: frame.constructing,
this: convertValue(frame.this),
script: gScripts.getId(frame.script),
script,
offset: frame.offset,
arguments: _arguments,
};
@ -1290,13 +1445,14 @@ function getPauseData() {
}
for (let i = 0; i < numFrames; i++) {
const dbgFrame = scriptFrameForIndex(i);
const frame = getFrameData(i);
const script = gScripts.getObject(frame.script);
rv.frames.push(frame);
rv.offsetMetadata.push({
scriptId: frame.script,
offset: frame.offset,
metadata: script.getOffsetMetadata(frame.offset),
metadata: script.getOffsetMetadata(dbgFrame.offset),
});
addScript(frame.script);
addValue(frame.this, true);
@ -1429,6 +1585,13 @@ const gRequestHandlers = {
getPossibleBreakpoints: forwardToScript("getPossibleBreakpoints"),
getPossibleBreakpointOffsets: forwardToScript("getPossibleBreakpointOffsets"),
frameStepsInfo(request) {
const script = gScripts.getObject(request.script);
return {
breakpointOffsets: script.getPossibleBreakpointOffsets(),
};
},
frameEvaluate(request) {
divergeFromRecording();
const frame = scriptFrameForIndex(request.index);
@ -1540,4 +1703,5 @@ var EXPORTED_SYMBOLS = [
"BeforeCheckpoint",
"AfterCheckpoint",
"NewTimeWarpTarget",
"ScriptResumeFrame",
];

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

@ -7,7 +7,7 @@
[scriptable, uuid(941b2e20-8558-4881-b5ad-dc3a1f2d9678)]
interface rrIGraphics : nsISupports {
void UpdateCanvas(in jsval buffer, in long width, in long height,
in boolean hadFailure);
void UpdateCanvas(in jsval buffer, in long width, in long height);
void ClearCanvas();
};

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

@ -14,4 +14,5 @@ interface rrIReplay : nsISupports {
void BeforeCheckpoint();
void AfterCheckpoint(in long checkpoint, in bool restoredCheckpoint);
long NewTimeWarpTarget();
void ScriptResumeFrame(in long script);
};

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

@ -163,20 +163,7 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
this._dbg.replayingOnForcedPause = this.replayingOnForcedPause.bind(
this
);
const sendProgress = throttle((recording, executionPoint) => {
if (this.attached) {
this.conn.send({
type: "progress",
from: this.actorID,
recording,
executionPoint,
});
}
}, 100);
this._dbg.replayingOnPositionChange = this.replayingOnPositionChange.bind(
this,
sendProgress
);
this._dbg.replayingOnPositionChange = this._makeReplayingOnPositionChange();
}
// Keep the debugger disabled until a client attaches.
this._dbg.enabled = this._state != "detached";
@ -1849,10 +1836,23 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
* changed its position: a checkpoint was reached or a switch between a
* recording and replaying child process occurred.
*/
replayingOnPositionChange: function(sendProgress) {
const recording = this.dbg.replayIsRecording();
const executionPoint = this.dbg.replayCurrentExecutionPoint();
sendProgress(recording, executionPoint);
_makeReplayingOnPositionChange() {
return throttle(() => {
if (this.attached) {
const recording = this.dbg.replayIsRecording();
const executionPoint = this.dbg.replayCurrentExecutionPoint();
const unscannedRegions = this.dbg.replayUnscannedRegions();
const cachedPoints = this.dbg.replayCachedPoints();
this.conn.send({
type: "progress",
from: this.actorID,
recording,
executionPoint,
unscannedRegions,
cachedPoints,
});
}
}, 100);
},
/**

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

@ -17,13 +17,13 @@
// "OnPop": Break when a script's frame with a given frame depth is popped.
// "EnterFrame": Break when any script is entered.
//
// script: For all kinds but "EnterFrame", the ID of the position's script.
// script: The ID of the position's script. This is optional for "EnterFrame".
//
// offset: For "Break" and "OnStep", the offset within the script.
//
// frameIndex: For "OnStep" and "OnPop", the index of the topmost script frame.
// Indexes start at zero for the first frame pushed, and increase with the
// depth of the frame.
// frameIndex: For "OnStep", "OnPop" and optionally "EnterFrame", the index of
// the topmost script frame. Indexes start at zero for the first frame pushed,
// and increase with the depth of the frame.
//
// An execution point is a unique identifier for a point in the recording where
// the debugger can pause, and has the following properties:
@ -73,11 +73,13 @@ function pointPrecedes(pointA, pointB) {
return false;
}
// If an execution point doesn't have a frame index (i.e. EnterFrame) then it
// has bumped the progress counter and predates everything else that is
// associated with the same progress counter.
if ("frameIndex" in posA != "frameIndex" in posB) {
return "frameIndex" in posB;
assert("frameIndex" in posA && "frameIndex" in posB);
assert("script" in posA && "script" in posB);
// If either point is an EnterFrame, it bumped the progress counter and
// happens first.
if (posA.kind == "EnterFrame" || posB.kind == "EnterFrame") {
return posA.kind == "EnterFrame";
}
// Only certain execution point kinds do not bump the progress counter.
@ -87,7 +89,6 @@ function pointPrecedes(pointA, pointB) {
// Deeper frames predate shallower frames, if the progress counter is the
// same. We bump the progress counter when pushing frames, but not when
// popping them.
assert("frameIndex" in posA && "frameIndex" in posB);
if (posA.frameIndex != posB.frameIndex) {
return posA.frameIndex > posB.frameIndex;
}
@ -108,6 +109,43 @@ function pointEquals(pointA, pointB) {
return !pointPrecedes(pointA, pointB) && !pointPrecedes(pointB, pointA);
}
// eslint-disable-next-line no-unused-vars
function pointToString(point) {
if (point.position) {
return `${point.checkpoint}:${point.progress}:${positionToString(
point.position
)}`;
}
return `${point.checkpoint}:${point.progress}`;
}
// eslint-disable-next-line no-unused-vars
function pointArrayIncludes(points, point) {
return points.some(p => pointEquals(point, p));
}
// Find the closest point in an array to point, either before or after it.
// If inclusive is set then the point itself can be returned, if it is in the
// array.
// eslint-disable-next-line no-unused-vars
function findClosestPoint(points, point, before, inclusive) {
let result = null;
for (const p of points) {
if (inclusive && pointEquals(p, point)) {
return p;
}
if (before ? pointPrecedes(p, point) : pointPrecedes(point, p)) {
if (
!result ||
(before ? pointPrecedes(result, p) : pointPrecedes(p, result))
) {
result = p;
}
}
}
return result;
}
// Return whether two breakpoint positions are the same.
function positionEquals(posA, posB) {
return (
@ -134,19 +172,37 @@ function positionSubsumes(posA, posB) {
return true;
}
// EnterFrame positions may or may not specify a script.
if (
posA.kind == "EnterFrame" &&
posB.kind == "EnterFrame" &&
!posA.script &&
posB.script
) {
return true;
}
return false;
}
function positionToString(pos) {
return `${pos.kind}:${pos.script}:${pos.offset}:${pos.frameIndex}`;
}
function assert(v) {
if (!v) {
dump(`Assertion failed: ${Error().stack}\n`);
throw new Error("Assertion failed!");
throw new Error(`Assertion failed! ${Error().stack}`);
}
}
this.EXPORTED_SYMBOLS = [
"pointPrecedes",
"pointEquals",
"pointToString",
"pointArrayIncludes",
"findClosestPoint",
"positionEquals",
"positionSubsumes",
"positionToString",
];

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

@ -42,6 +42,8 @@ const threadSpec = generateActorSpec({
progress: {
recording: Option(0, "json"),
executionPoint: Option(0, "json"),
unscannedRegions: Option(0, "json"),
cachedPoints: Option(0, "json"),
},
},

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

@ -1213,6 +1213,20 @@ bool DebuggerObject::makeDebuggeeValueMethod(JSContext* cx, unsigned argc,
return DebuggerObject::makeDebuggeeValue(cx, object, args[0], args.rval());
}
/* static */
bool DebuggerObject::makeDebuggeeNativeFunctionMethod(JSContext* cx,
unsigned argc,
Value* vp) {
THIS_DEBUGOBJECT(cx, argc, vp, "makeDebuggeeNativeFunction", args, object);
if (!args.requireAtLeast(
cx, "Debugger.Object.prototype.makeDebuggeeNativeFunction", 1)) {
return false;
}
return DebuggerObject::makeDebuggeeNativeFunction(cx, object, args[0],
args.rval());
}
/* static */
bool DebuggerObject::unsafeDereferenceMethod(JSContext* cx, unsigned argc,
Value* vp) {
@ -1405,6 +1419,8 @@ const JSFunctionSpec DebuggerObject::methods_[] = {
JS_FN("executeInGlobalWithBindings",
DebuggerObject::executeInGlobalWithBindingsMethod, 2, 0),
JS_FN("makeDebuggeeValue", DebuggerObject::makeDebuggeeValueMethod, 1, 0),
JS_FN("makeDebuggeeNativeFunction",
DebuggerObject::makeDebuggeeNativeFunctionMethod, 1, 0),
JS_FN("unsafeDereference", DebuggerObject::unsafeDereferenceMethod, 0, 0),
JS_FN("unwrap", DebuggerObject::unwrapMethod, 0, 0),
JS_FN("setInstrumentation", DebuggerObject::setInstrumentationMethod, 2, 0),
@ -2303,6 +2319,56 @@ bool DebuggerObject::makeDebuggeeValue(JSContext* cx,
return true;
}
/* static */
bool DebuggerObject::makeDebuggeeNativeFunction(JSContext* cx,
HandleDebuggerObject object,
HandleValue value,
MutableHandleValue result) {
RootedObject referent(cx, object->referent());
Debugger* dbg = object->owner();
if (!value.isObject()) {
JS_ReportErrorASCII(cx, "Need object");
return false;
}
RootedObject obj(cx, &value.toObject());
if (!obj->is<JSFunction>()) {
JS_ReportErrorASCII(cx, "Need function");
return false;
}
RootedFunction fun(cx, &obj->as<JSFunction>());
if (!fun->isNative() || fun->isExtended()) {
JS_ReportErrorASCII(cx, "Need native function");
return false;
}
RootedValue newValue(cx);
{
Maybe<AutoRealm> ar;
EnterDebuggeeObjectRealm(cx, ar, referent);
unsigned nargs = fun->nargs();
RootedAtom name(cx, fun->displayAtom());
JSFunction* newFun = NewNativeFunction(cx, fun->native(), nargs, name);
if (!newFun) {
return false;
}
newValue.setObject(*newFun);
}
// Back in the debugger's compartment, produce a new Debugger.Object
// instance referring to the wrapped argument.
if (!dbg->wrapDebuggeeValue(cx, &newValue)) {
return false;
}
result.set(newValue);
return true;
}
/* static */
bool DebuggerObject::unsafeDereference(JSContext* cx,
HandleDebuggerObject object,

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

@ -140,6 +140,9 @@ class DebuggerObject : public NativeObject {
HandleDebuggerObject object,
HandleValue value,
MutableHandleValue result);
static MOZ_MUST_USE bool makeDebuggeeNativeFunction(
JSContext* cx, HandleDebuggerObject object, HandleValue value,
MutableHandleValue result);
static MOZ_MUST_USE bool unsafeDereference(JSContext* cx,
HandleDebuggerObject object,
MutableHandleObject result);
@ -302,6 +305,9 @@ class DebuggerObject : public NativeObject {
Value* vp);
static MOZ_MUST_USE bool makeDebuggeeValueMethod(JSContext* cx, unsigned argc,
Value* vp);
static MOZ_MUST_USE bool makeDebuggeeNativeFunctionMethod(JSContext* cx,
unsigned argc,
Value* vp);
static MOZ_MUST_USE bool unsafeDereferenceMethod(JSContext* cx, unsigned argc,
Value* vp);
static MOZ_MUST_USE bool unwrapMethod(JSContext* cx, unsigned argc,

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

@ -435,26 +435,6 @@ of exotic object like an opaque wrapper.
`Object.isExtensible` function, except that the object inspected is
implicit and in a different compartment from the caller.)
<code>copy(<i>value</i>)</code>
: Apply the HTML5 "structured cloning" algorithm to create a copy of
<i>value</i> in the referent's global object (and thus in the referent's
compartment), and return a `Debugger.Object` instance referring to the
copy.
Note that this returns primitive values unchanged. This means you can
use `Debugger.Object.prototype.copy` as a generic "debugger value to
debuggee value" conversion function—within the limitations of the
"structured cloning" algorithm.
<code>create(<i>prototype</i>, [<i>properties</i>])</code>
: Create a new object in the referent's global (and thus in the
referent's compartment), and return a `Debugger.Object` referring to
it. The new object's prototype is <i>prototype</i>, which must be an
`Debugger.Object` instance. The new object's properties are as given by
<i>properties</i>, as if <i>properties</i> were passed to
`Debugger.Object.prototype.defineProperties`, with the new
`Debugger.Object` instance as the `this` value.
<code>makeDebuggeeValue(<i>value</i>)</code>
: Return the debuggee value that represents <i>value</i> in the debuggee.
If <i>value</i> is a primitive, we return it unchanged; if <i>value</i>
@ -473,6 +453,13 @@ of exotic object like an opaque wrapper.
`Debugger.Object` instance that presents <i>o</i> as it would be seen
by code in <i>d</i>'s compartment.
<code>makeDebuggeeNativeFunction(<i>value</i>)</code>
: If <i>value</i> is a native function in the debugger's compartment, create
an equivalent function for the same native in the debuggee's realm, and
return a `Debugger.Object` instance for the new function. The new function
can be accessed by code in the debuggee without going through a cross
compartment wrapper.
<code>decompile([<i>pretty</i>])</code>
: If the referent is a function that is debuggee code, return the
JavaScript source code for a function definition equivalent to the

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

@ -0,0 +1,28 @@
// Debugger.Object.prototype.makeDebuggeeNativeFunction does what it is
// supposed to do.
load(libdir + "asserts.js");
var g = newGlobal({newCompartment: true});
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
// It would be nice if we could test that this call doesn't produce a CCW,
// and that calling makeDebuggeeValue instead does, but
// Debugger.Object.isProxy only returns true for scripted proxies.
const push = gw.makeDebuggeeNativeFunction(Array.prototype.push);
gw.setProperty("push", push);
g.eval("var x = []; push.call(x, 2); x.push(3)");
assertEq(g.x[0], 2);
assertEq(g.x[1], 3);
// Interpreted functions should throw.
assertThrowsInstanceOf(() => {
gw.makeDebuggeeNativeFunction(() => {});
}, Error);
// Native functions which have extended slots should throw.
let f;
new Promise(resolve => { f = resolve; })
assertThrowsInstanceOf(() => gw.makeDebuggeeNativeFunction(f), Error);

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

@ -3580,12 +3580,9 @@ JS::CompileOptions::CompileOptions(JSContext* cx)
bigIntEnabledOption = cx->realm()->creationOptions().getBigIntEnabled();
fieldsEnabledOption = cx->realm()->creationOptions().getFieldsEnabled();
// Certain modes of operation disallow syntax parsing in general. The replay
// debugger requires scripts to be constructed in a consistent order, which
// might not happen with lazy parsing.
// Certain modes of operation disallow syntax parsing in general.
forceFullParse_ = cx->realm()->behaviors().disableLazyParsing() ||
coverage::IsLCovEnabled() ||
mozilla::recordreplay::IsRecordingOrReplaying();
coverage::IsLCovEnabled();
// If instrumentation is enabled in the realm, the compiler should insert the
// requested kinds of instrumentation into all scripts.

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

@ -15,6 +15,7 @@
#include "chrome/common/mach_ipc_mac.h"
#include "ipc/Channel.h"
#include "mac/handler/exception_handler.h"
#include "mozilla/Base64.h"
#include "mozilla/layers/ImageDataSerializer.h"
#include "mozilla/Sprintf.h"
#include "mozilla/VsyncDispatcher.h"
@ -29,6 +30,8 @@
#include "Thread.h"
#include "Units.h"
#include "imgIEncoder.h"
#include <algorithm>
#include <mach/mach_vm.h>
#include <unistd.h>
@ -520,15 +523,13 @@ static bool gDidRepaint;
// Whether we are currently repainting.
static bool gRepainting;
void Repaint(size_t* aWidth, size_t* aHeight) {
bool Repaint(nsAString& aData) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
MOZ_RELEASE_ASSERT(HasDivergedFromRecording());
// Don't try to repaint if the first normal paint hasn't occurred yet.
if (!gCompositorThreadId) {
*aWidth = 0;
*aHeight = 0;
return;
return false;
}
// Ignore the request to repaint if we already triggered a repaint, in which
@ -562,14 +563,34 @@ void Repaint(size_t* aWidth, size_t* aHeight) {
gRepainting = false;
}
if (gDrawTargetBuffer) {
memcpy(gGraphicsShmem, gDrawTargetBuffer, gDrawTargetBufferSize);
*aWidth = gPaintWidth;
*aHeight = gPaintHeight;
} else {
*aWidth = 0;
*aHeight = 0;
if (!gDrawTargetBuffer) {
return false;
}
// Get an image encoder for the media type.
nsCString encoderCID("@mozilla.org/image/encoder;2?type=image/png");
nsCOMPtr<imgIEncoder> encoder = do_CreateInstance(encoderCID.get());
nsString options;
nsresult rv = encoder->InitFromData((const uint8_t*) gDrawTargetBuffer,
gPaintWidth * gPaintHeight * 4,
gPaintWidth,
gPaintHeight,
gPaintWidth * 4,
imgIEncoder::INPUT_FORMAT_HOSTARGB,
options);
if (NS_FAILED(rv)) {
return false;
}
uint64_t count;
rv = encoder->Available(&count);
if (NS_FAILED(rv)) {
return false;
}
rv = Base64EncodeInputStream(encoder, aData, count);
return NS_SUCCEEDED(rv);
}
bool CurrentRepaintCannotFail() {

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

@ -60,6 +60,10 @@ void SendResetMiddlemanCalls();
// unhandled recording divergence per preferences.
bool CurrentRepaintCannotFail();
// Paint according to the current process state, then convert it to an image
// and serialize it in aData.
bool Repaint(nsAString& aData);
} // namespace child
} // namespace recordreplay
} // namespace mozilla

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

@ -69,7 +69,7 @@ void ChildProcessInfo::OnIncomingMessage(const Message& aMsg) {
return;
}
case MessageType::Paint:
UpdateGraphicsInUIProcess(&static_cast<const PaintMessage&>(aMsg));
UpdateGraphicsAfterPaint(static_cast<const PaintMessage&>(aMsg));
break;
case MessageType::ManifestFinished:
mPaused = true;

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

@ -222,26 +222,57 @@ static bool Middleman_SendManifest(JSContext* aCx, unsigned aArgc, Value* aVp) {
static bool Middleman_HadRepaint(JSContext* aCx, unsigned aArgc, Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isNumber() || !args.get(1).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad width/height");
if (!args.get(0).isString()) {
JS_ReportErrorASCII(aCx, "Bad arguments");
return false;
}
size_t width = args.get(0).toNumber();
size_t height = args.get(1).toNumber();
RootedString data(aCx, args.get(0).toString());
PaintMessage message(InvalidCheckpointId, width, height);
parent::UpdateGraphicsInUIProcess(&message);
MOZ_RELEASE_ASSERT(JS_StringHasLatin1Chars(data));
nsCString dataBinary;
bool decodeFailed;
{
JS::AutoAssertNoGC nogc(aCx);
size_t dataLength;
const JS::Latin1Char* dataChars =
JS_GetLatin1StringCharsAndLength(aCx, nogc, data, &dataLength);
if (!dataChars) {
return false;
}
nsDependentCString dataCString((const char*) dataChars, dataLength);
nsresult rv = Base64Decode(dataCString, dataBinary);
decodeFailed = NS_FAILED(rv);
}
if (decodeFailed) {
JS_ReportErrorASCII(aCx, "Base64 decode failed");
return false;
}
parent::UpdateGraphicsAfterRepaint(dataBinary);
args.rval().setUndefined();
return true;
}
static bool Middleman_HadRepaintFailure(JSContext* aCx, unsigned aArgc,
Value* aVp) {
static bool Middleman_RestoreMainGraphics(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
parent::UpdateGraphicsInUIProcess(nullptr);
parent::RestoreMainGraphics();
args.rval().setUndefined();
return true;
}
static bool Middleman_ClearGraphics(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
parent::ClearGraphics();
args.rval().setUndefined();
return true;
@ -284,6 +315,28 @@ static bool Middleman_WaitUntilPaused(JSContext* aCx, unsigned aArgc,
return true;
}
static bool Middleman_Atomize(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isString()) {
JS_ReportErrorASCII(aCx, "Bad parameter");
return false;
}
RootedString str(aCx, args.get(0).toString());
// We shouldn't really be pinning the atom as well, but there isn't a JSAPI
// method for atomizing a JSString without pinning it.
JSString* atom = JS_AtomizeAndPinJSString(aCx, str);
if (!atom) {
return false;
}
args.rval().setString(atom);
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Devtools Sandbox
///////////////////////////////////////////////////////////////////////////////
@ -323,10 +376,11 @@ MOZ_EXPORT bool RecordReplayInterface_ShouldUpdateProgressCounter(
// debugger. The devtools timeline is based on progress values and we don't
// want gaps on the timeline which users can't seek to.
if (gIncludeSystemScripts) {
// Always exclude ReplayScriptURL. Scripts in this file are internal to the
// record/replay infrastructure and run non-deterministically between
// recording and replaying.
return aURL && strcmp(aURL, ReplayScriptURL);
// Always exclude ReplayScriptURL, and any other code that it can invoke.
// Scripts in this file are internal to the record/replay infrastructure and
// run non-deterministically between recording and replaying.
return aURL && strcmp(aURL, ReplayScriptURL) &&
strcmp(aURL, "resource://devtools/shared/execution-point-utils.js");
} else {
return aURL && strncmp(aURL, "resource:", 9) && strncmp(aURL, "chrome:", 7);
}
@ -760,184 +814,6 @@ static bool RecordReplay_SaveCheckpoint(JSContext* aCx, unsigned aArgc,
return true;
}
// How many hits on a script location we will precisely track for a checkpoint.
static const size_t MaxHitsPerCheckpoint = 10;
struct ScriptHitInfo {
// Information about a location where a script offset has been hit, or an
// aggregate set of hits.
struct ScriptHit {
// The most recent checkpoint prior to the hit.
uint32_t mCheckpoint;
// Index of the frame where the hit occurred, or UINT32_MAX if this
// represents an aggregate set of hits after the checkpoint.
uint32_t mFrameIndex;
// Progress counter when the hit occurred, invalid if this represents an
// aggregate set of hits.
ProgressCounter mProgress;
explicit ScriptHit(uint32_t aCheckpoint)
: mCheckpoint(aCheckpoint), mFrameIndex(UINT32_MAX), mProgress(0) {}
ScriptHit(uint32_t aCheckpoint, uint32_t aFrameIndex,
ProgressCounter aProgress)
: mCheckpoint(aCheckpoint),
mFrameIndex(aFrameIndex),
mProgress(aProgress) {}
};
struct ScriptHitChunk {
ScriptHit mHits[7];
ScriptHitChunk* mPrevious;
};
struct ScriptHitKey {
uint32_t mScript;
uint32_t mOffset;
ScriptHitKey(uint32_t aScript, uint32_t aOffset)
: mScript(aScript), mOffset(aOffset) {}
typedef ScriptHitKey Lookup;
static HashNumber hash(const ScriptHitKey& aKey) {
return HashGeneric(aKey.mScript, aKey.mOffset);
}
static bool match(const ScriptHitKey& aFirst, const ScriptHitKey& aSecond) {
return aFirst.mScript == aSecond.mScript &&
aFirst.mOffset == aSecond.mOffset;
}
};
typedef HashMap<ScriptHitKey, ScriptHitChunk*, ScriptHitKey,
AllocPolicy<MemoryKind::ScriptHits>>
ScriptHitMap;
ScriptHitMap mTable;
ScriptHitChunk* mFreeChunk;
ScriptHitInfo() : mFreeChunk(nullptr) {}
ScriptHitChunk* FindHits(uint32_t aScript, uint32_t aOffset) {
ScriptHitKey key(aScript, aOffset);
ScriptHitMap::Ptr p = mTable.lookup(key);
return p ? p->value() : nullptr;
}
void AddHit(uint32_t aScript, uint32_t aOffset, uint32_t aCheckpoint,
uint32_t aFrameIndex, ProgressCounter aProgress) {
ScriptHitKey key(aScript, aOffset);
ScriptHitMap::AddPtr p = mTable.lookupForAdd(key);
if (!p && !mTable.add(p, key, NewChunk(nullptr))) {
MOZ_CRASH("ScriptHitInfo::AddScriptHit");
}
ScriptHitChunk* chunk = p->value();
p->value() = AddHit(chunk, ScriptHit(aCheckpoint, aFrameIndex, aProgress));
}
ScriptHitChunk* AddHit(ScriptHitChunk* aChunk, const ScriptHit& aHit) {
for (int i = ArrayLength(aChunk->mHits) - 1; i >= 0; i--) {
if (!aChunk->mHits[i].mCheckpoint) {
aChunk->mHits[i] = aHit;
return aChunk;
}
}
ScriptHitChunk* newChunk = NewChunk(aChunk);
newChunk->mHits[ArrayLength(newChunk->mHits) - 1] = aHit;
return newChunk;
}
ScriptHitChunk* NewChunk(ScriptHitChunk* aPrevious) {
if (!mFreeChunk) {
void* mem = AllocateMemory(PageSize, MemoryKind::ScriptHits);
ScriptHitChunk* chunks = reinterpret_cast<ScriptHitChunk*>(mem);
size_t numChunks = PageSize / sizeof(ScriptHitChunk);
for (size_t i = 0; i < numChunks - 1; i++) {
chunks[i].mPrevious = &chunks[i + 1];
}
mFreeChunk = chunks;
}
ScriptHitChunk* result = mFreeChunk;
mFreeChunk = mFreeChunk->mPrevious;
result->mPrevious = aPrevious;
return result;
}
};
static ScriptHitInfo* gScriptHits;
static void InitializeScriptHits() {
void* mem = AllocateMemory(sizeof(ScriptHitInfo), MemoryKind::ScriptHits);
gScriptHits = new (mem) ScriptHitInfo();
}
static bool RecordReplay_AddScriptHit(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isNumber() || !args.get(1).isNumber() ||
!args.get(2).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t script = args.get(0).toNumber();
uint32_t offset = args.get(1).toNumber();
uint32_t frameIndex = args.get(2).toNumber();
gScriptHits->AddHit(script, offset, GetLastCheckpoint(), frameIndex,
gProgressCounter);
args.rval().setUndefined();
return true;
}
static bool RecordReplay_FindScriptHits(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isNumber() || !args.get(1).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t script = args.get(0).toNumber();
uint32_t offset = args.get(1).toNumber();
RootedValueVector values(aCx);
ScriptHitInfo::ScriptHitChunk* chunk =
gScriptHits ? gScriptHits->FindHits(script, offset) : nullptr;
while (chunk) {
for (const auto& hit : chunk->mHits) {
if (hit.mCheckpoint) {
RootedObject hitObject(aCx, JS_NewObject(aCx, nullptr));
if (!hitObject ||
!JS_DefineProperty(aCx, hitObject, "checkpoint", hit.mCheckpoint,
JSPROP_ENUMERATE) ||
!JS_DefineProperty(aCx, hitObject, "progress",
(double)hit.mProgress, JSPROP_ENUMERATE) ||
!JS_DefineProperty(aCx, hitObject, "frameIndex", hit.mFrameIndex,
JSPROP_ENUMERATE) ||
!values.append(ObjectValue(*hitObject))) {
return false;
}
}
}
chunk = chunk->mPrevious;
}
JSObject* array = JS_NewArrayObject(aCx, values);
if (!array) {
return false;
}
args.rval().setObject(*array);
return true;
}
static bool RecordReplay_GetContent(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
@ -963,23 +839,22 @@ static bool RecordReplay_GetContent(JSContext* aCx, unsigned aArgc,
static bool RecordReplay_Repaint(JSContext* aCx, unsigned aArgc, Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
size_t width, height;
child::Repaint(&width, &height);
nsString data;
if (!child::Repaint(data)) {
args.rval().setNull();
return true;
}
RootedObject obj(aCx, JS_NewObject(aCx, nullptr));
if (!obj ||
!JS_DefineProperty(aCx, obj, "width", (double)width, JSPROP_ENUMERATE) ||
!JS_DefineProperty(aCx, obj, "height", (double)height,
JSPROP_ENUMERATE)) {
JSString* str = JS_NewUCStringCopyN(aCx, data.BeginReading(), data.Length());
if (!str) {
return false;
}
args.rval().setObject(*obj);
args.rval().setString(str);
return true;
}
static bool RecordReplay_MemoryUsage(JSContext* aCx, unsigned aArgc,
Value* aVp) {
static bool RecordReplay_MemoryUsage(JSContext* aCx, unsigned aArgc, Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isNumber()) {
@ -1011,13 +886,430 @@ static bool RecordReplay_Dump(JSContext* aCx, unsigned aArgc, Value* aVp) {
if (!cstr) {
return false;
}
Print("%s", cstr.get());
DirectPrint(cstr.get());
}
args.rval().setUndefined();
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Recording/Replaying Script Hit Methods
///////////////////////////////////////////////////////////////////////////////
enum ChangeFrameKind {
ChangeFrameEnter,
ChangeFrameExit,
ChangeFrameResume,
NumChangeFrameKinds
};
struct ScriptHitInfo {
typedef AllocPolicy<MemoryKind::ScriptHits> AllocPolicy;
// Information about a location where a script offset has been hit.
struct ScriptHit {
uint32_t mFrameIndex : 16;
ProgressCounter mProgress : 48;
ScriptHit(uint32_t aFrameIndex, ProgressCounter aProgress)
: mFrameIndex(aFrameIndex), mProgress(aProgress) {
MOZ_RELEASE_ASSERT(aFrameIndex < 1 << 16);
MOZ_RELEASE_ASSERT(aProgress < uint64_t(1) << 48);
}
};
static_assert(sizeof(ScriptHit) == 8, "Unexpected size");
struct ScriptHitChunk {
ScriptHit mHits[7];
ScriptHitChunk* mPrevious;
};
ScriptHitChunk* mFreeChunk;
struct ScriptHitKey {
uint32_t mScript;
uint32_t mOffset;
ScriptHitKey(uint32_t aScript, uint32_t aOffset)
: mScript(aScript), mOffset(aOffset) {}
typedef ScriptHitKey Lookup;
static HashNumber hash(const ScriptHitKey& aKey) {
return HashGeneric(aKey.mScript, aKey.mOffset);
}
static bool match(const ScriptHitKey& aFirst, const ScriptHitKey& aSecond) {
return aFirst.mScript == aSecond.mScript
&& aFirst.mOffset == aSecond.mOffset;
}
};
typedef HashMap<ScriptHitKey, ScriptHitChunk*, ScriptHitKey, AllocPolicy>
ScriptHitMap;
struct AnyScriptHit {
uint32_t mScript;
uint32_t mFrameIndex;
ProgressCounter mProgress;
AnyScriptHit(uint32_t aScript, uint32_t aFrameIndex,
ProgressCounter aProgress)
: mScript(aScript), mFrameIndex(aFrameIndex), mProgress(aProgress) {}
};
typedef InfallibleVector<AnyScriptHit, 128, AllocPolicy> AnyScriptHitVector;
struct CheckpointInfo {
ScriptHitMap mTable;
AnyScriptHitVector mChangeFrames[NumChangeFrameKinds];
};
InfallibleVector<CheckpointInfo*, 1024, AllocPolicy> mInfo;
ScriptHitInfo() : mFreeChunk(nullptr) {}
CheckpointInfo* GetInfo(uint32_t aCheckpoint) {
while (aCheckpoint >= mInfo.length()) {
mInfo.append(nullptr);
}
if (!mInfo[aCheckpoint]) {
void* mem = AllocateMemory(sizeof(CheckpointInfo), MemoryKind::ScriptHits);
mInfo[aCheckpoint] = new(mem) CheckpointInfo();
}
return mInfo[aCheckpoint];
}
ScriptHitChunk* FindHits(uint32_t aCheckpoint, uint32_t aScript, uint32_t aOffset) {
CheckpointInfo* info = GetInfo(aCheckpoint);
ScriptHitKey key(aScript, aOffset);
ScriptHitMap::Ptr p = info->mTable.lookup(key);
return p ? p->value() : nullptr;
}
void AddHit(uint32_t aCheckpoint, uint32_t aScript, uint32_t aOffset,
uint32_t aFrameIndex, ProgressCounter aProgress) {
CheckpointInfo* info = GetInfo(aCheckpoint);
ScriptHitKey key(aScript, aOffset);
ScriptHitMap::AddPtr p = info->mTable.lookupForAdd(key);
if (!p && !info->mTable.add(p, key, NewChunk(nullptr))) {
MOZ_CRASH("ScriptHitInfo::AddHit");
}
ScriptHitChunk* chunk = p->value();
p->value() = AddHit(chunk, ScriptHit(aFrameIndex, aProgress));
}
ScriptHitChunk* AddHit(ScriptHitChunk* aChunk, const ScriptHit& aHit) {
for (int i = ArrayLength(aChunk->mHits) - 1; i >= 0; i--) {
if (!aChunk->mHits[i].mProgress) {
aChunk->mHits[i] = aHit;
return aChunk;
}
}
ScriptHitChunk* newChunk = NewChunk(aChunk);
newChunk->mHits[ArrayLength(newChunk->mHits) - 1] = aHit;
return newChunk;
}
ScriptHitChunk* NewChunk(ScriptHitChunk* aPrevious) {
if (!mFreeChunk) {
void* mem = AllocateMemory(PageSize, MemoryKind::ScriptHits);
ScriptHitChunk* chunks = reinterpret_cast<ScriptHitChunk*>(mem);
size_t numChunks = PageSize / sizeof(ScriptHitChunk);
for (size_t i = 0; i < numChunks - 1; i++) {
chunks[i].mPrevious = &chunks[i + 1];
}
mFreeChunk = chunks;
}
ScriptHitChunk* result = mFreeChunk;
mFreeChunk = mFreeChunk->mPrevious;
result->mPrevious = aPrevious;
return result;
}
void AddChangeFrame(uint32_t aCheckpoint, uint32_t aWhich,
uint32_t aScript, uint32_t aFrameIndex,
ProgressCounter aProgress) {
CheckpointInfo* info = GetInfo(aCheckpoint);
MOZ_RELEASE_ASSERT(aWhich < NumChangeFrameKinds);
info->mChangeFrames[aWhich].emplaceBack(aScript, aFrameIndex, aProgress);
}
AnyScriptHitVector* FindChangeFrames(uint32_t aCheckpoint, uint32_t aWhich) {
CheckpointInfo* info = GetInfo(aCheckpoint);
MOZ_RELEASE_ASSERT(aWhich < NumChangeFrameKinds);
return &info->mChangeFrames[aWhich];
}
};
static ScriptHitInfo* gScriptHits;
// Interned atoms for the various instrumented operations.
static JSString* gMainAtom;
static JSString* gEntryAtom;
static JSString* gBreakpointAtom;
static JSString* gExitAtom;
static void InitializeScriptHits() {
void* mem = AllocateMemory(sizeof(ScriptHitInfo), MemoryKind::ScriptHits);
gScriptHits = new (mem) ScriptHitInfo();
AutoSafeJSContext cx;
JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
gMainAtom = JS_AtomizeAndPinString(cx, "main");
gEntryAtom = JS_AtomizeAndPinString(cx, "entry");
gBreakpointAtom = JS_AtomizeAndPinString(cx, "breakpoint");
gExitAtom = JS_AtomizeAndPinString(cx, "exit");
MOZ_RELEASE_ASSERT(gMainAtom && gEntryAtom && gBreakpointAtom && gExitAtom);
}
static bool gScanningScripts;
static uint32_t gFrameDepth;
static bool RecordReplay_IsScanningScripts(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
args.rval().setBoolean(gScanningScripts);
return true;
}
static bool RecordReplay_SetScanningScripts(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
MOZ_RELEASE_ASSERT(gFrameDepth == 0);
gScanningScripts = ToBoolean(args.get(0));
args.rval().setUndefined();
return true;
}
static bool RecordReplay_GetFrameDepth(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
args.rval().setNumber(gFrameDepth);
return true;
}
static bool RecordReplay_SetFrameDepth(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
MOZ_RELEASE_ASSERT(gScanningScripts);
if (!args.get(0).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameter");
return false;
}
gFrameDepth = args.get(0).toNumber();
args.rval().setUndefined();
return true;
}
static bool RecordReplay_OnScriptHit(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
MOZ_RELEASE_ASSERT(gScanningScripts);
if (!args.get(1).isNumber() || !args.get(2).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t script = args.get(1).toNumber();
uint32_t offset = args.get(2).toNumber();
uint32_t frameIndex = gFrameDepth - 1;
if (!script) {
// This script is not being tracked and doesn't update the frame depth.
args.rval().setUndefined();
return true;
}
gScriptHits->AddHit(GetLastCheckpoint(), script, offset,
frameIndex, gProgressCounter);
args.rval().setUndefined();
return true;
}
template <ChangeFrameKind Kind>
static bool RecordReplay_OnChangeFrame(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
MOZ_RELEASE_ASSERT(gScanningScripts);
if (!args.get(1).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t script = args.get(1).toNumber();
if (!script) {
// This script is not being tracked and doesn't update the frame depth.
args.rval().setUndefined();
return true;
}
if (Kind == ChangeFrameEnter || Kind == ChangeFrameResume) {
gFrameDepth++;
}
uint32_t frameIndex = gFrameDepth - 1;
gScriptHits->AddChangeFrame(GetLastCheckpoint(), Kind,
script, frameIndex, gProgressCounter);
if (Kind == ChangeFrameExit) {
gFrameDepth--;
}
args.rval().setUndefined();
return true;
}
static bool RecordReplay_InstrumentationCallback(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isString()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
// The kind string should be an atom which we have captured already.
JSString* kind = args.get(0).toString();
if (kind == gBreakpointAtom) {
return RecordReplay_OnScriptHit(aCx, aArgc, aVp);
}
if (kind == gMainAtom) {
return RecordReplay_OnChangeFrame<ChangeFrameEnter>(aCx, aArgc, aVp);
}
if (kind == gExitAtom) {
return RecordReplay_OnChangeFrame<ChangeFrameExit>(aCx, aArgc, aVp);
}
if (kind == gEntryAtom) {
if (!args.get(1).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t script = args.get(1).toNumber();
if (NS_FAILED(gReplay->ScriptResumeFrame(script))) {
MOZ_CRASH("RecordReplay_InstrumentationCallback");
}
args.rval().setUndefined();
return true;
}
JS_ReportErrorASCII(aCx, "Unexpected kind");
return false;
}
static bool RecordReplay_FindScriptHits(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isNumber() ||
!args.get(1).isNumber() ||
!args.get(2).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t checkpoint = args.get(0).toNumber();
uint32_t script = args.get(1).toNumber();
uint32_t offset = args.get(2).toNumber();
RootedValueVector values(aCx);
ScriptHitInfo::ScriptHitChunk* chunk =
gScriptHits ? gScriptHits->FindHits(checkpoint, script, offset) : nullptr;
while (chunk) {
for (const auto& hit : chunk->mHits) {
if (hit.mProgress) {
RootedObject hitObject(aCx, JS_NewObject(aCx, nullptr));
if (!hitObject ||
!JS_DefineProperty(aCx, hitObject, "progress",
(double) hit.mProgress, JSPROP_ENUMERATE) ||
!JS_DefineProperty(aCx, hitObject, "frameIndex",
hit.mFrameIndex, JSPROP_ENUMERATE) ||
!values.append(ObjectValue(*hitObject))) {
return false;
}
}
}
chunk = chunk->mPrevious;
}
JSObject* array = JS_NewArrayObject(aCx, values);
if (!array) {
return false;
}
args.rval().setObject(*array);
return true;
}
static bool RecordReplay_FindChangeFrames(JSContext* aCx, unsigned aArgc,
Value* aVp) {
CallArgs args = CallArgsFromVp(aArgc, aVp);
if (!args.get(0).isNumber() || !args.get(1).isNumber()) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
uint32_t checkpoint = args.get(0).toNumber();
uint32_t which = args.get(1).toNumber();
if (which >= NumChangeFrameKinds) {
JS_ReportErrorASCII(aCx, "Bad parameters");
return false;
}
RootedValueVector values(aCx);
ScriptHitInfo::AnyScriptHitVector* hits =
gScriptHits ? gScriptHits->FindChangeFrames(checkpoint, which) : nullptr;
if (hits) {
for (const ScriptHitInfo::AnyScriptHit& hit : *hits) {
RootedObject hitObject(aCx, JS_NewObject(aCx, nullptr));
if (!hitObject ||
!JS_DefineProperty(aCx, hitObject, "script",
hit.mScript, JSPROP_ENUMERATE) ||
!JS_DefineProperty(aCx, hitObject, "progress",
(double) hit.mProgress, JSPROP_ENUMERATE) ||
!JS_DefineProperty(aCx, hitObject, "frameIndex",
hit.mFrameIndex, JSPROP_ENUMERATE) ||
!values.append(ObjectValue(*hitObject))) {
return false;
}
}
}
JSObject* array = JS_NewArrayObject(aCx, values);
if (!array) {
return false;
}
args.rval().setObject(*array);
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Plumbing
///////////////////////////////////////////////////////////////////////////////
@ -1027,10 +1319,12 @@ static const JSFunctionSpec gMiddlemanMethods[] = {
JS_FN("canRewind", Middleman_CanRewind, 0, 0),
JS_FN("spawnReplayingChild", Middleman_SpawnReplayingChild, 0, 0),
JS_FN("sendManifest", Middleman_SendManifest, 2, 0),
JS_FN("hadRepaint", Middleman_HadRepaint, 2, 0),
JS_FN("hadRepaintFailure", Middleman_HadRepaintFailure, 0, 0),
JS_FN("hadRepaint", Middleman_HadRepaint, 1, 0),
JS_FN("restoreMainGraphics", Middleman_RestoreMainGraphics, 0, 0),
JS_FN("clearGraphics", Middleman_ClearGraphics, 0, 0),
JS_FN("inRepaintStressMode", Middleman_InRepaintStressMode, 0, 0),
JS_FN("waitUntilPaused", Middleman_WaitUntilPaused, 1, 0),
JS_FN("atomize", Middleman_Atomize, 1, 0),
JS_FS_END};
static const JSFunctionSpec gRecordReplayMethods[] = {
@ -1049,11 +1343,21 @@ static const JSFunctionSpec gRecordReplayMethods[] = {
JS_FN("flushRecording", RecordReplay_FlushRecording, 0, 0),
JS_FN("setMainChild", RecordReplay_SetMainChild, 0, 0),
JS_FN("saveCheckpoint", RecordReplay_SaveCheckpoint, 1, 0),
JS_FN("addScriptHit", RecordReplay_AddScriptHit, 3, 0),
JS_FN("findScriptHits", RecordReplay_FindScriptHits, 2, 0),
JS_FN("getContent", RecordReplay_GetContent, 1, 0),
JS_FN("repaint", RecordReplay_Repaint, 0, 0),
JS_FN("memoryUsage", RecordReplay_MemoryUsage, 0, 0),
JS_FN("isScanningScripts", RecordReplay_IsScanningScripts, 0, 0),
JS_FN("setScanningScripts", RecordReplay_SetScanningScripts, 1, 0),
JS_FN("getFrameDepth", RecordReplay_GetFrameDepth, 0, 0),
JS_FN("setFrameDepth", RecordReplay_SetFrameDepth, 1, 0),
JS_FN("onScriptHit", RecordReplay_OnScriptHit, 3, 0),
JS_FN("onEnterFrame", RecordReplay_OnChangeFrame<ChangeFrameEnter>, 2, 0),
JS_FN("onExitFrame", RecordReplay_OnChangeFrame<ChangeFrameExit>, 2, 0),
JS_FN("onResumeFrame", RecordReplay_OnChangeFrame<ChangeFrameResume>, 2, 0),
JS_FN("instrumentationCallback", RecordReplay_InstrumentationCallback, 3,
0),
JS_FN("findScriptHits", RecordReplay_FindScriptHits, 3, 0),
JS_FN("findChangeFrames", RecordReplay_FindChangeFrames, 2, 0),
JS_FN("dump", RecordReplay_Dump, 1, 0),
JS_FS_END};

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

@ -23,7 +23,9 @@
#include "mozilla/layers/LayerTransactionChild.h"
#include "mozilla/layers/PTextureChild.h"
#include "nsImportModule.h"
#include "nsStringStream.h"
#include "rrIGraphics.h"
#include "ImageOps.h"
#include <mach/mach_vm.h>
@ -91,47 +93,12 @@ static void InitGraphicsSandbox() {
// Buffer used to transform graphics memory, if necessary.
static void* gBufferMemory;
// The dimensions of the data in the graphics shmem buffer.
static size_t gLastPaintWidth, gLastPaintHeight;
// Explicit Paint messages received from the child need to be handled with
// care to make sure we show correct graphics. Each Paint message is for the
// the process state at the most recent checkpoint in the past. When running
// (forwards or backwards) between the checkpoint and the Paint message,
// we could pause at a breakpoint and repaint the graphics at that point,
// reflecting the process state at a point later than at the checkpoint.
// In this case the Paint message's graphics will be stale. To avoid showing
// its graphics, we wait until both the Paint and the checkpoint itself have
// been hit, with no intervening repaint.
void UpdateGraphicsInUIProcess(const PaintMessage* aMsg) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
if (aMsg) {
gLastPaintWidth = aMsg->mWidth;
gLastPaintHeight = aMsg->mHeight;
}
if (!gLastPaintWidth || !gLastPaintHeight) {
return;
}
bool hadFailure = !aMsg;
// Make sure there is a sandbox which is running the graphics JS module.
if (!gGraphics) {
InitGraphicsSandbox();
}
size_t width = gLastPaintWidth;
size_t height = gLastPaintHeight;
size_t stride =
layers::ImageDataSerializer::ComputeRGBStride(gSurfaceFormat, width);
static void UpdateMiddlemanCanvas(size_t aWidth, size_t aHeight, size_t aStride,
void* aData) {
// Make sure the width and height are appropriately sized.
CheckedInt<size_t> scaledWidth = CheckedInt<size_t>(width) * 4;
CheckedInt<size_t> scaledHeight = CheckedInt<size_t>(height) * stride;
MOZ_RELEASE_ASSERT(scaledWidth.isValid() && scaledWidth.value() <= stride);
CheckedInt<size_t> scaledWidth = CheckedInt<size_t>(aWidth) * 4;
CheckedInt<size_t> scaledHeight = CheckedInt<size_t>(aHeight) * aStride;
MOZ_RELEASE_ASSERT(scaledWidth.isValid() && scaledWidth.value() <= aStride);
MOZ_RELEASE_ASSERT(scaledHeight.isValid() &&
scaledHeight.value() <= GraphicsMemorySize);
@ -139,17 +106,17 @@ void UpdateGraphicsInUIProcess(const PaintMessage* aMsg) {
// Use the shared memory buffer directly, unless we need to transform the
// data due to extra memory in each row of the data which the child process
// sent us.
MOZ_RELEASE_ASSERT(gGraphicsMemory);
void* memory = gGraphicsMemory;
if (stride != width * 4) {
MOZ_RELEASE_ASSERT(aData);
void* memory = aData;
if (aStride != aWidth * 4) {
if (!gBufferMemory) {
gBufferMemory = malloc(GraphicsMemorySize);
}
memory = gBufferMemory;
for (size_t y = 0; y < height; y++) {
char* src = (char*)gGraphicsMemory + y * stride;
char* dst = (char*)gBufferMemory + y * width * 4;
memcpy(dst, src, width * 4);
for (size_t y = 0; y < aHeight; y++) {
char* src = (char*)aData + y * aStride;
char* dst = (char*)gBufferMemory + y * aWidth * 4;
memcpy(dst, src, aWidth * 4);
}
}
@ -159,14 +126,14 @@ void UpdateGraphicsInUIProcess(const PaintMessage* aMsg) {
// Create an ArrayBuffer whose contents are the externally-provided |memory|.
JS::Rooted<JSObject*> bufferObject(cx);
bufferObject =
JS::NewArrayBufferWithUserOwnedContents(cx, width * height * 4, memory);
JS::NewArrayBufferWithUserOwnedContents(cx, aWidth * aHeight * 4, memory);
MOZ_RELEASE_ASSERT(bufferObject);
JS::Rooted<JS::Value> buffer(cx, JS::ObjectValue(*bufferObject));
// Call into the graphics module to update the canvas it manages.
if (NS_FAILED(gGraphics->UpdateCanvas(buffer, width, height, hadFailure))) {
MOZ_CRASH("UpdateGraphicsInUIProcess");
if (NS_FAILED(gGraphics->UpdateCanvas(buffer, aWidth, aHeight))) {
MOZ_CRASH("UpdateMiddlemanCanvas");
}
// Manually detach this ArrayBuffer once this update completes, as the
@ -175,6 +142,75 @@ void UpdateGraphicsInUIProcess(const PaintMessage* aMsg) {
MOZ_ALWAYS_TRUE(JS::DetachArrayBuffer(cx, bufferObject));
}
// The dimensions of the data in the graphics shmem buffer.
static size_t gLastPaintWidth, gLastPaintHeight;
void UpdateGraphicsAfterPaint(const PaintMessage& aMsg) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
gLastPaintWidth = aMsg.mWidth;
gLastPaintHeight = aMsg.mHeight;
if (!aMsg.mWidth || !aMsg.mHeight) {
return;
}
// Make sure there is a sandbox which is running the graphics JS module.
if (!gGraphics) {
InitGraphicsSandbox();
}
size_t stride =
layers::ImageDataSerializer::ComputeRGBStride(gSurfaceFormat,
aMsg.mWidth);
UpdateMiddlemanCanvas(aMsg.mWidth, aMsg.mHeight, stride, gGraphicsMemory);
}
void UpdateGraphicsAfterRepaint(const nsACString& aImageData) {
nsCOMPtr<nsIInputStream> stream;
nsresult rv = NS_NewCStringInputStream(getter_AddRefs(stream), aImageData);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
RefPtr<gfx::SourceSurface> surface =
image::ImageOps::DecodeToSurface(stream.forget(),
NS_LITERAL_CSTRING("image/png"), 0);
MOZ_RELEASE_ASSERT(surface);
RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
gfx::DataSourceSurface::ScopedMap map(dataSurface,
gfx::DataSourceSurface::READ);
UpdateMiddlemanCanvas(surface->GetSize().width,
surface->GetSize().height,
map.GetStride(), map.GetData());
}
void RestoreMainGraphics() {
if (!gLastPaintWidth || !gLastPaintHeight) {
return;
}
size_t stride =
layers::ImageDataSerializer::ComputeRGBStride(gSurfaceFormat,
gLastPaintWidth);
UpdateMiddlemanCanvas(gLastPaintWidth, gLastPaintHeight, stride,
gGraphicsMemory);
}
void ClearGraphics() {
if (!gGraphics) {
return;
}
AutoSafeJSContext cx;
JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
if (NS_FAILED(gGraphics->ClearCanvas())) {
MOZ_CRASH("ClearGraphics");
}
}
bool InRepaintStressMode() {
static bool checked = false;
static bool rv;

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

@ -71,15 +71,41 @@ static Monitor* gMonitor;
// Graphics
///////////////////////////////////////////////////////////////////////////////
// Painting can happen in two ways:
//
// - When the main child runs (the recording child, or a dedicated replaying
// child if there is no recording child), it does so on the user's machine and
// paints into gGraphicsMemory, a buffer shared with the middleman process.
// After the buffer has been updated, a PaintMessage is sent to the middleman.
//
// - When the user is within the recording and we want to repaint old graphics,
// gGraphicsMemory is not updated (the replaying process could be on a distant
// machine and be unable to access the buffer). Instead, the replaying process
// does its repaint locally, losslessly compresses it to a PNG image, encodes
// it to base64, and sends it to the middleman. The middleman then undoes this
// encoding and paints the resulting image.
//
// In either case, a canvas in the middleman is filled with the paint data,
// updating the graphics shown by the UI process. The canvas is managed by
// devtools/server/actors/replay/graphics.js
extern void* gGraphicsMemory;
void InitializeGraphicsMemory();
void SendGraphicsMemoryToChild();
// Update the graphics painted in the UI process, per painting data received
// from a child process, or null if a repaint was triggered and failed due to
// an unhandled recording divergence.
void UpdateGraphicsInUIProcess(const PaintMessage* aMsg);
// Update the graphics painted in the UI process after a paint happened in the
// main child.
void UpdateGraphicsAfterPaint(const PaintMessage& aMsg);
// Update the graphics painted in the UI process after a repaint happened in
// some replaying child.
void UpdateGraphicsAfterRepaint(const nsACString& imageData);
// Restore the graphics last painted by the main child.
void RestoreMainGraphics();
// Clear any graphics painted in the UI process.
void ClearGraphics();
// ID for the mach message sent from a child process to the middleman to
// request a port for the graphics shmem.