зеркало из https://github.com/mozilla/gecko-dev.git
Merge inbound to mozilla-central. a=merge
This commit is contained in:
Коммит
1eb05019f4
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче