author | Dorel Luca <dluca@mozilla.com> |
Fri, 03 Jan 2020 18:43:40 +0200 | |
changeset 508781 | eb5a66c10085c83f5a341a7ba302c98bd6c43a2a |
parent 508780 | 6512aa950f9182e8dd8c55ccb56d06720fb56537 |
child 508782 | e33851f3f935f473adf9b1dbb430fe805f86ff91 |
push id | 104196 |
push user | dluca@mozilla.com |
push date | Fri, 03 Jan 2020 16:44:44 +0000 |
treeherder | autoland@eb5a66c10085 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 1603856, 1606447, 1605584, 1598951 |
milestone | 73.0a1 |
backs out | cf403935cf9bc721f22f808a539a79fc9aa13720 af6e8b6be5740148d3c342723e28f4bcdf1748e4 fbfa92bd4ec8e82b7141a1f2d816a0432de60278 a6d13450295dd3e20f0410d2a1501bf1ed630009 e0b049ff115f5d4a72c005127c26424c6753b277 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/webreplay/components/WebReplayPlayer.js +++ b/devtools/client/webreplay/components/WebReplayPlayer.js @@ -612,24 +612,21 @@ class WebReplayPlayer extends Component const isHighlighted = highlightedMessage == message.id; const uncached = message.executionPoint && !this.isCached(message); const atPausedLocation = pausedMessage && sameLocation(pausedMessage, message); - let frameLocation = ""; - if (message.frame) { - const { source, line, column } = message.frame; - const filename = source.split("/").pop(); - frameLocation = `${filename}:${line}`; - if (column > 100) { - frameLocation += `:${column}`; - } + const { source, line, column } = message.frame; + const filename = source.split("/").pop(); + let frameLocation = `${filename}:${line}`; + if (column > 100) { + frameLocation += `:${column}`; } return dom.a({ className: classname("message", { overlayed: isOverlayed, future: isFuture, highlighted: isHighlighted, uncached,
--- a/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js +++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_breakpoints-07.js @@ -6,26 +6,25 @@ "use strict"; // Pausing at a debugger statement on startup confuses the debugger. PromiseTestUtils.whitelistRejectionsGlobally(/Unknown source actor/); // Test interaction of breakpoints with debugger statements. add_task(async function() { - const dbg = await attachRecordingDebugger("doc_debugger_statements.html"); - await resume(dbg); - - invokeInTab("foo"); + const dbg = await attachRecordingDebugger("doc_debugger_statements.html", { + skipInterrupt: true, + }); await waitForPaused(dbg); const pauseLine = getVisibleSelectedFrameLine(dbg); - ok(pauseLine == 7, "Paused at first debugger statement"); + ok(pauseLine == 6, "Paused at first debugger statement"); - await addBreakpoint(dbg, "doc_debugger_statements.html", 8); + await addBreakpoint(dbg, "doc_debugger_statements.html", 7); + await resumeToLine(dbg, 7); await resumeToLine(dbg, 8); - await resumeToLine(dbg, 9); await dbg.actions.removeAllBreakpoints(getContext(dbg)); - await rewindToLine(dbg, 7); - await resumeToLine(dbg, 9); + await rewindToLine(dbg, 6); + await resumeToLine(dbg, 8); await shutdownDebugger(dbg); });
--- a/devtools/client/webreplay/mochitest/examples/doc_debugger_statements.html +++ b/devtools/client/webreplay/mochitest/examples/doc_debugger_statements.html @@ -1,12 +1,10 @@ <html lang="en" dir="ltr"> <body> <div id="maindiv" style="padding-top:50px">Hello World!</div> </body> <script> -function foo() { - debugger; - document.getElementById("maindiv").innerHTML = "Foobar!"; - debugger; -} +debugger; +document.getElementById("maindiv").innerHTML = "Foobar!"; +debugger; </script> </html>
--- a/devtools/client/webreplay/mochitest/examples/doc_rr_basic.html +++ b/devtools/client/webreplay/mochitest/examples/doc_rr_basic.html @@ -9,28 +9,28 @@ function recordingFinished() { } var number = 0; function f() { updateNumber(); if (number >= 10) { window.setTimeout(recordingFinished); return; } - window.setTimeout(f, 100); + window.setTimeout(f, 1); } function updateNumber() { number++; document.getElementById("maindiv").innerHTML = "Number: " + number; testStepping(); } function testStepping() { var a = 0; testStepping2(); return a; } function testStepping2() { var c = this; // Note: using 'this' causes the script to have a prologue. c++; c--; } -window.setTimeout(f, 100); +window.setTimeout(f, 1); </script> </html>
deleted file mode 100644 --- a/devtools/server/actors/replay/connection-worker.js +++ /dev/null @@ -1,123 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint-disable spaced-comment, brace-style, indent-legacy, no-shadow */ - -"use strict"; - -// This worker is spawned in the parent process the first time a middleman -// connects to a cloud based replaying process, and manages the web sockets -// which are used to connect to those remote processes. This could be done on -// the main thread, but is handled here because main thread web sockets must -// be associated with particular windows, which is not the case for the -// scope which connection.js is created in. - -self.addEventListener("message", function({ data }) { - const { type, id } = data; - switch (type) { - case "connect": - try { - doConnect(id, data.channelId, data.address); - } catch (e) { - dump(`doConnect error: ${e}\n`); - } - break; - case "send": - try { - doSend(id, data.buf); - } catch (e) { - dump(`doSend error: ${e}\n`); - } - break; - default: - ThrowError(`Unknown event type ${type}`); - } -}); - -const gConnections = []; - -function doConnect(id, channelId, address) { - if (gConnections[id]) { - ThrowError(`Duplicate connection ID ${id}`); - } - const socket = new WebSocket(address); - socket.onopen = evt => onOpen(id, evt); - socket.onclose = evt => onClose(id, evt); - socket.onmessage = evt => onMessage(id, evt); - socket.onerror = evt => onError(id, evt); - - const connection = { outgoing: [] }; - const promise = new Promise(r => (connection.openWaiter = r)); - - gConnections[id] = connection; - - (async function sendMessages() { - await promise; - while (gConnections[id]) { - if (connection.outgoing.length) { - const buf = connection.outgoing.shift(); - try { - socket.send(buf); - } catch (e) { - ThrowError(`Send error ${e}`); - } - } else { - await new Promise(resolve => (connection.sendWaiter = resolve)); - } - } - })(); -} - -function doSend(id, buf) { - const connection = gConnections[id]; - connection.outgoing.push(buf); - if (connection.sendWaiter) { - connection.sendWaiter(); - connection.sendWaiter = null; - } -} - -function onOpen(id) { - // Messages can now be sent to the socket. - gConnections[id].openWaiter(); -} - -function onClose(id, evt) { - gConnections[id] = null; -} - -// Message data must be sent to the main thread in the order it was received. -// This is a bit tricky with web socket messages as they return data blobs, -// and blob.arrayBuffer() returns a promise such that multiple promises could -// be resolved out of order. Make sure this doesn't happen by using a single -// async frame to resolve the promises and forward them in order. -const gMessages = []; -let gMessageWaiter = null; -(async function processMessages() { - while (true) { - if (gMessages.length) { - const { id, promise } = gMessages.shift(); - const buf = await promise; - postMessage({ id, buf }); - } else { - await new Promise(resolve => (gMessageWaiter = resolve)); - } - } -})(); - -function onMessage(id, evt) { - gMessages.push({ id, promise: evt.data.arrayBuffer() }); - if (gMessageWaiter) { - gMessageWaiter(); - gMessageWaiter = null; - } -} - -function onError(id, evt) { - ThrowError(`Socket error ${evt}`); -} - -function ThrowError(msg) { - dump(`Connection Worker Error: ${msg}\n`); - throw new Error(msg); -}
deleted file mode 100644 --- a/devtools/server/actors/replay/connection.js +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint-disable spaced-comment, brace-style, indent-legacy, no-shadow */ - -"use strict"; - -// This file provides an interface for connecting middleman processes with -// replaying processes living remotely in the cloud. - -let gWorker; -let gCallback; -let gNextConnectionId = 1; - -// eslint-disable-next-line no-unused-vars -function Initialize(callback) { - gWorker = new Worker("connection-worker.js"); - gWorker.addEventListener("message", onMessage); - gCallback = callback; -} - -function onMessage(evt) { - gCallback(evt.data.id, evt.data.buf); -} - -// eslint-disable-next-line no-unused-vars -function Connect(channelId, address, callback) { - const id = gNextConnectionId++; - gWorker.postMessage({ type: "connect", id, channelId, address }); - return id; -} - -// eslint-disable-next-line no-unused-vars -function SendMessage(id, buf) { - gWorker.postMessage({ type: "send", id, buf }); -} - -// eslint-disable-next-line no-unused-vars -var EXPORTED_SYMBOLS = ["Initialize", "Connect", "SendMessage"];
--- a/devtools/server/actors/replay/control.js +++ b/devtools/server/actors/replay/control.js @@ -25,582 +25,470 @@ Cu.evalInSandbox( "addDebuggerToGlobal(this);", sandbox ); const { RecordReplayControl, Services, pointEquals, pointToString, - positionToString, findClosestPoint, pointArrayIncludes, pointPrecedes, + positionEquals, positionSubsumes, setInterval, } = sandbox; const InvalidCheckpointId = 0; const FirstCheckpointId = 1; // Application State Control // // This section describes the strategy used for managing child processes so that // we can be responsive to user interactions. There is at most one recording -// child process, and any number of replaying child processes. +// child process, and one or more replaying child processes. // -// When the user is not viewing old parts of the recording, they will interact -// with the recording process. The recording process only runs forward and adds -// new data to the recording which the replaying processes can consume. +// The recording child cannot rewind: it only runs forward and adds new data to +// the recording. If we are paused or trying to pause at a place within the +// recording, then the recording child is also paused. // -// Replaying processes can be created in two ways: we can spawn a replaying -// process at the beginning of the recording, or we can ask an existing -// replaying process to fork, which creates a new replaying process at the same -// point in the recording. -// -// Replaying processes fit into one of the following categories: +// To manage the replaying children, we identify a set of checkpoints that will +// be saved by some replaying child. The duration between saved checkpoints +// should be roughly equal, and they must be sufficiently closely spaced that +// any point in the recording can be quickly reached by some replaying child +// restoring the previous saved checkpoint and then running forward to the +// target point. // -// * Root children are spawned at the beginning of the recording. They stop at -// the first checkpoint and coordinate communication with other processes that -// have been forked from them. +// As we identify the saved checkpoints, each is assigned to some replaying +// child, which is responsible for both saving that checkpoint and for scanning +// the contents of the recording from that checkpoint until the next saved +// checkpoint. // -// * Trunk children are forked from the root child. They run through the -// recording and periodically fork off new branches. There is only one trunk -// for each root child. +// When adding new data to the recording, replaying children will scan and save +// the regions and checkpoints they are responsible for, and will otherwise play +// forward as far as they can in the recording. We always want to have one or +// more replaying children that are at far end of the recording and able to +// start scanning/saving as the recording grows. In order to ensure this, +// consider the following: // -// * Branch children are forked from trunk or other branch children. They run -// forward to a specific point in the recording, and then sit idle while they -// periodically fork off new leaves. +// - Replaying must be faster than recording. While recording there is idle time +// as we wait for user input, timers, etc. that is not present while +// replaying. Suppose that replaying speed is F times the recording speed. +// F must be less than one. // -// * Leaf children are forked from branches. They run through a part of the -// recording and can perform actions such as scanning the recording or -// performing operations at different points in the recording. +// - Scanning and saving a section of the recording is slower than recording. +// Both of these have a lot of overhead, and suppose that scanning is S times +// the recording speed. S will be more than one. // -// During startup, we will spawn a single trunk child which runs through the -// recording and forks a branch child at certain saved checkpoints. The saved -// checkpoints are spaced so that they aren't far apart. Each branch child forks -// a leaf child which scans the recording from that branch's saved checkpoint up -// to the next saved checkpoint, and then sits idle and responds to queries -// about the scanned region. Other leaf children can be forked from the -// branches, allowing us to quickly perform tasks anywhere in the recording once -// all the branches are in place. +// - When there is more than one replaying child, each child can divide its time +// between scanning/saving and simply replaying. We want its average speed to +// match that of the recording. If there are N replaying children, each child +// scans 1/N of the recording, so the average speed compared to the recording +// is S/N + F*(N-1)/N. We want this term to be one or slightly less. // -// The number of leaves which can simultaneously be operating has a small -// limit to avoid overloading the system. Tasks requiring new children are -// placed in a queue and resolved in priority order. +// For example, if F = 0.75 and S = 3, then replaying is 33% faster than +// recording, and scanning is three times slower than recording. If N = 4, +// the average speed is 3/4 + 0.75*3/4 = 1.31 times that of recording, which +// will cause the replaying processes to fall further and further behind the +// recording. If N = 12, the average speed is 3/12 + 0.75*11/12 = 0.94 times +// that of the recording, which will allow the replaying processes to keep up +// with the recording. +// +// Eventually we'll want to do this analysis dynamically to scale up or throttle +// back the number of active replaying processes. For now, though, we rely on +// a fixed number of replaying processes, and hope that it is enough. //////////////////////////////////////////////////////////////////////////////// // Child Processes //////////////////////////////////////////////////////////////////////////////// -// Get a unique ID for a child process. -function processId(rootId, forkId) { - return forkId ? `#${rootId}:${forkId}` : `#${rootId}`; -} - -// Child which is always at the end of the recording. When there is a recording -// child this is it, and when we are replaying an old execution this is a -// replaying child that doesn't fork and is used like a recording child. -let gMainChild; - -// All child processes, indexed by their ID. -const gChildren = new Map(); - -// All child processes that are currently unpaused. -const gUnpausedChildren = new Set(); - // Information about a child recording or replaying process. -function ChildProcess(rootId, forkId, recording, recordingLength) { - // ID for the root recording/replaying process this is associated with. - this.rootId = rootId; - - // ID for the particular fork of the root this process represents, or zero if - // not forked. - this.forkId = forkId; - - // Unique ID for this process. - this.id = processId(rootId, forkId); - gChildren.set(this.id, this); +function ChildProcess(id, recording) { + this.id = id; // Whether this process is recording. this.recording = recording; - // How much of the recording is available to this process. - this.recordingLength = recordingLength; - // Whether this process is paused. this.paused = false; - gUnpausedChildren.add(this); // The last point we paused at. this.lastPausePoint = null; - // Whether this child has terminated and is unusable. - this.terminated = false; + // Last reported memory usage for this child. + this.lastMemoryUsage = null; + + // All checkpoints which this process has saved or will save, which is a + // subset of all the saved checkpoints. + this.savedCheckpoints = new Set(recording ? [] : [FirstCheckpointId]); - // The last time a ping message was sent to the process. - this.lastPingTime = Date.now(); + // All saved checkpoints whose region of the recording has been scanned or is + // in the process of being scanned by this child. + this.scannedCheckpoints = new Set(); - // All pings we have sent since they were reset for this process. - this.pings = []; + // All snapshots which this child has taken. + this.snapshots = []; + + // Whether this child has diverged from the recording and cannot run forward. + this.divergedFromRecording = false; + + // Whether this child has crashed and is unusable. + this.crashed = false; - // Manifests queued up for this child. If the child is unpaused, the first one - // is currently being processed. Child processes initially have a manifest to - // run forward to the first checkpoint. - this.manifests = [ - { - manifest: {}, - onFinished: ({ point }) => { - if (this == gMainChild) { - getCheckpointInfo(FirstCheckpointId).point = point; - Services.tm.dispatchToMainThread( - recording ? maybeResumeRecording : setMainChild - ); - } - }, + // Any manifest which is currently being executed. Child processes initially + // have a manifest to run forward to the first checkpoint. + this.manifest = { + onFinished: ({ point }) => { + if (this == gMainChild) { + getCheckpointInfo(FirstCheckpointId).point = point; + Services.tm.dispatchToMainThread( + recording ? maybeResumeRecording : setMainChild + ); + } else { + this.snapshots.push(checkpointExecutionPoint(FirstCheckpointId)); + } }, - ]; + }; + + // The time the manifest was sent to the child. + this.manifestSendTime = Date.now(); + + // Any async manifest which this child has partially processed. + this.asyncManifest = null; } -const PingIntervalSeconds = 2; -const MaxStalledPings = 10; -let gNextPingId = 1; - ChildProcess.prototype = { - // Stop this child. - terminate() { - gChildren.delete(this.id); - gUnpausedChildren.delete(this); - RecordReplayControl.terminate(this.rootId, this.forkId); - this.terminated = true; - }, - // Get the execution point where this child is currently paused. pausePoint() { assert(this.paused); return this.lastPausePoint; }, // Get the checkpoint where this child is currently paused. pauseCheckpoint() { const point = this.pausePoint(); assert(!point.position); return point.checkpoint; }, // Send a manifest to paused child to execute. The child unpauses while - // executing the manifest, and pauses again when it finishes. This returns a - // promise that resolves with the manifest's result when it finishes. - // If specified, onFinishedCallback will be invoked with the manifest - // result as well. - sendManifest(manifest, onFinishedCallback) { - assert(!this.terminated); - return new Promise(resolve => { - this.manifests.push({ - manifest, - onFinished(response) { - if (onFinishedCallback) { - onFinishedCallback(response); - } - resolve(response); - }, - }); + // executing the manifest, and pauses again when it finishes. Manifests have + // the following properties: + // + // contents: The JSON object to send to the child describing the operation. + // onFinished: A callback which is called after the manifest finishes with the + // manifest's result. + // destination: An optional destination where the child will end up. + // expectedDuration: Optional estimate of the time needed to run the manifest. + // mightRewind: Flag that must be set if the child might restore a snapshot + // while processing the manifest. + sendManifest(manifest) { + assert(!this.crashed); + assert(this.paused); + this.paused = false; + this.manifest = manifest; + this.manifestSendTime = Date.now(); - if (this.paused) { - this._startNextManifest(); - } - }); + const { contents, mightRewind } = manifest; + + dumpv(`SendManifest #${this.id} ${stringify(contents)}`); + RecordReplayControl.sendManifest(this.id, contents, mightRewind); }, // Called when the child's current manifest finishes. manifestFinished(response) { - dumpv(`ManifestFinished ${this.id} ${stringify(response)}`); - assert(!this.paused); - this.paused = true; - gUnpausedChildren.delete(this); - const { onFinished } = this.manifests.shift(); - if (response) { if (response.point) { this.lastPausePoint = response.point; } + if (response.memoryUsage) { + this.lastMemoryUsage = response.memoryUsage; + } if (response.exception) { ThrowError(response.exception); } + if (response.restoredSnapshot) { + assert(this.manifest.mightRewind); + } } - onFinished(response); - - this._startNextManifest(); - }, - - // Send this child the next manifest in its queue, if there is one. - _startNextManifest() { - assert(this.paused); + this.paused = true; + this.manifest.onFinished(response); + this.manifest = null; + maybeDumpStatistics(); - if (this.manifests.length) { - const { manifest } = this.manifests[0]; - dumpv(`SendManifest ${this.id} ${stringify(manifest)}`); - RecordReplayControl.sendManifest(this.rootId, this.forkId, manifest); - this.paused = false; - gUnpausedChildren.add(this); - - // Reset pings when sending a new manifest. - this.pings.length = 0; - this.lastPingTime = Date.now(); + if (this != gMainChild) { + pokeChildSoon(this); } }, // Block until this child is paused. If maybeCreateCheckpoint is specified // then a checkpoint is created if this child is recording, so that it pauses // quickly (otherwise it could sit indefinitely if there is no page activity). waitUntilPaused(maybeCreateCheckpoint) { if (this.paused) { return; } - dumpv(`WaitUntilPaused ${this.id}`); - if (maybeCreateCheckpoint) { - assert(this.recording && !this.forkId); - RecordReplayControl.createCheckpointInRecording(this.rootId); + RecordReplayControl.waitUntilPaused(this.id, maybeCreateCheckpoint); + assert(this.paused || this.crashed); + }, + + // Add a checkpoint for this child to save. + addSavedCheckpoint(checkpoint) { + dumpv(`AddSavedCheckpoint #${this.id} ${checkpoint}`); + this.savedCheckpoints.add(checkpoint); + }, + + // Get the checkpoints which the child must save in the range [start, end]. + savedCheckpointsInRange(start, end) { + const rv = []; + for (let i = start; i <= end; i++) { + if (this.savedCheckpoints.has(i)) { + rv.push(i); + } } - while (!this.paused) { - this.maybePing(); - RecordReplayControl.maybeProcessNextMessage(); - } - dumpv(`WaitUntilPaused Done`); - assert(this.paused || this.terminated); + return rv; + }, + + // Get the locations of snapshots for saved checkpoints which the child must + // save when running forward to endpoint. + getSnapshotsForSavedCheckpoints(endpoint) { + assert(pointPrecedes(this.pausePoint(), endpoint)); + return this.savedCheckpointsInRange( + this.pausePoint().checkpoint + 1, + endpoint.checkpoint + ).map(checkpointExecutionPoint); }, - // Hang Detection - // - // Replaying processes will be terminated if no execution progress has been - // made for some number of seconds. This generates a crash report for - // diagnosis and allows another replaying process to be spawned in its place. - // We detect that no progress is being made by periodically sending ping - // messages to the replaying process, and comparing the progress values - // returned by them. The ping messages are sent at least PingIntervalSeconds - // apart, and the process is considered hanged if at any point the last - // MaxStalledPings ping messages either did not respond or responded with the - // same progress value. - // - // Dividing our accounting between different ping messages avoids treating - // processes as hanged when the computer wakes up after sleeping: no pings - // will be sent while the computer is sleeping and processes are suspended, - // so the computer will need to be awake for some time before any processes - // are marked as hanged (before which they will hopefully be able to make - // progress). - isHanged() { - if (this.pings.length < MaxStalledPings) { - return false; + // Get the point of the last snapshot which was taken. + lastSnapshot() { + return this.snapshots[this.snapshots.length - 1]; + }, + + // Get an estimate of the amount of time required for this child to reach an + // execution point. + timeToReachPoint(point) { + let startDelay = 0; + let startPoint = this.lastPausePoint; + if (!startPoint) { + startPoint = checkpointExecutionPoint(FirstCheckpointId); } - - const firstIndex = this.pings.length - MaxStalledPings; - const firstValue = this.pings[firstIndex].progress; - if (!firstValue) { - // The child hasn't responded to any of the pings. - return true; - } - - for (let i = firstIndex; i < this.pings.length; i++) { - if (this.pings[i].progress && this.pings[i].progress != firstValue) { - return false; + if (!this.paused) { + if (this.manifest.expectedDuration) { + const elapsed = Date.now() - this.manifestSendTime; + if (elapsed < this.manifest.expectedDuration) { + startDelay = this.manifest.expectedDuration - elapsed; + } + } + if (this.manifest.destination) { + startPoint = this.manifest.destination; } } - - return true; - }, - - maybePing() { - assert(!this.paused); - if (this.recording) { - return; - } - - const now = Date.now(); - if (now < this.lastPingTime + PingIntervalSeconds * 1000) { - return; - } - - if (this.isHanged()) { - // Try to get the child to crash, so that we can get a minidump. - RecordReplayControl.crashHangedChild(this.rootId, this.forkId); + let startCheckpoint; + if (this.snapshots.length) { + let snapshotIndex = this.snapshots.length - 1; + while (pointPrecedes(point, this.snapshots[snapshotIndex])) { + snapshotIndex--; + } + startCheckpoint = this.snapshots[snapshotIndex].checkpoint; } else { - const id = gNextPingId++; - RecordReplayControl.ping(this.rootId, this.forkId, id); - this.pings.push({ id }); + // Children which just started haven't taken snapshots. + startCheckpoint = FirstCheckpointId; } - }, - - pingResponse(id, progress) { - for (const entry of this.pings) { - if (entry.id == id) { - entry.progress = progress; - break; - } - } - }, - - updateRecording() { - RecordReplayControl.updateRecording( - this.rootId, - this.forkId, - this.recordingLength + return ( + startDelay + checkpointRangeDuration(startCheckpoint, point.checkpoint) ); - this.recordingLength = RecordReplayControl.recordingLength(); }, }; -// eslint-disable-next-line no-unused-vars -function ManifestFinished(rootId, forkId, response) { - try { - const child = gChildren.get(processId(rootId, forkId)); - if (child) { - child.manifestFinished(response); - } - } catch (e) { - dump(`ERROR: ManifestFinished threw exception: ${e} ${e.stack}\n`); +// Child which is always at the end of the recording. When there is a recording +// child this is it, and when we are replaying an old execution this is a +// replaying child that is unable to rewind and is used in the same way as the +// recording child. +let gMainChild; + +// Replaying children available for exploring the interior of the recording, +// indexed by their ID. +const gReplayingChildren = []; + +function lookupChild(id) { + if (id == gMainChild.id) { + return gMainChild; } + return gReplayingChildren[id]; } -// eslint-disable-next-line no-unused-vars -function PingResponse(rootId, forkId, pingId, progress) { - try { - const child = gChildren.get(processId(rootId, forkId)); +function closestChild(point) { + let minChild = null, + minTime = Infinity; + for (const child of gReplayingChildren) { if (child) { - child.pingResponse(pingId, progress); + const time = child.timeToReachPoint(point); + if (time < minTime) { + minChild = child; + minTime = time; + } } - } catch (e) { - dump(`ERROR: PingResponse threw exception: ${e} ${e.stack}\n`); } + return minChild; } // The singleton ReplayDebugger, or undefined if it doesn't exist. let gDebugger; //////////////////////////////////////////////////////////////////////////////// -// Child Management +// Asynchronous Manifests //////////////////////////////////////////////////////////////////////////////// // Priority levels for asynchronous manifests. const Priority = { HIGH: 0, MEDIUM: 1, LOW: 2, }; -// The singleton root child, if any. -let gRootChild; - -// The singleton trunk child, if any. -let gTrunkChild; - -// All branch children available for creating new leaves, in order. -const gBranchChildren = []; - -// ID for the next fork we create. -let gNextForkId = 1; - -// Fork a replaying child, returning the new one. -function forkChild(child) { - const forkId = gNextForkId++; - const newChild = new ChildProcess( - child.rootId, - forkId, - false, - child.recordingLength - ); - - dumpv(`Forking ${child.id} -> ${newChild.id}`); - - child.sendManifest({ kind: "fork", id: forkId }); - return newChild; -} - -// Immediately create a new leaf child and take it to endpoint, by forking the -// closest branch child to endpoint. onFinished will be called when the child -// reaches the endpoint. -function newLeafChild(endpoint, onFinished = () => {}) { - assert( - !pointPrecedes(checkpointExecutionPoint(gLastSavedCheckpoint), endpoint) - ); +// Asynchronous manifest worklists. +const gAsyncManifests = [new Set(), new Set(), new Set()]; - let entry; - for (let i = gBranchChildren.length - 1; i >= 0; i--) { - entry = gBranchChildren[i]; - if (!pointPrecedes(endpoint, entry.point)) { - break; - } - } - - const child = forkChild(entry.child); - if (pointEquals(endpoint, entry.point)) { - onFinished(child); - } else { - child.sendManifest({ kind: "runToPoint", endpoint }, () => - onFinished(child) - ); - } - return child; -} - -// How many leaf children we can have running simultaneously. -const MaxRunningLeafChildren = 4; - -// How many leaf children are currently running. -let gNumRunningLeafChildren = 0; - -// Resolve hooks for tasks waiting on a new leaf child, organized by priority. -const gChildWaiters = [[], [], []]; - -// Asynchronously create a new leaf child and take it to endpoint, respecting -// the limits on the maximum number of running leaves and returning the new -// child when it reaches the endpoint. -async function ensureLeafChild(endpoint, priority = Priority.HIGH) { - if (gNumRunningLeafChildren < MaxRunningLeafChildren) { - gNumRunningLeafChildren++; - } else { - await new Promise(resolve => gChildWaiters[priority].push(resolve)); - } - +// Send a manifest asynchronously, returning a promise that resolves when the +// manifest has been finished. Async manifests have the following properties: +// +// shouldSkip: Callback invoked before sending the manifest. Returns true if the +// manifest should not be sent, and the promise resolved immediately. +// +// contents: Callback invoked with the executing child when it is being sent. +// Returns the contents to send to the child. +// +// onFinished: Callback invoked with the executing child and manifest response +// after the manifest finishes. +// +// point: Optional point which the associated child must reach before sending +// the manifest. +// +// snapshot: Optional point at which the associated child should take a snapshot +// while heading to the point. +// +// scanCheckpoint: If the manifest relies on scan data, the saved checkpoint +// whose range the child must have scanned. Such manifests do not have side +// effects in the child, and can be sent to the active child. +// +// priority: Optional priority for this manifest. Default is HIGH. +// +// destination: An optional destination where the child will end up. +// +// expectedDuration: Optional estimate of the time needed to run the manifest. +// +// mightRewind: Flag that must be set if the child might restore a snapshot +// while processing the manifest. +function sendAsyncManifest(manifest) { + pokeChildrenSoon(); return new Promise(resolve => { - newLeafChild(endpoint, child => resolve(child)); + manifest.resolve = resolve; + const priority = manifest.priority || Priority.HIGH; + gAsyncManifests[priority].add(manifest); }); } -// After a leaf child becomes idle and/or is about to be terminated, mark it ass -// no longer running and continue any task waiting on a new leaf. -function stopRunningLeafChild() { - gNumRunningLeafChildren--; +// Pick the best async manifest for a child to process. +function pickAsyncManifest(child, priority) { + const worklist = gAsyncManifests[priority]; + + let best = null, + bestTime = Infinity; + for (const manifest of worklist) { + // Prune any manifests that can be skipped. + if (manifest.shouldSkip()) { + manifest.resolve(); + worklist.delete(manifest); + continue; + } - if (gNumRunningLeafChildren < MaxRunningLeafChildren) { - for (const waiters of gChildWaiters) { - if (waiters.length) { - const resolve = waiters.shift(); - gNumRunningLeafChildren++; - resolve(); + // Manifests relying on scan data can be handled by any child, at any point. + // These are the best ones to pick. + if (manifest.scanCheckpoint) { + if (child.scannedCheckpoints.has(manifest.scanCheckpoint)) { + assert(!manifest.point); + best = manifest; + break; + } else { + continue; } } + + // The active child cannot process other asynchronous manifests which don't + // rely on scan data, as they can move the child or have other side effects. + if (child == gActiveChild) { + continue; + } + + // Pick the manifest which requires the least amount of travel time. + assert(manifest.point); + const time = child.timeToReachPoint(manifest.point); + if (time < bestTime) { + best = manifest; + bestTime = time; + } } -} -// Terminate a running leaf child that is no longer needed. -function terminateRunningLeafChild(child) { - stopRunningLeafChild(); - - dumpv(`Terminate ${child.id}`); - child.terminate(); -} - -// The next ID to use for a root replaying process. -let gNextRootId = 1; - -// Spawn the single trunk child. -function spawnTrunkChild() { - if (!RecordReplayControl.canRewind()) { - return; - } - - const id = gNextRootId++; - RecordReplayControl.spawnReplayingChild(id); - gRootChild = new ChildProcess( - id, - 0, - false, - RecordReplayControl.recordingLength() - ); - - gTrunkChild = forkChild(gRootChild); - - // We should be at the beginning of the recording, and don't have enough space - // to create any branches yet. - assert(gLastSavedCheckpoint == InvalidCheckpointId); -} - -function forkBranchChild(lastSavedCheckpoint, nextSavedCheckpoint) { - if (!RecordReplayControl.canRewind()) { - return; + if (best) { + worklist.delete(best); } - // The recording is flushed and the trunk child updated every time we add - // a saved checkpoint, so the child we fork here will have the recording - // contents up to the next saved checkpoint. Branch children are only able to - // run up to the next saved checkpoint. - gTrunkChild.updateRecording(); - - const child = forkChild(gTrunkChild); - const point = checkpointExecutionPoint(lastSavedCheckpoint); - gBranchChildren.push({ child, point }); - - const endpoint = checkpointExecutionPoint(nextSavedCheckpoint); - gTrunkChild.sendManifest({ - kind: "runToPoint", - endpoint, - - // External calls are flushed in the trunk child so that external calls - // from throughout the recording will be cached in the root child. - flushExternalCalls: true, - }); + return best; } -// eslint-disable-next-line no-unused-vars -function Initialize(recordingChildId) { - try { - if (recordingChildId != undefined) { - assert(recordingChildId == 0); - gMainChild = new ChildProcess(recordingChildId, 0, true); - } else { - // If there is no recording child, spawn a replaying child in its place. - const id = gNextRootId++; - RecordReplayControl.spawnReplayingChild(id); - gMainChild = new ChildProcess( - id, - 0, - false, - RecordReplayControl.recordingLength - ); +function processAsyncManifest(child) { + // If the child has partially processed a manifest, continue with it. + let manifest = child.asyncManifest; + child.asyncManifest = null; + + if (manifest && child == gActiveChild) { + // After a child becomes the active child, it gives up on any in progress + // async manifest it was processing. + sendAsyncManifest(manifest); + manifest = null; + } + + if (!manifest) { + for (const priority of Object.values(Priority)) { + manifest = pickAsyncManifest(child, priority); + if (manifest) { + break; + } } - gActiveChild = gMainChild; - return gControl; - } catch (e) { - dump(`ERROR: Initialize threw exception: ${e}\n`); + if (!manifest) { + return false; + } } -} - -//////////////////////////////////////////////////////////////////////////////// -// PromiseMap -//////////////////////////////////////////////////////////////////////////////// -// Map from arbitrary keys to promises that resolve when any associated work is -// complete. This is used to avoid doing redundant work in different async -// tasks. -function PromiseMap() { - this.map = new Map(); -} + child.asyncManifest = manifest; + + if ( + manifest.point && + maybeReachPoint(child, manifest.point, manifest.snapshot) + ) { + // The manifest has been partially processed. + return true; + } -PromiseMap.prototype = { - // Returns either { promise } or { promise, resolve }. If resolve is included - // then the caller is responsible for invoking it later. - get(key) { - let promise = this.map.get(key); - if (promise) { - return { promise }; - } + child.sendManifest({ + contents: manifest.contents(child), + onFinished: data => { + child.asyncManifest = null; + manifest.onFinished(child, data); + manifest.resolve(); + }, + destination: manifest.destination, + expectedDuration: manifest.expectedDuration, + mightRewind: manifest.mightRewind, + }); - let resolve; - promise = new Promise(r => { - resolve = r; - }); - this.map.set(key, promise); - return { promise, resolve }; - }, - - set(key, value) { - this.map.set(key, value); - }, -}; + return true; +} //////////////////////////////////////////////////////////////////////////////// // Application State //////////////////////////////////////////////////////////////////////////////// // Any child the user is interacting with, which may be paused or not. let gActiveChild = null; @@ -613,16 +501,25 @@ function CheckpointInfo() { this.duration = 0; // Execution point at the checkpoint. this.point = null; // Whether the checkpoint is saved. this.saved = false; + // If the checkpoint is saved, the time it was assigned to a child. + this.assignTime = null; + + // If the checkpoint is saved and scanned, the time it finished being scanned. + this.scanTime = null; + + // If the checkpoint is saved and scanned, the duration of the scan. + this.scanDuration = null; + // If the checkpoint is saved, any debugger statement hits in its region. this.debuggerStatements = []; // If the checkpoint is saved, any events in its region. this.events = []; } function getCheckpointInfo(id) { @@ -641,58 +538,123 @@ function checkpointRangeDuration(start, return time; } // How much execution time has elapsed since a checkpoint. function timeSinceCheckpoint(id) { return checkpointRangeDuration(id, gCheckpoints.length); } +// How much execution time is captured by a saved checkpoint. +function timeForSavedCheckpoint(id) { + return checkpointRangeDuration(id, nextSavedCheckpoint(id)); +} + +// The checkpoint up to which the recording runs. +let gLastFlushCheckpoint = InvalidCheckpointId; + // How often we want to flush the recording. const FlushMs = 0.5 * 1000; -// The most recent saved checkpoint. The recording is flushed at every saved -// checkpoint. -let gLastSavedCheckpoint = InvalidCheckpointId; -const gSavedCheckpointWaiters = []; +// ID of the last replaying child we picked for saving a checkpoint. +let gLastPickedChildId = 0; function addSavedCheckpoint(checkpoint) { - assert(checkpoint >= gLastSavedCheckpoint); - if (checkpoint == gLastSavedCheckpoint) { - return; - } - - if (gLastSavedCheckpoint != InvalidCheckpointId) { - forkBranchChild(gLastSavedCheckpoint, checkpoint); - } + getCheckpointInfo(checkpoint).saved = true; + getCheckpointInfo(checkpoint).assignTime = Date.now(); - getCheckpointInfo(checkpoint).saved = true; - gLastSavedCheckpoint = checkpoint; + if (RecordReplayControl.canRewind()) { + // Use a round robin approach when picking children for saving checkpoints. + let child; + while (true) { + gLastPickedChildId = (gLastPickedChildId + 1) % gReplayingChildren.length; + child = gReplayingChildren[gLastPickedChildId]; + if (child) { + break; + } + } - gSavedCheckpointWaiters.forEach(resolve => resolve()); - gSavedCheckpointWaiters.length = 0; -} - -function waitForSavedCheckpoint() { - return new Promise(resolve => gSavedCheckpointWaiters.push(resolve)); + child.addSavedCheckpoint(checkpoint); + } } function addCheckpoint(checkpoint, duration) { assert(!getCheckpointInfo(checkpoint).duration); getCheckpointInfo(checkpoint).duration = duration; } +// Bring a child to the specified execution point, sending it one or more +// manifests if necessary. Returns true if the child has not reached the point +// yet but some progress was made, or false if the child is at the point. +// Snapshot specifies any point at which a snapshot should be taken. +function maybeReachPoint(child, endpoint, snapshot) { + if ( + pointEquals(child.pausePoint(), endpoint) && + !child.divergedFromRecording + ) { + return false; + } + + if (pointPrecedes(endpoint, child.pausePoint())) { + restoreSnapshotPriorTo(endpoint); + return true; + } + + if (child.divergedFromRecording) { + restoreSnapshotPriorTo(child.pausePoint()); + return true; + } + + const snapshotPoints = child.getSnapshotsForSavedCheckpoints(endpoint); + if ( + snapshot && + pointPrecedes(child.pausePoint(), snapshot) && + !pointArrayIncludes(snapshotPoints, snapshot) + ) { + snapshotPoints.push(snapshot); + } + + child.sendManifest({ + contents: { kind: "runToPoint", endpoint, snapshotPoints }, + onFinished() { + child.snapshots.push(...snapshotPoints); + }, + destination: endpoint, + expectedDuration: checkpointRangeDuration( + child.pausePoint().checkpoint, + endpoint.checkpoint + ), + }); + + return true; + + // Send the child to its most recent saved checkpoint at or before target. + function restoreSnapshotPriorTo(target) { + let numSnapshots = 0; + while (pointPrecedes(target, child.lastSnapshot())) { + numSnapshots++; + child.snapshots.pop(); + } + child.sendManifest({ + contents: { kind: "restoreSnapshot", numSnapshots }, + onFinished({ restoredSnapshot }) { + assert(restoredSnapshot); + child.divergedFromRecording = false; + }, + destination: child.lastSnapshot(), + mightRewind: true, + }); + } +} + function nextSavedCheckpoint(checkpoint) { - assert(checkpoint == InvalidCheckpointId || gCheckpoints[checkpoint].saved); - while (++checkpoint < gCheckpoints.length) { - if (gCheckpoints[checkpoint].saved) { - return checkpoint; - } - } - return undefined; + assert(gCheckpoints[checkpoint].saved); + // eslint-disable-next-line no-empty + while (!gCheckpoints[++checkpoint].saved) {} + return checkpoint; } function forSavedCheckpointsInRange(start, end, callback) { if (start == FirstCheckpointId && !gCheckpoints[start].saved) { return; } assert(gCheckpoints[start].saved); for ( @@ -700,82 +662,145 @@ function forSavedCheckpointsInRange(star checkpoint < end; checkpoint = nextSavedCheckpoint(checkpoint) ) { callback(checkpoint); } } function forAllSavedCheckpoints(callback) { - forSavedCheckpointsInRange(FirstCheckpointId, gLastSavedCheckpoint, callback); + forSavedCheckpointsInRange(FirstCheckpointId, gLastFlushCheckpoint, callback); } function getSavedCheckpoint(checkpoint) { while (!gCheckpoints[checkpoint].saved) { checkpoint--; } return checkpoint; } // Get the execution point to use for a checkpoint. function checkpointExecutionPoint(checkpoint) { return gCheckpoints[checkpoint].point; } +// Check to see if an idle replaying child can make any progress. +function pokeChild(child) { + assert(child != gMainChild); + + if (!child.paused) { + return; + } + + if (processAsyncManifest(child)) { + return; + } + + if (child == gActiveChild) { + sendActiveChildToPausePoint(); + return; + } + + // If there is nothing to do, run forward to the end of the recording. + if (gLastFlushCheckpoint) { + maybeReachPoint(child, checkpointExecutionPoint(gLastFlushCheckpoint)); + } +} + +function pokeChildSoon(child) { + Services.tm.dispatchToMainThread(() => pokeChild(child)); +} + +let gPendingPokeChildren = false; + +function pokeChildren() { + gPendingPokeChildren = false; + for (const child of gReplayingChildren) { + if (child) { + pokeChild(child); + } + } +} + +function pokeChildrenSoon() { + if (!gPendingPokeChildren) { + Services.tm.dispatchToMainThread(() => pokeChildren()); + gPendingPokeChildren = true; + } +} + //////////////////////////////////////////////////////////////////////////////// // Search State //////////////////////////////////////////////////////////////////////////////// // All currently installed breakpoints. const gBreakpoints = []; // Recording Scanning // // Scanning a section of the recording between two neighboring saved checkpoints // allows the execution points for each script breakpoint position to be queried -// by sending a manifest to the child which performed the scan. Scanning is done -// using leaf children. When the child has finished scanning, it is marked as no -// longer running and remains idle remains idle while it responds to queries on -// the scanned data. - -// Map from saved checkpoints to the leaf child responsible for scanning that saved -// checkpoint's region. -const gSavedCheckpointChildren = new PromiseMap(); - -// Set of saved checkpoints which have finished scanning. -const gScannedSavedCheckpoints = new Set(); +// by sending a manifest to the child which performed the scan. -// Ensure the region for a saved checkpoint has been scanned by some child, and -// return that child. +function findScanChild(checkpoint, requireComplete) { + for (const child of gReplayingChildren) { + if (child && child.scannedCheckpoints.has(checkpoint)) { + if ( + requireComplete && + !child.paused && + child.manifest.contents.kind == "scanRecording" && + child.lastPausePoint.checkpoint == checkpoint + ) { + continue; + } + return child; + } + } + return null; +} + +// Ensure the region for a saved checkpoint has been scanned by some child. async function scanRecording(checkpoint) { - assert(gCheckpoints[checkpoint].saved); - if (checkpoint == gLastSavedCheckpoint) { - await waitForSavedCheckpoint(); - } + assert(checkpoint < gLastFlushCheckpoint); - const { promise, resolve } = gSavedCheckpointChildren.get(checkpoint); - if (!resolve) { - return promise; + const child = findScanChild(checkpoint); + if (child) { + return; } const endpoint = checkpointExecutionPoint(nextSavedCheckpoint(checkpoint)); - const child = await ensureLeafChild(checkpointExecutionPoint(checkpoint)); - await child.sendManifest({ kind: "scanRecording", endpoint }); - - stopRunningLeafChild(); - - gScannedSavedCheckpoints.add(checkpoint); + let snapshotPoints = null; + await sendAsyncManifest({ + shouldSkip: () => !!findScanChild(checkpoint), + contents(child) { + child.scannedCheckpoints.add(checkpoint); + snapshotPoints = child.getSnapshotsForSavedCheckpoints(endpoint); + return { + kind: "scanRecording", + endpoint, + snapshotPoints, + }; + }, + onFinished(child, { duration }) { + child.snapshots.push(...snapshotPoints); + const info = getCheckpointInfo(checkpoint); + if (!info.scanTime) { + info.scanTime = Date.now(); + info.scanDuration = duration; + } + if (gDebugger) { + gDebugger._callOnPositionChange(); + } + }, + point: checkpointExecutionPoint(checkpoint), + destination: endpoint, + expectedDuration: checkpointRangeDuration(checkpoint, endpoint) * 5, + }); - // Update the unscanned regions in the UI. - if (gDebugger) { - gDebugger._callOnPositionChange(); - } - - resolve(child); - return child; + assert(findScanChild(checkpoint)); } function unscannedRegions() { const result = []; function addRegion(startCheckpoint, endCheckpoint) { const start = checkpointExecutionPoint(startCheckpoint).progress; const end = checkpointExecutionPoint(endCheckpoint).progress; @@ -783,63 +808,94 @@ function unscannedRegions() { if (result.length && result[result.length - 1].end == start) { result[result.length - 1].end = end; } else { result.push({ start, end }); } } forAllSavedCheckpoints(checkpoint => { - if (!gScannedSavedCheckpoints.has(checkpoint)) { + if (!findScanChild(checkpoint, /* requireComplete */ true)) { addRegion(checkpoint, nextSavedCheckpoint(checkpoint)); } }); - const lastFlush = gLastSavedCheckpoint || FirstCheckpointId; + const lastFlush = gLastFlushCheckpoint || FirstCheckpointId; if (lastFlush != gRecordingEndpoint) { addRegion(lastFlush, gMainChild.lastPausePoint.checkpoint); } return result; } -// Map from saved checkpoints and positions to the breakpoint hits for that -// position within the range of the checkpoint. -const gHitSearches = new PromiseMap(); +// Map from saved checkpoints to information about breakpoint hits within the +// range of that checkpoint. +const gHitSearches = new Map(); // Only hits on script locations (Break and OnStep positions) can be found by // scanning the recording. function canFindHits(position) { return position.kind == "Break" || position.kind == "OnStep"; } // Find all hits on the specified position between a saved checkpoint and the // following saved checkpoint, using data from scanning the recording. This // returns a promise that resolves with the resulting hits. async function findHits(checkpoint, position) { assert(canFindHits(position)); assert(gCheckpoints[checkpoint].saved); - const key = `${checkpoint}:${positionToString(position)}`; - const { promise, resolve } = gHitSearches.get(key); - if (!resolve) { - return promise; + if (!gHitSearches.has(checkpoint)) { + gHitSearches.set(checkpoint, []); + } + + // Check if we already have the hits. + let hits = findExisting(); + if (hits) { + return hits; } + await scanRecording(checkpoint); const endpoint = nextSavedCheckpoint(checkpoint); - const child = await scanRecording(checkpoint); - const hits = await child.sendManifest({ - kind: "findHits", - position, - startpoint: checkpoint, - endpoint, + await sendAsyncManifest({ + shouldSkip: () => !!findExisting(), + contents() { + return { + kind: "findHits", + position, + startpoint: checkpoint, + endpoint, + }; + }, + onFinished(_, hits) { + if (!gHitSearches.has(checkpoint)) { + gHitSearches.set(checkpoint, []); + } + const checkpointHits = gHitSearches.get(checkpoint); + checkpointHits.push({ position, hits }); + }, + scanCheckpoint: checkpoint, }); - resolve(hits); + hits = findExisting(); + assert(hits); return hits; + + function findExisting() { + const checkpointHits = gHitSearches.get(checkpoint); + if (!checkpointHits) { + return null; + } + const entry = checkpointHits.find( + ({ position: existingPosition, hits }) => { + return positionEquals(position, existingPosition); + } + ); + return entry ? entry.hits : null; + } } // Asynchronously find all hits on a breakpoint's position. async function findBreakpointHits(checkpoint, position) { if (position.kind == "Break") { findHits(checkpoint, position); } } @@ -852,111 +908,149 @@ async function findBreakpointHits(checkp // stepping in, and will be used in the future to improve stepping performance. // // The steps for a frame are the list of execution points for breakpoint // positions traversed when executing a particular script frame, from the // initial EnterFrame to the final OnPop. The steps also include the EnterFrame // execution points for any direct callees of the frame. // Map from point strings to the steps which contain them. -const gFrameSteps = new PromiseMap(); +const gFrameSteps = new Map(); // Map from frame entry point strings to the parent frame's entry point. -const gParentFrames = new PromiseMap(); +const gParentFrames = new Map(); // Find all the steps in the frame which point is part of. This returns a // promise that resolves with the steps that were found. async function findFrameSteps(point) { if (!point.position) { return null; } assert( point.position.kind == "EnterFrame" || point.position.kind == "OnStep" || point.position.kind == "OnPop" ); - const { promise, resolve } = gFrameSteps.get(pointToString(point)); - if (!resolve) { - return promise; + let steps = findExisting(); + if (steps) { + return steps; } // Gather information which the child which did the scan can use to figure out // the different frame steps. const info = gControl.sendRequestMainChild({ type: "frameStepsInfo", script: point.position.script, }); const checkpoint = getSavedCheckpoint(point.checkpoint); - const child = await scanRecording(checkpoint); - const steps = await child.sendManifest({ - kind: "findFrameSteps", - targetPoint: point, - ...info, + await scanRecording(checkpoint); + await sendAsyncManifest({ + shouldSkip: () => !!findExisting(), + contents: () => ({ kind: "findFrameSteps", targetPoint: point, ...info }), + onFinished: (_, steps) => { + for (const p of steps) { + if (p.position.frameIndex == point.position.frameIndex) { + gFrameSteps.set(pointToString(p), steps); + } else { + assert(p.position.kind == "EnterFrame"); + gParentFrames.set(pointToString(p), steps[0]); + } + } + }, + scanCheckpoint: checkpoint, }); - for (const p of steps) { - if (p.position.frameIndex == point.position.frameIndex) { - gFrameSteps.set(pointToString(p), steps); - } else { - assert(p.position.kind == "EnterFrame"); - gParentFrames.set(pointToString(p), steps[0]); - } + steps = findExisting(); + assert(steps); + return steps; + + function findExisting() { + return gFrameSteps.get(pointToString(point)); } - - resolve(steps); - return steps; } async function findParentFrameEntryPoint(point) { assert(point.position.kind == "EnterFrame"); assert(point.position.frameIndex > 0); - const { promise, resolve } = gParentFrames.get(pointToString(point)); - if (!resolve) { - return promise; + let parentPoint = findExisting(); + if (parentPoint) { + return parentPoint; } const checkpoint = getSavedCheckpoint(point.checkpoint); - const child = await scanRecording(checkpoint); - const { parentPoint } = await child.sendManifest({ - kind: "findParentFrameEntryPoint", - point, + await scanRecording(checkpoint); + await sendAsyncManifest({ + shouldSkip: () => !!findExisting(), + contents: () => ({ kind: "findParentFrameEntryPoint", point }), + onFinished: (_, { parentPoint }) => { + gParentFrames.set(pointToString(point), parentPoint); + }, + scanCheckpoint: checkpoint, }); - resolve(parentPoint); + parentPoint = findExisting(); + assert(parentPoint); return parentPoint; + + function findExisting() { + return gParentFrames.get(pointToString(point)); + } } //////////////////////////////////////////////////////////////////////////////// // Pause Data //////////////////////////////////////////////////////////////////////////////// const gPauseData = new Map(); const gQueuedPauseData = new Set(); // Cached points indicate messages where we have gathered pause data. These are // shown differently in the UI. const gCachedPoints = new Map(); -async function queuePauseData({ point, trackCached }) { +async function queuePauseData({ + point, + snapshot, + trackCached, + shouldSkip: shouldSkipCallback, +}) { if (gQueuedPauseData.has(pointToString(point))) { return; } gQueuedPauseData.add(pointToString(point)); await waitForFlushed(point.checkpoint); - const child = await ensureLeafChild(point, Priority.LOW); - const data = await child.sendManifest({ kind: "getPauseData" }); + await sendAsyncManifest({ + shouldSkip() { + if (maybeGetPauseData(point)) { + return true; + } - addPauseData(point, data, trackCached); - terminateRunningLeafChild(child); + return shouldSkipCallback && shouldSkipCallback(); + }, + contents() { + return { kind: "getPauseData" }; + }, + onFinished(child, data) { + if (!data.restoredSnapshot) { + addPauseData(point, data, trackCached); + child.divergedFromRecording = true; + } + }, + point, + snapshot, + expectedDuration: 250, + priority: Priority.LOW, + mightRewind: true, + }); } function addPauseData(point, data, trackCached) { if (data.paintData) { // Atomize paint data strings to ensure that we don't store redundant // strings for execution points with the same paint data. data.paintData = RecordReplayControl.atomize(data.paintData); } @@ -983,80 +1077,132 @@ function cachedPoints() { //////////////////////////////////////////////////////////////////////////////// // The pause mode classifies the current state of the debugger. const PauseModes = { // The main child is the active child, and is either paused or actively // recording. gPausePoint is the last point the main child reached. RUNNING: "RUNNING", - // gActiveChild is a replaying child paused or being taken to gPausePoint. + // gActiveChild is a replaying child paused at gPausePoint. PAUSED: "PAUSED", + // gActiveChild is a replaying child being taken to gPausePoint. The debugger + // is considered to be paused, but interacting with the child must wait until + // it arrives. + ARRIVING: "ARRIVING", + // gActiveChild is null, and we are looking for the last breakpoint hit prior // to or following gPausePoint, at which we will pause. RESUMING_BACKWARD: "RESUMING_BACKWARD", RESUMING_FORWARD: "RESUMING_FORWARD", }; // Current pause mode. let gPauseMode = PauseModes.RUNNING; -// In PAUSED mode, the point we are paused at or sending the active child to. +// In PAUSED or ARRIVING modes, the point we are paused at or sending the active +// child to. let gPausePoint = null; // In PAUSED mode, any debugger requests that have been sent to the child. +// In ARRIVING mode, the requests must be sent once the child arrives. const gDebuggerRequests = []; function addDebuggerRequest(request) { gDebuggerRequests.push({ request, stack: Error().stack, }); } function setPauseState(mode, point, child) { assert(mode); - const idString = child ? ` ${child.id}` : ""; + const idString = child ? ` #${child.id}` : ""; dumpv(`SetPauseState ${mode} ${JSON.stringify(point)}${idString}`); - if (gActiveChild && gActiveChild != gMainChild && gActiveChild != child) { - terminateRunningLeafChild(gActiveChild); - } - gPauseMode = mode; gPausePoint = point; gActiveChild = child; - if (mode == PauseModes.PAUSED) { + if (mode == PauseModes.ARRIVING) { simulateNearbyNavigation(); } + + pokeChildrenSoon(); } // Mark the debugger as paused, and asynchronously send a child to the pause // point. function setReplayingPauseTarget(point) { assert(!gDebuggerRequests.length); - - const child = newLeafChild(point); - setPauseState(PauseModes.PAUSED, point, child); + setPauseState(PauseModes.ARRIVING, point, closestChild(point.checkpoint)); gDebugger._onPause(); + findFrameSteps(point); } -// Synchronously bring a new leaf child to the current pause point. -function bringNewReplayingChildToPausePoint() { - const child = newLeafChild(gPausePoint); - setPauseState(PauseModes.PAUSED, gPausePoint, child); +function sendActiveChildToPausePoint() { + assert(gActiveChild.paused); + + switch (gPauseMode) { + case PauseModes.PAUSED: + assert(pointEquals(gActiveChild.pausePoint(), gPausePoint)); + return; + + case PauseModes.ARRIVING: + if (pointEquals(gActiveChild.pausePoint(), gPausePoint)) { + setPauseState(PauseModes.PAUSED, gPausePoint, gActiveChild); - child.sendManifest({ - kind: "batchDebuggerRequest", - requests: gDebuggerRequests.map(r => r.request), - }); + // Send any debugger requests the child is considered to have received. + if (gDebuggerRequests.length) { + const child = gActiveChild; + child.sendManifest({ + contents: { + kind: "batchDebuggerRequest", + requests: gDebuggerRequests.map(r => r.request), + }, + onFinished(finishData) { + if (finishData.divergedFromRecording) { + child.divergedFromRecording = true; + } + }, + }); + } + } else { + maybeReachPoint(gActiveChild, gPausePoint); + } + return; + + default: + ThrowError(`Unexpected pause mode: ${gPauseMode}`); + } +} + +function waitUntilPauseFinishes() { + assert(gActiveChild); + + if (gActiveChild == gMainChild) { + gActiveChild.waitUntilPaused(true); + return; + } + + while (gPauseMode != PauseModes.PAUSED) { + gActiveChild.waitUntilPaused(); + pokeChild(gActiveChild); + } + + gActiveChild.waitUntilPaused(); +} + +// Synchronously send a child to the specific point and pause. +function pauseReplayingChild(child, point) { + setPauseState(PauseModes.ARRIVING, point, child); + waitUntilPauseFinishes(); } // Find the point where the debugger should pause when running forward or // backward from a point and using a given set of breakpoints. Returns null if // there is no point to pause at before hitting the beginning or end of the // recording. async function resumeTarget(point, forward, breakpoints) { let startCheckpoint = point.checkpoint; @@ -1065,17 +1211,17 @@ async function resumeTarget(point, forwa if (startCheckpoint == InvalidCheckpointId) { return null; } } startCheckpoint = getSavedCheckpoint(startCheckpoint); let checkpoint = startCheckpoint; for (; ; forward ? checkpoint++ : checkpoint--) { - if ([InvalidCheckpointId, gLastSavedCheckpoint].includes(checkpoint)) { + if ([InvalidCheckpointId, gLastFlushCheckpoint].includes(checkpoint)) { return null; } if (!gCheckpoints[checkpoint].saved) { continue; } const hits = []; @@ -1164,16 +1310,17 @@ function resume(forward) { return; } setPauseState( forward ? PauseModes.RESUMING_FORWARD : PauseModes.RESUMING_BACKWARD, gPausePoint, null ); finishResume(); + pokeChildren(); } // Synchronously bring the active child to the specified execution point. function timeWarp(point) { gDebuggerRequests.length = 0; setReplayingPauseTarget(point); Services.cpmm.sendAsyncMessage("TimeWarpFinished"); } @@ -1184,47 +1331,71 @@ function timeWarp(point) { // The maximum number of crashes which we can recover from. const MaxCrashes = 4; // How many child processes have crashed. let gNumCrashes = 0; // eslint-disable-next-line no-unused-vars -function ChildCrashed(rootId, forkId) { - const id = processId(rootId, forkId); +function ChildCrashed(id) { dumpv(`Child Crashed: ${id}`); // In principle we can recover when any replaying child process crashes. // For simplicity, there are some cases where we don't yet try to recover if // a replaying process crashes. // // - It is the main child, and running forward through the recording. While it // could crash here just as easily as any other replaying process, any crash // will happen early on and won't interrupt a long-running debugger session. // // - It is the active child, and is paused at gPausePoint. It must have // crashed while processing a debugger request, which is unlikely. - const child = gChildren.get(id); + const child = gReplayingChildren[id]; if ( !child || !child.manifest || (child == gActiveChild && gPauseMode == PauseModes.PAUSED) ) { ThrowError("Cannot recover from crashed child"); } if (++gNumCrashes > MaxCrashes) { ThrowError("Too many crashes"); } - child.terminate(); + delete gReplayingChildren[id]; + child.crashed = true; + + // Spawn a new child to replace the one which just crashed. + const newChild = spawnReplayingChild(); + pokeChildSoon(newChild); + + // The new child should save the same checkpoints as the old one. + for (const checkpoint of child.savedCheckpoints) { + newChild.addSavedCheckpoint(checkpoint); + } - // Crash recovery does not yet deal with fork based rewinding. - ThrowError("Fork based crash recovery NYI"); + // Any regions the old child scanned need to be rescanned. + for (const checkpoint of child.scannedCheckpoints) { + scanRecording(checkpoint); + } + + // Requeue any async manifest the child was processing. + if (child.asyncManifest) { + sendAsyncManifest(child.asyncManifest); + } + + // If the active child crashed while heading to the pause point, pick another + // child to head to the pause point. + if (child == gActiveChild) { + assert(gPauseMode == PauseModes.ARRIVING); + gActiveChild = closestChild(gPausePoint.checkpoint); + pokeChildSoon(gActiveChild); + } } //////////////////////////////////////////////////////////////////////////////// // Simulated Navigation //////////////////////////////////////////////////////////////////////////////// // When the user is paused somewhere in the recording, we want to obtain pause // data for points which they can get to via the UI. This includes all messages @@ -1262,17 +1433,16 @@ async function findFrameEntryPoint(point // when a frame was pushed on the stack. Instead, find the first point in the // frame which is a breakpoint site. assert(point.position.kind == "EnterFrame"); const steps = await findFrameSteps(point); assert(pointEquals(steps[0], point)); return steps[1]; } -// eslint-disable-next-line complexity async function simulateSteppingNavigation(point, count, frameCount, last) { if (!count || !point.position) { return; } const { script } = point.position; const dbgScript = gDebugger._getScript(script); const steps = await findFrameSteps(point); @@ -1309,19 +1479,17 @@ async function simulateSteppingNavigatio if (p.position.script != script) { // Currently, the debugger will stop at the EnterFrame site and then run // forward to the first breakpoint site before pausing. We need pause // data from both points, unfortunately. queuePauseData({ point: p, snapshot: steps[0] }); const np = await findFrameEntryPoint(p); queuePauseData({ point: np, snapshot: steps[0] }); - if (canFindHits(np.position)) { - findHits(getSavedCheckpoint(point.checkpoint), np.position); - } + findHits(getSavedCheckpoint(point.checkpoint), np.position); simulateSteppingNavigation(np, count - 1, frameCount - 1, "stepIn"); break; } } } if ( frameCount && @@ -1336,19 +1504,17 @@ async function simulateSteppingNavigatio const parentEntryPoint = await findParentFrameEntryPoint(steps[0]); const parentSteps = await findFrameSteps(parentEntryPoint); for (let i = 0; i < parentSteps.length; i++) { const p = parentSteps[i]; if (pointPrecedes(point, p)) { // When stepping out we will stop at the next breakpoint site, // and do not need a point that is a stepping target. queuePauseData({ point: p, snapshot: parentSteps[0] }); - if (canFindHits(p.position)) { - findHits(getSavedCheckpoint(point.checkpoint), p.position); - } + findHits(getSavedCheckpoint(point.checkpoint), p.position); simulateSteppingNavigation(p, count - 1, frameCount - 1, "stepOut"); break; } } } function isStepOverTarget(p) { const { kind, offset } = p.position; @@ -1389,29 +1555,39 @@ async function evaluateLogpoint({ point, text, condition, callback, snapshot, fast, }) { assert(point); - - const child = await ensureLeafChild(point, Priority.MEDIUM); - const { result, resultData } = await child.sendManifest({ - kind: "hitLogpoint", - text, - condition, - fast, + await sendAsyncManifest({ + shouldSkip: () => false, + contents() { + return { kind: "hitLogpoint", text, condition, fast }; + }, + onFinished(child, { result, resultData, restoredSnapshot }) { + if (restoredSnapshot) { + callback(point, ["Recording divergence evaluating logpoint"]); + } else { + if (result) { + callback(point, result, resultData); + } + if (!fast) { + child.divergedFromRecording = true; + } + } + }, + point, + snapshot, + expectedDuration: 250, + priority: Priority.MEDIUM, + mightRewind: true, }); - terminateRunningLeafChild(child); - - if (result) { - callback(point, result, resultData); - } } // Asynchronously invoke a logpoint's callback with all results from hitting // the logpoint in the range of the recording covered by checkpoint. async function findLogpointHits( checkpoint, { position, text, condition, messageCallback, validCallback } ) { @@ -1473,35 +1649,38 @@ async function findLogpointHits( //////////////////////////////////////////////////////////////////////////////// // Event Breakpoints //////////////////////////////////////////////////////////////////////////////// // Event kinds which will be logged. For now this set can only grow, as we don't // have a way to remove old event logpoints from the client. const gLoggedEvents = []; -const gEventFrameEntryPoints = new PromiseMap(); +const gEventFrameEntryPoints = new Map(); async function findEventFrameEntry(checkpoint, progress) { - const { promise, resolve } = gEventFrameEntryPoints.get(progress); - if (!resolve) { - return promise; + if (gEventFrameEntryPoints.has(progress)) { + return gEventFrameEntryPoints.get(progress); } const savedCheckpoint = getSavedCheckpoint(checkpoint); - const child = await scanRecording(savedCheckpoint); - const { rv } = await child.sendManifest({ - kind: "findEventFrameEntry", - checkpoint, - progress, + await scanRecording(savedCheckpoint); + await sendAsyncManifest({ + shouldSkip: () => gEventFrameEntryPoints.has(progress), + contents: () => ({ kind: "findEventFrameEntry", checkpoint, progress }), + onFinished: (_, { rv }) => gEventFrameEntryPoints.set(progress, rv), + scanCheckpoint: savedCheckpoint, }); - const point = await findFrameEntryPoint(rv); - resolve(point); - return point; + const point = gEventFrameEntryPoints.get(progress); + if (!point) { + return null; + } + + return findFrameEntryPoint(point); } async function findEventLogpointHits(checkpoint, event, callback) { for (const info of getCheckpointInfo(checkpoint).events) { if (info.event == event) { const point = await findEventFrameEntry(info.checkpoint, info.progress); if (point) { callback(point, ["Loading..."]); @@ -1584,96 +1763,100 @@ function handleResumeManifestResponse({ // If necessary, continue executing in the main child. function maybeResumeRecording() { if (gActiveChild != gMainChild) { return; } if ( - !gLastSavedCheckpoint || - timeSinceCheckpoint(gLastSavedCheckpoint) >= FlushMs + !gLastFlushCheckpoint || + timeSinceCheckpoint(gLastFlushCheckpoint) >= FlushMs ) { ensureFlushed(); } const checkpoint = gMainChild.pausePoint().checkpoint; if (!gMainChild.recording && checkpoint == gRecordingEndpoint) { ensureFlushed(); Services.cpmm.sendAsyncMessage("HitRecordingEndpoint"); if (gDebugger) { gDebugger._hitRecordingBoundary(); } return; } - gMainChild.sendManifest( - { + gMainChild.sendManifest({ + contents: { kind: "resume", breakpoints: gBreakpoints, - pauseOnDebuggerStatement: !!gDebugger, + pauseOnDebuggerStatement: true, }, - response => { + onFinished(response) { handleResumeManifestResponse(response); gPausePoint = gMainChild.pausePoint(); if (gDebugger) { gDebugger._onPause(); } else { Services.tm.dispatchToMainThread(maybeResumeRecording); } - } - ); + }, + }); } // Resolve callbacks for any promises waiting on the recording to be flushed. const gFlushWaiters = []; function waitForFlushed(checkpoint) { - if (checkpoint < gLastSavedCheckpoint) { + if (checkpoint < gLastFlushCheckpoint) { return undefined; } return new Promise(resolve => { gFlushWaiters.push(resolve); }); } let gLastFlushTime = Date.now(); // If necessary, synchronously flush the recording to disk. function ensureFlushed() { gMainChild.waitUntilPaused(true); gLastFlushTime = Date.now(); - if (gLastSavedCheckpoint == gMainChild.pauseCheckpoint()) { + if (gLastFlushCheckpoint == gMainChild.pauseCheckpoint()) { return; } if (gMainChild.recording) { - gMainChild.sendManifest({ kind: "flushRecording" }); + gMainChild.sendManifest({ + contents: { kind: "flushRecording" }, + onFinished() {}, + }); gMainChild.waitUntilPaused(); } + const oldFlushCheckpoint = gLastFlushCheckpoint || FirstCheckpointId; + gLastFlushCheckpoint = gMainChild.pauseCheckpoint(); + // We now have a usable recording for replaying children, so spawn them if // necessary. - if (!gTrunkChild) { - spawnTrunkChild(); + if (gReplayingChildren.length == 0) { + spawnReplayingChildren(); } - const oldSavedCheckpoint = gLastSavedCheckpoint || FirstCheckpointId; - addSavedCheckpoint(gMainChild.pauseCheckpoint()); - // Checkpoints where the recording was flushed to disk are saved. This allows // the recording to be scanned as soon as it has been flushed. + addSavedCheckpoint(gLastFlushCheckpoint); // Flushing creates a new region of the recording for replaying children // to scan. forSavedCheckpointsInRange( - oldSavedCheckpoint, - gLastSavedCheckpoint, + oldFlushCheckpoint, + gLastFlushCheckpoint, checkpoint => { scanRecording(checkpoint); // Scan for breakpoint and logpoint hits in this new region. gBreakpoints.forEach(position => findBreakpointHits(checkpoint, position) ); gLogpoints.forEach(logpoint => findLogpointHits(checkpoint, logpoint)); @@ -1682,36 +1865,38 @@ function ensureFlushed() { } } ); for (const waiter of gFlushWaiters) { waiter(); } gFlushWaiters.length = 0; + + pokeChildren(); } const CheckFlushMs = 1000; setInterval(() => { // Periodically make sure the recording is flushed. If the tab is sitting // idle we still want to keep the recording up to date. const elapsed = Date.now() - gLastFlushTime; if ( elapsed > CheckFlushMs && gMainChild.lastPausePoint && - gMainChild.lastPausePoint.checkpoint != gLastSavedCheckpoint + gMainChild.lastPausePoint.checkpoint != gLastFlushCheckpoint ) { ensureFlushed(); } // Ping children that are executing manifests to ensure they haven't hanged. - for (const child of gUnpausedChildren) { - if (!child.recording) { - child.maybePing(); + for (const child of gReplayingChildren) { + if (child) { + RecordReplayControl.maybePing(child.id); } } }, 1000); // eslint-disable-next-line no-unused-vars function BeforeSaveRecording() { if (gActiveChild == gMainChild) { // The recording might not be up to date, ensure it flushes after pausing. @@ -1721,39 +1906,100 @@ function BeforeSaveRecording() { // eslint-disable-next-line no-unused-vars function AfterSaveRecording() { Services.cpmm.sendAsyncMessage("SaveRecordingFinished"); } let gRecordingEndpoint; -async function setMainChild() { +function setMainChild() { assert(!gMainChild.recording); - const { endpoint } = await gMainChild.sendManifest({ kind: "setMainChild" }); - gRecordingEndpoint = endpoint; - Services.tm.dispatchToMainThread(maybeResumeRecording); + gMainChild.sendManifest({ + contents: { kind: "setMainChild" }, + onFinished({ endpoint }) { + gRecordingEndpoint = endpoint; + Services.tm.dispatchToMainThread(maybeResumeRecording); + }, + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Child Management +//////////////////////////////////////////////////////////////////////////////// + +function spawnReplayingChild() { + const id = RecordReplayControl.spawnReplayingChild(); + const child = new ChildProcess(id, false); + gReplayingChildren[id] = child; + return child; +} + +// How many replaying children to spawn. This should be a pref instead... +const NumReplayingChildren = 4; + +function spawnReplayingChildren() { + if (RecordReplayControl.canRewind()) { + for (let i = 0; i < NumReplayingChildren; i++) { + spawnReplayingChild(); + } + } + addSavedCheckpoint(FirstCheckpointId); +} + +// eslint-disable-next-line no-unused-vars +function Initialize(recordingChildId) { + try { + if (recordingChildId != undefined) { + gMainChild = new ChildProcess(recordingChildId, true); + } else { + // If there is no recording child, we have now initialized enough state + // that we can start spawning replaying children. + const id = RecordReplayControl.spawnReplayingChild(); + gMainChild = new ChildProcess(id, false); + spawnReplayingChildren(); + } + gActiveChild = gMainChild; + return gControl; + } catch (e) { + dump(`ERROR: Initialize threw exception: ${e}\n`); + } +} + +// eslint-disable-next-line no-unused-vars +function ManifestFinished(id, response) { + try { + dumpv(`ManifestFinished #${id} ${stringify(response)}`); + const child = lookupChild(id); + if (child) { + child.manifestFinished(response); + } else { + // Ignore messages from child processes that we have marked as crashed. + } + } catch (e) { + dump(`ERROR: ManifestFinished threw exception: ${e} ${e.stack}\n`); + } } //////////////////////////////////////////////////////////////////////////////// // Debugger Operations //////////////////////////////////////////////////////////////////////////////// // From the debugger's perspective, there is a single target to interact with, // represented by gActiveChild. The details of the various children the control // system is managing are hidden away. This object describes the interface which // the debugger uses to access the control system. const gControl = { // Get the current point where the active child is paused, or null. pausePoint() { if (gActiveChild && gActiveChild == gMainChild) { return gActiveChild.paused ? gActiveChild.pausePoint() : null; } - if (gPauseMode == PauseModes.PAUSED) { + if (gPauseMode == PauseModes.PAUSED || gPauseMode == PauseModes.ARRIVING) { return gPausePoint; } return null; }, lastPausePoint() { return gPausePoint; }, @@ -1829,66 +2075,79 @@ const gControl = { maybeSwitchToReplayingChild() { assert(gControl.pausePoint()); if (gActiveChild == gMainChild && RecordReplayControl.canRewind()) { const point = gActiveChild.pausePoint(); if (point.position) { // We can only flush the recording at checkpoints, so we need to send the // main child forward and pause/flush ASAP. - gMainChild.sendManifest( - { + gMainChild.sendManifest({ + contents: { kind: "resume", breakpoints: [], pauseOnDebuggerStatement: false, }, - handleResumeManifestResponse - ); + onFinished(response) { + handleResumeManifestResponse(response); + }, + }); gMainChild.waitUntilPaused(true); } ensureFlushed(); - bringNewReplayingChildToPausePoint(); + const child = closestChild(point); + pauseReplayingChild(child, point); } }, // Synchronously send a debugger request to a paused active child, returning // the response. sendRequest(request) { + waitUntilPauseFinishes(); + let data; - gActiveChild.sendManifest( - { kind: "debuggerRequest", request }, - finishData => { + gActiveChild.sendManifest({ + contents: { kind: "debuggerRequest", request }, + onFinished(finishData) { data = finishData; - } - ); - while (!data) { - gActiveChild.waitUntilPaused(); + }, + mightRewind: true, + }); + gActiveChild.waitUntilPaused(); + + if (data.restoredSnapshot) { + // The child had an unhandled recording diverge and restored an earlier + // checkpoint. Restore the child to the point it should be paused at and + // fill its paused state back in by resending earlier debugger requests. + pauseReplayingChild(gActiveChild, gPausePoint); + return { unhandledDivergence: true }; } - if (data.unhandledDivergence) { - bringNewReplayingChildToPausePoint(); - } else { - addDebuggerRequest(request); + if (data.divergedFromRecording) { + // Remember whether the child diverged from the recording. + gActiveChild.divergedFromRecording = true; } + + addDebuggerRequest(request); return data.response; }, // Synchronously send a debugger request to the main child, which will always // be at the end of the recording and can receive requests even when the // active child is not currently paused. sendRequestMainChild(request) { gMainChild.waitUntilPaused(true); let data; - gMainChild.sendManifest( - { kind: "debuggerRequest", request }, - finishData => { + gMainChild.sendManifest({ + contents: { kind: "debuggerRequest", request }, + onFinished(finishData) { data = finishData; - } - ); + }, + }); gMainChild.waitUntilPaused(); assert(!data.divergedFromRecording); return data.response; }, resume, timeWarp, @@ -1905,35 +2164,31 @@ const gControl = { unscannedRegions, cachedPoints, debuggerRequests() { return gDebuggerRequests; }, getPauseDataAndRepaint() { - // Use cached pause data if possible, which we can immediately return - // without waiting for the child to arrive at the pause point. - if (!gDebuggerRequests.length) { + // If the child has not arrived at the pause point yet, see if there is + // cached pause data for this point already which we can immediately return. + if (gPauseMode == PauseModes.ARRIVING && !gDebuggerRequests.length) { const data = maybeGetPauseData(gPausePoint); if (data) { // After the child pauses, it will need to generate the pause data so // that any referenced objects will be instantiated. - gActiveChild.sendManifest({ - kind: "debuggerRequest", - request: { type: "pauseData" }, - }); addDebuggerRequest({ type: "pauseData" }); RecordReplayControl.hadRepaint(data.paintData); return data; } } gControl.maybeSwitchToReplayingChild(); const data = gControl.sendRequest({ type: "pauseData" }); - if (!data) { + if (data.unhandledDivergence) { RecordReplayControl.clearGraphics(); } else { addPauseData(gPausePoint, data, /* trackCached */ true); if (data.paintData) { RecordReplayControl.hadRepaint(data.paintData); } } return data; @@ -1953,16 +2208,81 @@ const gControl = { const { debuggerStatements } = getCheckpointInfo(checkpoint); return pointArrayIncludes(debuggerStatements, point); } return false; }, }; /////////////////////////////////////////////////////////////////////////////// +// Statistics +/////////////////////////////////////////////////////////////////////////////// + +let lastDumpTime = Date.now(); + +function maybeDumpStatistics() { + const now = Date.now(); + if (now - lastDumpTime < 5000) { + return; + } + lastDumpTime = now; + + let delayTotal = 0; + let unscannedTotal = 0; + let timeTotal = 0; + let scanDurationTotal = 0; + + forAllSavedCheckpoints(checkpoint => { + const checkpointTime = timeForSavedCheckpoint(checkpoint); + const info = getCheckpointInfo(checkpoint); + + timeTotal += checkpointTime; + if (info.scanTime) { + delayTotal += checkpointTime * (info.scanTime - info.assignTime); + scanDurationTotal += info.scanDuration; + } else { + unscannedTotal += checkpointTime; + } + }); + + const memoryUsage = []; + let totalSaved = 0; + + for (const child of gReplayingChildren) { + if (!child) { + continue; + } + totalSaved += child.savedCheckpoints.size; + if (!child.lastMemoryUsage) { + continue; + } + for (const [name, value] of Object.entries(child.lastMemoryUsage)) { + if (!memoryUsage[name]) { + memoryUsage[name] = 0; + } + memoryUsage[name] += value; + } + } + + const delay = delayTotal / timeTotal; + const overhead = scanDurationTotal / (timeTotal - unscannedTotal); + + dumpv(`Statistics:`); + dumpv(`Total recording time: ${timeTotal}`); + dumpv(`Unscanned fraction: ${unscannedTotal / timeTotal}`); + dumpv(`Average scan delay: ${delay}`); + dumpv(`Average scanning overhead: ${overhead}`); + + dumpv(`Saved checkpoints: ${totalSaved}`); + for (const [name, value] of Object.entries(memoryUsage)) { + dumpv(`Memory ${name}: ${value}`); + } +} + +/////////////////////////////////////////////////////////////////////////////// // Utilities /////////////////////////////////////////////////////////////////////////////// function getPreference(name) { return Services.prefs.getBoolPref(`devtools.recordreplay.${name}`); } const loggingFullEnabled = getPreference("loggingFull"); @@ -2010,10 +2330,9 @@ function stringify(object) { // eslint-disable-next-line no-unused-vars var EXPORTED_SYMBOLS = [ "Initialize", "ConnectDebugger", "ManifestFinished", "BeforeSaveRecording", "AfterSaveRecording", "ChildCrashed", - "PingResponse", ];
--- a/devtools/server/actors/replay/moz.build +++ b/devtools/server/actors/replay/moz.build @@ -4,25 +4,22 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. DIRS += [ 'utils' ] DevToolsModules( - 'connection-worker.js', - 'connection.js', 'control.js', 'debugger.js', 'graphics.js', 'inspector.js', 'replay.js', ) XPIDL_MODULE = 'devtools_rr' XPIDL_SOURCES = [ - 'rrIConnection.idl', 'rrIControl.idl', 'rrIGraphics.idl', 'rrIReplay.idl', ]
--- a/devtools/server/actors/replay/replay.js +++ b/devtools/server/actors/replay/replay.js @@ -3,19 +3,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-disable spaced-comment, brace-style, indent-legacy, consistent-return */ // This file defines the logic that runs in the record/replay devtools sandbox. // This code is loaded into all recording/replaying processes, and responds to // requests and other instructions from the middleman via the exported symbols // defined at the end of this file. // -// In the process of handling the middleman's requests, state in this file may -// vary between recording and replaying, or between different replays. -// As a result, we have to be very careful about performing operations +// Like all other JavaScript in the recording/replaying process, this code's +// state is included in memory snapshots and reset when checkpoints are +// restored. In the process of handling the middleman's requests, however, its +// state may vary between recording and replaying, or between different +// replays. As a result, we have to be very careful about performing operations // that might interact with the recording --- any time we enter the debuggee // and evaluate code or perform other operations. // The divergeFromRecording function should be used at any point where such // interactions might occur. // eslint-disable spaced-comment "use strict"; @@ -38,16 +40,17 @@ Cu.evalInSandbox( const { Debugger, RecordReplayControl, Services, InspectorUtils, CSSRule, pointPrecedes, pointEquals, + pointArrayIncludes, findClosestPoint, } = sandbox; const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); const jsmScope = require("resource://devtools/shared/Loader.jsm"); const { DebuggerNotificationObserver } = Cu.getGlobalForObject(jsmScope); const { @@ -165,26 +168,46 @@ function scriptFrameForIndex(index) { } return frame; } function isNonNullObject(obj) { return obj && (typeof obj == "object" || typeof obj == "function"); } +function getMemoryUsage() { + const memoryKinds = { + Generic: [1], + Snapshots: [2, 3, 4, 5, 6, 7], + ScriptHits: [8], + }; + + const rv = {}; + for (const [name, kinds] of Object.entries(memoryKinds)) { + let total = 0; + kinds.forEach(kind => { + total += RecordReplayControl.memoryUsage(kind); + }); + rv[name] = total; + } + return rv; +} + /////////////////////////////////////////////////////////////////////////////// // Persistent Script State /////////////////////////////////////////////////////////////////////////////// // Association between Debugger.Scripts and their IDs. The indices that this -// table assigns to scripts are stable across the entire recording. In debuggee -// time, this table only grows (there is no way to remove entries). -// Scripts created for debugger activity (e.g. eval) are ignored, and off thread -// compilation is disabled, so this table acquires the same scripts in the same -// order as we roll back and run forward in the recording. +// table assigns to scripts are stable across the entire recording, even though +// this table (like all JS state) is included in snapshots, rolled back when +// rewinding, and so forth. In debuggee time, this table only grows (there is +// no way to remove entries). Scripts created for debugger activity (e.g. eval) +// are ignored, and off thread compilation is disabled, so this table acquires +// the same scripts in the same order as we roll back and run forward in the +// recording. const gScripts = new IdMap(); // Any scripts added since the last checkpoint. const gNewScripts = []; function addScript(script) { const id = gScripts.add(script); script.setInstrumentationId(id); @@ -929,38 +952,42 @@ let gManifest = { kind: "primordial" }; let gManifestStartTime; // Points of any debugger statements that need to be flushed to the middleman. const gNewDebuggerStatements = []; // Whether to pause on debugger statements when running forward. let gPauseOnDebuggerStatement = false; -function ensureRunToPointPositionHandlers({ endpoint }) { +function ensureRunToPointPositionHandlers({ endpoint, snapshotPoints }) { if (gLastCheckpoint == endpoint.checkpoint) { assert(endpoint.position); ensurePositionHandler(endpoint.position); } + snapshotPoints.forEach(snapshot => { + if (gLastCheckpoint == snapshot.checkpoint && snapshot.position) { + ensurePositionHandler(snapshot.position); + } + }); } // Handlers that run when a manifest is first received. This must be specified // for all manifests. const gManifestStartHandlers = { resume({ breakpoints, pauseOnDebuggerStatement }) { RecordReplayControl.resumeExecution(); breakpoints.forEach(ensurePositionHandler); gPauseOnDebuggerStatement = pauseOnDebuggerStatement; dbg.onDebuggerStatement = debuggerStatementHit; }, - fork({ id }) { - const point = currentScriptedExecutionPoint() || currentExecutionPoint(); - RecordReplayControl.fork(id); - RecordReplayControl.manifestFinished({ point }); + restoreSnapshot({ numSnapshots }) { + RecordReplayControl.restoreSnapshot(numSnapshots); + throwError("Unreachable!"); }, runToPoint(manifest) { ensureRunToPointPositionHandlers(manifest); RecordReplayControl.resumeExecution(); }, scanRecording() { @@ -1085,17 +1112,17 @@ function currentExecutionPoint(position) const checkpoint = gLastCheckpoint; const progress = RecordReplayControl.progressCounter(); return { checkpoint, progress, position }; } function currentScriptedExecutionPoint() { const numFrames = countScriptFrames(); if (!numFrames) { - return undefined; + return null; } const index = numFrames - 1; const frame = scriptFrameForIndex(index); return currentExecutionPoint({ kind: "OnStep", script: gScripts.getId(frame.script), offset: frame.offset, @@ -1120,40 +1147,50 @@ function finishResume(point) { // Handlers that run after a checkpoint is reached to see if the manifest has // finished. This does not need to be specified for all manifests. const gManifestFinishedAfterCheckpointHandlers = { primordial(_, point) { // The primordial manifest runs forward to the first checkpoint, saves it, // and then finishes. assert(point.checkpoint == FirstCheckpointId); + if (!newSnapshot(point)) { + return; + } RecordReplayControl.manifestFinished({ point }); }, resume(_, point) { clearPositionHandlers(); finishResume(point); }, - runToPoint({ endpoint, flushExternalCalls }, point) { + runToPoint({ endpoint, snapshotPoints }, point) { assert(endpoint.checkpoint >= point.checkpoint); + if (pointArrayIncludes(snapshotPoints, point) && !newSnapshot(point)) { + return; + } if (!endpoint.position && point.checkpoint == endpoint.checkpoint) { - if (flushExternalCalls) { - RecordReplayControl.flushExternalCalls(); - } RecordReplayControl.manifestFinished({ point }); } }, - scanRecording({ endpoint }, point) { + scanRecording({ endpoint, snapshotPoints }, point) { stopScanningAllScripts(); + if (pointArrayIncludes(snapshotPoints, point) && !newSnapshot(point)) { + return; + } if (point.checkpoint == endpoint.checkpoint) { const duration = RecordReplayControl.currentExecutionTime() - gManifestStartTime; - RecordReplayControl.manifestFinished({ point, duration }); + RecordReplayControl.manifestFinished({ + point, + duration, + memoryUsage: getMemoryUsage(), + }); } }, }; // Handlers that run after a checkpoint is reached and before execution resumes. // This does not need to be specified for all manifests. This is specified // separately from gManifestFinishedAfterCheckpointHandlers to ensure that if // we finish a manifest after the checkpoint and then start a new one, that new @@ -1163,17 +1200,17 @@ const gManifestPrepareAfterCheckpointHan runToPoint: ensureRunToPointPositionHandlers, scanRecording({ endpoint }) { assert(!endpoint.position); startScanningAllScripts(); }, }; -function processManifestAfterCheckpoint(point) { +function processManifestAfterCheckpoint(point, restoredSnapshot) { if (gManifestFinishedAfterCheckpointHandlers[gManifest.kind]) { gManifestFinishedAfterCheckpointHandlers[gManifest.kind](gManifest, point); } if (gManifestPrepareAfterCheckpointHandlers[gManifest.kind]) { gManifestPrepareAfterCheckpointHandlers[gManifest.kind](gManifest, point); } } @@ -1202,20 +1239,25 @@ function HitCheckpoint(id) { // Handlers that run after reaching a position watched by ensurePositionHandler. // This must be specified for any manifest that uses ensurePositionHandler. const gManifestPositionHandlers = { resume(manifest, point) { clearPositionHandlers(); finishResume(point); }, - runToPoint({ endpoint, flushExternalCalls }, point) { + runToPoint({ endpoint, snapshotPoints }, point) { + if (pointArrayIncludes(snapshotPoints, point)) { + clearPositionHandlers(); + if (newSnapshot(point)) { + ensureRunToPointPositionHandlers({ endpoint, snapshotPoints }); + } + } if (pointEquals(point, endpoint)) { clearPositionHandlers(); - assert(!flushExternalCalls); RecordReplayControl.manifestFinished({ point }); } }, }; function positionHit(position, frame) { const point = currentExecutionPoint(position); @@ -1232,16 +1274,28 @@ function debuggerStatementHit() { gNewDebuggerStatements.push(point); if (gPauseOnDebuggerStatement) { clearPositionHandlers(); finishResume(point); } } +function newSnapshot(point) { + if (RecordReplayControl.newSnapshot()) { + return true; + } + + // 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. + RecordReplayControl.manifestFinished({ restoredSnapshot: true, point }); + + return false; +} + /////////////////////////////////////////////////////////////////////////////// // Handler Helpers /////////////////////////////////////////////////////////////////////////////// function getScriptData(id) { const script = gScripts.getObject(id); return { id,
deleted file mode 100644 --- a/devtools/server/actors/replay/rrIConnection.idl +++ /dev/null @@ -1,23 +0,0 @@ -/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -// This interface defines the methods used when communicating with remote -// replaying processes over web sockets in the parent process. -[scriptable, uuid(df6d8e96-4cba-4a1d-893a-1ee19e8d8468)] -interface rrIConnection : nsISupports { - // Supply the callback which will be invoked with the connection ID and an - // array buffer when new data is received from a remote process. - void Initialize(in jsval callback); - - // Start a new connection with a new remote replaying process, specifying - // the channel ID the process will use (unique for the middleman this is - // associated with) and returning the globally unique connection ID. - long Connect(in long channelId, in AString address); - - // Send message data through a particular connection. - void SendMessage(in long connectionId, in jsval buf); -};
--- a/devtools/server/actors/replay/rrIControl.idl +++ b/devtools/server/actors/replay/rrIControl.idl @@ -6,15 +6,13 @@ #include "nsISupports.idl" // This interface defines the methods used for calling into control.js in a // middleman process. [scriptable, uuid(c296e7c3-8a27-4fd0-94c2-b6e5126909ba)] interface rrIControl : nsISupports { void Initialize(in jsval recordingChildId); void ConnectDebugger(in jsval replayDebugger); - void ManifestFinished(in long rootId, in long forkId, in jsval response); - void PingResponse(in long rootId, in long forkId, in long pingId, - in long progress); + void ManifestFinished(in long childId, in jsval response); void BeforeSaveRecording(); void AfterSaveRecording(); - void ChildCrashed(in long rootId, in long forkId); + void ChildCrashed(in long childId); };
--- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -1977,52 +1977,37 @@ mozilla::ipc::IPCResult ContentParent::R mozilla::ipc::IPCResult ContentParent::RecvCreateReplayingProcess( const uint32_t& aChannelId) { // We should only get this message from the child if it is recording or // replaying. if (!this->IsRecordingOrReplaying()) { return IPC_FAIL_NO_REASON(this); } - if (recordreplay::parent::UseCloudForReplayingProcesses()) { - recordreplay::parent::CreateReplayingCloudProcess(Pid(), aChannelId); - return IPC_OK(); - } - while (aChannelId >= mReplayingChildren.length()) { if (!mReplayingChildren.append(nullptr)) { return IPC_FAIL_NO_REASON(this); } } if (mReplayingChildren[aChannelId]) { return IPC_FAIL_NO_REASON(this); } std::vector<std::string> extraArgs; recordreplay::parent::GetArgumentsForChildProcess( Pid(), aChannelId, NS_ConvertUTF16toUTF8(mRecordingFile).get(), /* aRecording = */ false, extraArgs); - GeckoChildProcessHost* child = + mReplayingChildren[aChannelId] = new GeckoChildProcessHost(GeckoProcessType_Content); - mReplayingChildren[aChannelId] = child; - if (!child->LaunchAndWaitForProcessHandle(extraArgs)) { + if (!mReplayingChildren[aChannelId]->LaunchAndWaitForProcessHandle( + extraArgs)) { return IPC_FAIL_NO_REASON(this); } - // Replaying processes can fork themselves, and we can get crashes for - // them that correspond with one of those forked processes. When the crash - // reporter tries to read exception time annotations for one of these crashes, - // it hangs because the original replaying process hasn't actually crashed. - // Workaround this by removing the file descriptor for exception time - // annotations in replaying processes, so that the crash reporter will not - // attempt to read them. - ProcessId pid = base::GetProcId(child->GetChildProcessHandle()); - CrashReporter::DeregisterChildCrashAnnotationFileDescriptor(pid); - return IPC_OK(); } mozilla::ipc::IPCResult ContentParent::RecvGenerateReplayCrashReport( const uint32_t& aChannelId) { if (aChannelId >= mReplayingChildren.length()) { return IPC_FAIL(this, "invalid channel ID"); }
--- a/mfbt/RecordReplay.cpp +++ b/mfbt/RecordReplay.cpp @@ -43,18 +43,16 @@ namespace recordreplay { Macro(DefineRecordReplayControlObject, bool, (void* aCx, void* aObj), \ (aCx, aObj)) #define FOR_EACH_INTERFACE_VOID(Macro) \ Macro(InternalBeginOrderedAtomicAccess, (const void* aValue), (aValue)) \ Macro(InternalEndOrderedAtomicAccess, (), ()) \ Macro(InternalBeginPassThroughThreadEvents, (), ()) \ Macro(InternalEndPassThroughThreadEvents, (), ()) \ - Macro(InternalBeginPassThroughThreadEventsWithLocalReplay, (), ()) \ - Macro(InternalEndPassThroughThreadEventsWithLocalReplay, (), ()) \ Macro(InternalBeginDisallowThreadEvents, (), ()) \ Macro(InternalEndDisallowThreadEvents, (), ()) \ Macro(InternalRecordReplayBytes, (void* aData, size_t aSize), \ (aData, aSize)) \ Macro(InternalInvalidateRecording, (const char* aWhy), (aWhy)) \ Macro(InternalDestroyPLDHashTableCallbacks, (const PLDHashTableOps* aOps), \ (aOps)) \ Macro(InternalMovePLDHashTableContents, \ @@ -89,20 +87,17 @@ FOR_EACH_INTERFACE(DECLARE_SYMBOL) FOR_EACH_INTERFACE_VOID(DECLARE_SYMBOL_VOID) #undef DECLARE_SYMBOL #undef DECLARE_SYMBOL_VOID static void* LoadSymbol(const char* aName) { #ifdef ENABLE_RECORD_REPLAY void* rv = dlsym(RTLD_DEFAULT, aName); - if (!rv) { - fprintf(stderr, "Record/Replay LoadSymbol failed: %s\n", aName); - MOZ_CRASH("LoadSymbol"); - } + MOZ_RELEASE_ASSERT(rv); return rv; #else return nullptr; #endif } void Initialize(int aArgc, char* aArgv[]) { // Only initialize if the right command line option was specified.
--- a/mfbt/RecordReplay.h +++ b/mfbt/RecordReplay.h @@ -142,38 +142,16 @@ struct MOZ_RAII AutoEnsurePassThroughThr ~AutoEnsurePassThroughThreadEvents() { if (!mPassedThrough) EndPassThroughThreadEvents(); } private: bool mPassedThrough; }; -// Mark a region where thread events are passed through when locally replaying. -// Replaying processes can run either on a local machine as a content process -// associated with a firefox parent process, or on remote machines in the cloud. -// We want local replaying processes to be able to interact with the system so -// that they can connect with the parent process and e.g. report crashes. -// We also want to avoid such interaction when replaying in the cloud, as there -// won't be a parent process to connect to. Using these methods allows us to -// handle both of these cases without changing the calling code's control flow. -static inline void BeginPassThroughThreadEventsWithLocalReplay(); -static inline void EndPassThroughThreadEventsWithLocalReplay(); - -// RAII class for regions where thread events are passed through when replaying -// locally. -struct MOZ_RAII AutoPassThroughThreadEventsWithLocalReplay { - AutoPassThroughThreadEventsWithLocalReplay() { - BeginPassThroughThreadEventsWithLocalReplay(); - } - ~AutoPassThroughThreadEventsWithLocalReplay() { - EndPassThroughThreadEventsWithLocalReplay(); - } -}; - // Mark a region where thread events are not allowed to occur. The process will // crash immediately if an event does happen. static inline void BeginDisallowThreadEvents(); static inline void EndDisallowThreadEvents(); // Whether events in this thread are disallowed. static inline bool AreThreadEventsDisallowed(); @@ -373,20 +351,16 @@ static inline void NoteContentParse(cons MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginOrderedAtomicAccess, (const void* aValue), (aValue)) MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndOrderedAtomicAccess, (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginPassThroughThreadEvents, (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndPassThroughThreadEvents, (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER(AreThreadEventsPassedThrough, bool, false, (), ()) -MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginPassThroughThreadEventsWithLocalReplay, - (), ()) -MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndPassThroughThreadEventsWithLocalReplay, - (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(BeginDisallowThreadEvents, (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(EndDisallowThreadEvents, (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER(AreThreadEventsDisallowed, bool, false, (), ()) MOZ_MAKE_RECORD_REPLAY_WRAPPER(RecordReplayValue, size_t, aValue, (size_t aValue), (aValue)) MOZ_MAKE_RECORD_REPLAY_WRAPPER_VOID(RecordReplayBytes, (void* aData, size_t aSize), (aData, aSize)) MOZ_MAKE_RECORD_REPLAY_WRAPPER(HasDivergedFromRecording, bool, false, (), ())
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -848,17 +848,16 @@ pref("toolkit.dump.emit", false); #endif pref("devtools.recordreplay.mvp.enabled", false); pref("devtools.recordreplay.allowRepaintFailures", true); pref("devtools.recordreplay.includeSystemScripts", false); pref("devtools.recordreplay.logging", false); pref("devtools.recordreplay.loggingFull", false); pref("devtools.recordreplay.fastLogpoints", false); -pref("devtools.recordreplay.cloudServer", ""); // Preferences for the new performance panel. // This pref configures the base URL for the profiler.firefox.com instance to // use. This is useful so that a developer can change it while working on // profiler.firefox.com, or in tests. This isn't exposed directly to the user. pref("devtools.performance.recording.ui-base-url", "https://profiler.firefox.com"); // Profiler buffer size. It is the maximum number of 8-bytes entries in the
--- a/toolkit/crashreporter/breakpad-client/mac/crash_generation/crash_generation_client.cc +++ b/toolkit/crashreporter/breakpad-client/mac/crash_generation/crash_generation_client.cc @@ -35,24 +35,23 @@ #include "mozilla/recordreplay/ChildIPC.h" namespace google_breakpad { bool CrashGenerationClient::RequestDumpForException( int exception_type, int exception_code, int exception_subcode, - mach_port_t crashing_thread, - mach_port_t crashing_task) { + mach_port_t crashing_thread) { // The server will send a message to this port indicating that it // has finished its work. ReceivePort acknowledge_port; MachSendMessage message(kDumpRequestMessage); - message.AddDescriptor(crashing_task); // crashing task + message.AddDescriptor(mach_task_self()); // this task message.AddDescriptor(crashing_thread); // crashing thread message.AddDescriptor(MACH_PORT_NULL); // handler thread message.AddDescriptor(acknowledge_port.GetPort()); // message receive port ExceptionInfo info; info.exception_type = exception_type; info.exception_code = exception_code; info.exception_subcode = exception_subcode;
--- a/toolkit/crashreporter/breakpad-client/mac/crash_generation/crash_generation_client.h +++ b/toolkit/crashreporter/breakpad-client/mac/crash_generation/crash_generation_client.h @@ -41,21 +41,20 @@ class CrashGenerationClient { } // Request the crash server to generate a dump. // // Return true if the dump was successful; false otherwise. bool RequestDumpForException(int exception_type, int exception_code, int exception_subcode, - mach_port_t crashing_thread, - mach_port_t crashing_task); + mach_port_t crashing_thread); bool RequestDump() { - return RequestDumpForException(0, 0, 0, MACH_PORT_NULL, mach_task_self()); + return RequestDumpForException(0, 0, 0, MACH_PORT_NULL); } private: MachPortSender sender_; // Prevent copy construction and assignment. CrashGenerationClient(const CrashGenerationClient&); CrashGenerationClient& operator=(const CrashGenerationClient&);
--- a/toolkit/crashreporter/breakpad-client/mac/handler/exception_handler.cc +++ b/toolkit/crashreporter/breakpad-client/mac/handler/exception_handler.cc @@ -366,17 +366,16 @@ static void GetPHCAddrInfo(int64_t excep #endif bool ExceptionHandler::WriteMinidumpWithException( int exception_type, int exception_code, int exception_subcode, breakpad_ucontext_t* task_context, mach_port_t thread_name, - mach_port_t task_name, bool exit_after_write, bool report_current_thread) { bool result = false; #if TARGET_OS_IPHONE // _exit() should never be called on iOS. exit_after_write = false; #endif @@ -401,18 +400,17 @@ bool ExceptionHandler::WriteMinidumpWith // If this is a real exception, give the filter (if any) a chance to // decide if this should be sent. if (filter_ && !filter_(callback_context_, &addr_info)) return false; result = crash_generation_client_->RequestDumpForException( exception_type, exception_code, exception_subcode, - thread_name, - task_name); + thread_name); if (result && exit_after_write) { _exit(exception_type); } } #endif } else { string minidump_id; @@ -562,17 +560,17 @@ void* ExceptionHandler::WaitForMessage(v #else #error architecture not supported #endif } // Write out the dump and save the result for later retrieval self->last_minidump_write_result_ = self->WriteMinidumpWithException(exception_type, exception_code, - 0, NULL, thread, mach_task_self(), + 0, NULL, thread, false, false); #if USE_PROTECTED_ALLOCATIONS if (gBreakpadAllocator) gBreakpadAllocator->Protect(); #endif self->ResumeThreads(); @@ -598,17 +596,17 @@ void* ExceptionHandler::WaitForMessage(v int subcode = 0; if (receive.exception == EXC_BAD_ACCESS && receive.code_count > 1) subcode = receive.code[1]; // Generate the minidump with the exception data. self->WriteMinidumpWithException(receive.exception, receive.code[0], subcode, NULL, receive.thread.name, - mach_task_self(), true, false); + true, false); #if USE_PROTECTED_ALLOCATIONS // This may have become protected again within // WriteMinidumpWithException, but it needs to be unprotected for // UninstallHandler. if (gBreakpadAllocator) gBreakpadAllocator->Unprotect(); #endif @@ -650,37 +648,35 @@ void ExceptionHandler::SignalHandler(int gBreakpadAllocator->Unprotect(); #endif gProtectedData.handler->WriteMinidumpWithException( EXC_SOFTWARE, MD_EXCEPTION_CODE_MAC_ABORT, 0, static_cast<breakpad_ucontext_t*>(uc), mach_thread_self(), - mach_task_self(), true, true); #if USE_PROTECTED_ALLOCATIONS if (gBreakpadAllocator) gBreakpadAllocator->Protect(); #endif } // static bool ExceptionHandler::WriteForwardedExceptionMinidump(int exception_type, int exception_code, int exception_subcode, - mach_port_t thread, - mach_port_t task) + mach_port_t thread) { if (!gProtectedData.handler) { return false; } return gProtectedData.handler->WriteMinidumpWithException(exception_type, exception_code, - exception_subcode, NULL, thread, task, + exception_subcode, NULL, thread, /* exit_after_write = */ false, /* report_current_thread = */ true); } bool ExceptionHandler::InstallHandler() { // If a handler is already installed, something is really wrong. if (gProtectedData.handler != NULL) { return false; @@ -810,17 +806,16 @@ bool ExceptionHandler::Setup(bool instal if (install_handler && result == KERN_SUCCESS) if (!InstallHandler()) return false; // Don't spawn the handler thread when replaying, as we have not set up // exception ports for it to monitor. if (result == KERN_SUCCESS && !mozilla::recordreplay::IsReplaying()) { // Install the handler in its own thread, detached as we won't be joining. - mozilla::recordreplay::AutoPassThroughThreadEvents pt; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int thread_create_result = pthread_create(&handler_thread_, &attr, &WaitForMessage, this); pthread_attr_destroy(&attr); result = thread_create_result ? KERN_FAILURE : KERN_SUCCESS; }
--- a/toolkit/crashreporter/breakpad-client/mac/handler/exception_handler.h +++ b/toolkit/crashreporter/breakpad-client/mac/handler/exception_handler.h @@ -163,18 +163,17 @@ class ExceptionHandler { const std::string &dump_path, MinidumpCallback callback, void *callback_context); // Write a minidump for an exception that was received by another handler. static bool WriteForwardedExceptionMinidump(int exception_type, int exception_code, int exception_subcode, - mach_port_t thread, - mach_port_t task); + mach_port_t thread); // Returns whether out-of-process dump generation is used or not. bool IsOutOfProcess() const { #if TARGET_OS_IPHONE return false; #else return crash_generation_client_.get() != NULL; #endif @@ -202,17 +201,16 @@ class ExceptionHandler { // All minidump writing goes through this one routine. // |task_context| can be NULL. If not, it will be used to retrieve the // context of the current thread, instead of using |thread_get_state|. bool WriteMinidumpWithException(int exception_type, int exception_code, int exception_subcode, breakpad_ucontext_t *task_context, mach_port_t thread_name, - mach_port_t task_name, bool exit_after_write, bool report_current_thread); // When installed, this static function will be call from a newly created // pthread with |this| as the argument static void *WaitForMessage(void *exception_handler_class); // Signal handler for SIGABRT.
--- a/toolkit/recordreplay/Assembler.cpp +++ b/toolkit/recordreplay/Assembler.cpp @@ -42,27 +42,29 @@ void Assembler::NoteOriginalInstruction( mCopiedInstructions.emplaceBack(aIp, Current()); } void Assembler::Advance(size_t aSize) { MOZ_RELEASE_ASSERT(aSize <= MaximumAdvance); mCursor += aSize; } +static const size_t JumpBytes = 17; + uint8_t* Assembler::Current() { // Reallocate the buffer if there is not enough space. We need enough for the // maximum space used by any of the assembling functions, as well as for a // following jump for fallthrough to the next allocated space. if (size_t(mCursorEnd - mCursor) <= MaximumAdvance + JumpBytes) { MOZ_RELEASE_ASSERT(mCanAllocateStorage); // Allocate some writable, executable memory. - static const size_t BufferSize = PageSize * 32; - uint8_t* buffer = new uint8_t[BufferSize]; - UnprotectExecutableMemory(buffer, BufferSize); + static const size_t BufferSize = PageSize; + uint8_t* buffer = new uint8_t[PageSize]; + UnprotectExecutableMemory(buffer, PageSize); if (mCursor) { // Patch a jump for fallthrough from the last allocation. MOZ_RELEASE_ASSERT(size_t(mCursorEnd - mCursor) >= JumpBytes); PatchJump(mCursor, buffer); } mCursor = buffer; @@ -74,53 +76,35 @@ uint8_t* Assembler::Current() { static void Push16(uint8_t** aIp, uint16_t aValue) { (*aIp)[0] = 0x66; (*aIp)[1] = 0x68; *reinterpret_cast<uint16_t*>(*aIp + 2) = aValue; (*aIp) += 4; } -static void PushImmediateAtIp(uint8_t** aIp, void* aValue) { +/* static */ +void Assembler::PatchJump(uint8_t* aIp, void* aTarget) { // Push the target literal onto the stack, 2 bytes at a time. This is // apparently the best way of getting an arbitrary 8 byte literal onto the // stack, as 4 byte literals we push will be sign extended to 8 bytes. - size_t nvalue = reinterpret_cast<size_t>(aValue); - Push16(aIp, nvalue >> 48); - Push16(aIp, nvalue >> 32); - Push16(aIp, nvalue >> 16); - Push16(aIp, nvalue); -} - -/* static */ -void Assembler::PatchJump(uint8_t* aIp, void* aTarget) { - PushImmediateAtIp(&aIp, aTarget); + size_t ntarget = reinterpret_cast<size_t>(aTarget); + Push16(&aIp, ntarget >> 48); + Push16(&aIp, ntarget >> 32); + Push16(&aIp, ntarget >> 16); + Push16(&aIp, ntarget); *aIp = 0xC3; // ret } void Assembler::Jump(void* aTarget) { PatchJump(Current(), aTarget); mJumps.emplaceBack(Current(), (uint8_t*)aTarget); Advance(JumpBytes); } -void Assembler::PushImmediate(void* aValue) { - uint8_t* ip = Current(); - PushImmediateAtIp(&ip, aValue); - Advance(PushImmediateBytes); -} - -void Assembler::Return() { - NewInstruction(0xC3); -} - -void Assembler::Breakpoint() { - NewInstruction(0xCC); -} - static uint8_t OppositeJump(uint8_t aOpcode) { // Get the opposite single byte jump opcode for a one or two byte conditional // jump. Opposite opcodes are adjacent, e.g. 0x7C -> jl and 0x7D -> jge. if (aOpcode >= 0x80 && aOpcode <= 0x8F) { aOpcode -= 0x10; } else { MOZ_RELEASE_ASSERT(aOpcode >= 0x70 && aOpcode <= 0x7F); } @@ -167,34 +151,20 @@ void Assembler::LoadRax(size_t aWidth) { MOZ_CRASH(); } } void Assembler::CompareRaxWithTopOfStack() { NewInstruction(0x48, 0x39, 0x04, 0x24); } -void Assembler::CompareTopOfStackWithRax() { - NewInstruction(0x48, 0x3B, 0x04, 0x24); -} - void Assembler::PushRbx() { NewInstruction(0x53); } void Assembler::PopRbx() { NewInstruction(0x5B); } -void Assembler::PopRegister(/*ud_type*/ int aRegister) { - MOZ_RELEASE_ASSERT(aRegister == NormalizeRegister(aRegister)); - - if (aRegister <= UD_R_RDI) { - NewInstruction(0x58 + aRegister - UD_R_RAX); - } else { - NewInstruction(0x41, 0x58 + aRegister - UD_R_R8); - } -} - void Assembler::StoreRbxToRax(size_t aWidth) { switch (aWidth) { case 1: NewInstruction(0x88, 0x18); break; case 2: NewInstruction(0x66, 0x89, 0x18); break;
--- a/toolkit/recordreplay/Assembler.h +++ b/toolkit/recordreplay/Assembler.h @@ -39,25 +39,16 @@ class Assembler { /////////////////////////////////////////////////////////////////////////////// // Routines for assembling instructions in new instruction storage /////////////////////////////////////////////////////////////////////////////// // Jump to aTarget. If aTarget is in the range of instructions being copied, // the target will be the copy of aTarget instead. void Jump(void* aTarget); - // Push aValue onto the stack. - void PushImmediate(void* aValue); - - // Return to the address at the top of the stack. - void Return(); - - // For debugging, insert a breakpoint instruction. - void Breakpoint(); - // Conditionally jump to aTarget, depending on the short jump opcode aCode. // If aTarget is in the range of instructions being copied, the target will // be the copy of aTarget instead. void ConditionalJump(uint8_t aCode, void* aTarget); // Copy an instruction verbatim from aIp. void CopyInstruction(uint8_t* aIp, size_t aSize); @@ -72,25 +63,20 @@ class Assembler { void CallRax(); // movq/movl/movb 0(%rax), %rax void LoadRax(size_t aWidth); // cmpq %rax, 0(%rsp) void CompareRaxWithTopOfStack(); - // cmpq 0(%rsp), %rax - void CompareTopOfStackWithRax(); - // push/pop %rbx void PushRbx(); void PopRbx(); - void PopRegister(/*ud_type*/ int aRegister); - // movq/movl/movb %rbx, 0(%rax) void StoreRbxToRax(size_t aWidth); // cmpq/cmpb $literal8, %rax void CompareValueWithRax(uint8_t aValue, size_t aWidth); // movq $value, %rax void MoveImmediateToRax(void* aValue); @@ -183,21 +169,16 @@ class Assembler { }; // The number of instruction bytes required for a short jump. static const size_t ShortJumpBytes = 2; // The number of instruction bytes required for a jump that may clobber rax. static const size_t JumpBytesClobberRax = 12; -// The number of instruction bytes required for an arbitrary jump. -static const size_t JumpBytes = 17; - -static const size_t PushImmediateBytes = 16; - // The maximum byte length of an x86/x64 instruction. static const size_t MaximumInstructionLength = 15; // Make a region of memory RWX. void UnprotectExecutableMemory(uint8_t* aAddress, size_t aSize); } // namespace recordreplay } // namespace mozilla
--- a/toolkit/recordreplay/BufferStream.h +++ b/toolkit/recordreplay/BufferStream.h @@ -7,18 +7,19 @@ #ifndef mozilla_recordreplay_BufferStream_h #define mozilla_recordreplay_BufferStream_h #include "InfallibleVector.h" namespace mozilla { namespace recordreplay { -// BufferStream is a simplified form of Stream from Recording.h, allowing -// reading or writing to a stream of data backed by an in memory buffer. +// BufferStream provides similar functionality to Stream in File.h, allowing +// reading or writing to a stream of data backed by an in memory buffer instead +// of data stored on disk. class BufferStream { InfallibleVector<char>* mOutput; const char* mInput; size_t mInputSize; public: BufferStream(const char* aInput, size_t aInputSize)
new file mode 100644 --- /dev/null +++ b/toolkit/recordreplay/DirtyMemoryHandler.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DirtyMemoryHandler.h" + +#include "ipc/ChildInternal.h" +#include "mozilla/Sprintf.h" +#include "MemorySnapshot.h" +#include "Thread.h" + +#include <mach/exc.h> +#include <mach/mach.h> +#include <mach/mach_vm.h> +#include <sys/time.h> + +namespace mozilla { +namespace recordreplay { + +static mach_port_t gDirtyMemoryExceptionPort; + +// See AsmJSSignalHandlers.cpp. +static const mach_msg_id_t sExceptionId = 2405; + +// This definition was generated by mig (the Mach Interface Generator) for the +// routine 'exception_raise' (exc.defs). See js/src/wasm/WasmSignalHandlers.cpp. +#pragma pack(4) +typedef struct { + mach_msg_header_t Head; + /* start of the kernel processed data */ + mach_msg_body_t msgh_body; + mach_msg_port_descriptor_t thread; + mach_msg_port_descriptor_t task; + /* end of the kernel processed data */ + NDR_record_t NDR; + exception_type_t exception; + mach_msg_type_number_t codeCnt; + int64_t code[2]; +} Request__mach_exception_raise_t; +#pragma pack() + +typedef struct { + Request__mach_exception_raise_t body; + mach_msg_trailer_t trailer; +} ExceptionRequest; + +static void DirtyMemoryExceptionHandlerThread(void*) { + kern_return_t kret; + + while (true) { + ExceptionRequest request; + kret = mach_msg(&request.body.Head, MACH_RCV_MSG, 0, sizeof(request), + gDirtyMemoryExceptionPort, MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL); + kern_return_t replyCode = KERN_FAILURE; + if (kret == KERN_SUCCESS && request.body.Head.msgh_id == sExceptionId && + request.body.exception == EXC_BAD_ACCESS && request.body.codeCnt == 2) { + uint8_t* faultingAddress = (uint8_t*)request.body.code[1]; + if (HandleDirtyMemoryFault(faultingAddress)) { + replyCode = KERN_SUCCESS; + } else { + child::MinidumpInfo info(request.body.exception, request.body.code[0], + request.body.code[1], + request.body.thread.name); + child::ReportFatalError( + Some(info), "HandleDirtyMemoryFault failed %p %s", faultingAddress, + gMozCrashReason ? gMozCrashReason : ""); + } + } else { + child::ReportFatalError(Nothing(), + "DirtyMemoryExceptionHandlerThread mach_msg " + "returned unexpected data"); + } + + __Reply__exception_raise_t reply; + reply.Head.msgh_bits = + MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request.body.Head.msgh_bits), 0); + reply.Head.msgh_size = sizeof(reply); + reply.Head.msgh_remote_port = request.body.Head.msgh_remote_port; + reply.Head.msgh_local_port = MACH_PORT_NULL; + reply.Head.msgh_id = request.body.Head.msgh_id + 100; + reply.NDR = NDR_record; + reply.RetCode = replyCode; + mach_msg(&reply.Head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); + } +} + +void SetupDirtyMemoryHandler() { + // Allow repeated calls. + static bool hasDirtyMemoryHandler = false; + if (hasDirtyMemoryHandler) { + return; + } + hasDirtyMemoryHandler = true; + + MOZ_RELEASE_ASSERT(AreThreadEventsPassedThrough()); + kern_return_t kret; + + // Get a port which can send and receive data. + kret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, + &gDirtyMemoryExceptionPort); + MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS); + + kret = mach_port_insert_right(mach_task_self(), gDirtyMemoryExceptionPort, + gDirtyMemoryExceptionPort, + MACH_MSG_TYPE_MAKE_SEND); + MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS); + + // Create a thread to block on reading the port. + Thread::SpawnNonRecordedThread(DirtyMemoryExceptionHandlerThread, nullptr); + + // Set exception ports on the entire task. Unfortunately, this clobbers any + // other exception ports for the task, and forwarding to those other ports + // is not easy to get right. + kret = task_set_exception_ports( + mach_task_self(), EXC_MASK_BAD_ACCESS, gDirtyMemoryExceptionPort, + EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE); + MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS); +} + +} // namespace recordreplay +} // namespace mozilla
new file mode 100644 --- /dev/null +++ b/toolkit/recordreplay/DirtyMemoryHandler.h @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_recordreplay_DirtyMemoryHandler_h +#define mozilla_recordreplay_DirtyMemoryHandler_h + +namespace mozilla { +namespace recordreplay { + +// Set up a handler to catch SEGV hardware exceptions and pass them on to +// HandleDirtyMemoryFault in MemorySnapshot.h for handling. +void SetupDirtyMemoryHandler(); + +} // namespace recordreplay +} // namespace mozilla + +#endif // mozilla_recordreplay_DirtyMemoryHandler_h
rename from toolkit/recordreplay/Recording.cpp rename to toolkit/recordreplay/File.cpp --- a/toolkit/recordreplay/Recording.cpp +++ b/toolkit/recordreplay/File.cpp @@ -1,15 +1,15 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "Recording.h" +#include "File.h" #include "ipc/ChildInternal.h" #include "mozilla/Compression.h" #include "mozilla/Sprintf.h" #include "ProcessRewind.h" #include "SpinLock.h" #include <algorithm> @@ -17,17 +17,17 @@ namespace mozilla { namespace recordreplay { /////////////////////////////////////////////////////////////////////////////// // Stream /////////////////////////////////////////////////////////////////////////////// void Stream::ReadBytes(void* aData, size_t aSize) { - MOZ_RELEASE_ASSERT(mRecording->IsReading()); + MOZ_RELEASE_ASSERT(mFile->OpenForReading()); size_t totalRead = 0; while (true) { // Read what we can from the data buffer. MOZ_RELEASE_ASSERT(mBufferPos <= mBufferLength); size_t bufAvailable = mBufferLength - mBufferPos; size_t bufRead = std::min(bufAvailable, aSize); @@ -47,17 +47,17 @@ void Stream::ReadBytes(void* aData, size MOZ_RELEASE_ASSERT(mBufferPos == mBufferLength); MOZ_RELEASE_ASSERT(mChunkIndex < mChunks.length()); const StreamChunkLocation& chunk = mChunks[mChunkIndex++]; MOZ_RELEASE_ASSERT(chunk.mStreamPos == mStreamPos); EnsureMemory(&mBallast, &mBallastSize, chunk.mCompressedSize, BallastMaxSize(), DontCopyExistingData); - mRecording->ReadChunk(mBallast.get(), chunk); + mFile->ReadChunk(mBallast.get(), chunk); EnsureMemory(&mBuffer, &mBufferSize, chunk.mDecompressedSize, BUFFER_MAX, DontCopyExistingData); size_t bytesWritten; if (!Compression::LZ4::decompress(mBallast.get(), chunk.mCompressedSize, mBuffer.get(), chunk.mDecompressedSize, &bytesWritten) || @@ -66,43 +66,43 @@ void Stream::ReadBytes(void* aData, size } mBufferPos = 0; mBufferLength = chunk.mDecompressedSize; } } bool Stream::AtEnd() { - MOZ_RELEASE_ASSERT(mRecording->IsReading()); + MOZ_RELEASE_ASSERT(mFile->OpenForReading()); return mBufferPos == mBufferLength && mChunkIndex == mChunks.length(); } void Stream::WriteBytes(const void* aData, size_t aSize) { - MOZ_RELEASE_ASSERT(mRecording->IsWriting()); + MOZ_RELEASE_ASSERT(mFile->OpenForWriting()); MOZ_RELEASE_ASSERT(mName != StreamName::Event || mInRecordingEventSection); - // Prevent the recording from being flushed while we write this data. - AutoReadSpinLock streamLock(mRecording->mStreamLock); + // Prevent the entire file from being flushed while we write this data. + AutoReadSpinLock streamLock(mFile->mStreamLock); while (true) { // Fill up the data buffer first. MOZ_RELEASE_ASSERT(mBufferPos <= mBufferSize); size_t bufAvailable = mBufferSize - mBufferPos; size_t bufWrite = (bufAvailable < aSize) ? bufAvailable : aSize; memcpy(&mBuffer[mBufferPos], aData, bufWrite); mBufferPos += bufWrite; mStreamPos += bufWrite; if (bufWrite == aSize) { return; } aData = (char*)aData + bufWrite; aSize -= bufWrite; - // Grow the stream's buffer if it is not at its maximum size. + // Grow the file's buffer if it is not at its maximum size. if (mBufferSize < BUFFER_MAX) { EnsureMemory(&mBuffer, &mBufferSize, mBufferSize + 1, BUFFER_MAX, CopyExistingData); continue; } Flush(/* aTakeLock = */ true); } @@ -137,39 +137,36 @@ void Stream::WriteScalar(size_t aValue) aValue = aValue >> 7; if (aValue) { bits |= 128; } WriteBytes(&bits, 1); } while (aValue); } -void Stream::RecordOrReplayThreadEvent(ThreadEvent aEvent, const char* aExtra) { +void Stream::RecordOrReplayThreadEvent(ThreadEvent aEvent) { if (IsRecording()) { WriteScalar((size_t)aEvent); } else { ThreadEvent oldEvent = (ThreadEvent)ReadScalar(); if (oldEvent != aEvent) { - DumpEvents(); const char* extra = ""; if (oldEvent == ThreadEvent::Assert) { // Include the asserted string in the error. This must match up with // the writes in RecordReplayAssert. if (mNameIndex == MainThreadId) { (void)ReadScalar(); // For the ExecutionProgressCounter write below. } extra = ReadInputString(); } child::ReportFatalError( - "Event Mismatch: Recorded %s %s Replayed %s %s", - ThreadEventName(oldEvent), extra, - ThreadEventName(aEvent), aExtra ? aExtra : ""); + Nothing(), "Event Mismatch: Recorded %s %s Replayed %s", + ThreadEventName(oldEvent), extra, ThreadEventName(aEvent)); } mLastEvent = aEvent; - PushEvent(ThreadEventName(aEvent)); } // Check the execution progress counter for events executing on the main // thread. if (mNameIndex == MainThreadId) { CheckInput(*ExecutionProgressCounter()); } } @@ -183,18 +180,18 @@ ThreadEvent Stream::ReplayThreadEvent() } void Stream::CheckInput(size_t aValue) { if (IsRecording()) { WriteScalar(aValue); } else { size_t oldValue = ReadScalar(); if (oldValue != aValue) { - DumpEvents(); - child::ReportFatalError("Input Mismatch: %s Recorded %llu Replayed %llu", + child::ReportFatalError(Nothing(), + "Input Mismatch: %s Recorded %llu Replayed %llu", ThreadEventName(mLastEvent), oldValue, aValue); } } } const char* Stream::ReadInputString() { size_t len = ReadScalar(); EnsureInputBallast(len + 1); @@ -206,35 +203,33 @@ const char* Stream::ReadInputString() { void Stream::CheckInput(const char* aValue) { size_t len = strlen(aValue); if (IsRecording()) { WriteScalar(len); WriteBytes(aValue, len); } else { const char* oldInput = ReadInputString(); if (strcmp(oldInput, aValue) != 0) { - DumpEvents(); - child::ReportFatalError("Input Mismatch: %s Recorded %s Replayed %s", + child::ReportFatalError(Nothing(), + "Input Mismatch: %s Recorded %s Replayed %s", ThreadEventName(mLastEvent), oldInput, aValue); } - PushEvent(aValue); } } void Stream::CheckInput(const void* aData, size_t aSize) { CheckInput(aSize); if (IsRecording()) { WriteBytes(aData, aSize); } else { EnsureInputBallast(aSize); ReadBytes(mInputBallast.get(), aSize); if (memcmp(aData, mInputBallast.get(), aSize) != 0) { - DumpEvents(); - child::ReportFatalError("Input Buffer Mismatch: %s", + child::ReportFatalError(Nothing(), "Input Buffer Mismatch: %s", ThreadEventName(mLastEvent)); } } } void Stream::EnsureMemory(UniquePtr<char[]>* aBuf, size_t* aSize, size_t aNeededSize, size_t aMaxSize, ShouldCopy aCopy) { @@ -254,165 +249,241 @@ void Stream::EnsureMemory(UniquePtr<char } void Stream::EnsureInputBallast(size_t aSize) { EnsureMemory(&mInputBallast, &mInputBallastSize, aSize, (size_t)-1, DontCopyExistingData); } void Stream::Flush(bool aTakeLock) { - MOZ_RELEASE_ASSERT(mRecording->IsWriting()); + MOZ_RELEASE_ASSERT(mFile && mFile->OpenForWriting()); if (!mBufferPos) { return; } size_t bound = Compression::LZ4::maxCompressedSize(mBufferPos); EnsureMemory(&mBallast, &mBallastSize, bound, BallastMaxSize(), DontCopyExistingData); size_t compressedSize = Compression::LZ4::compress(mBuffer.get(), mBufferPos, mBallast.get()); MOZ_RELEASE_ASSERT(compressedSize != 0); MOZ_RELEASE_ASSERT((size_t)compressedSize <= bound); StreamChunkLocation chunk = - mRecording->WriteChunk(mName, mNameIndex, - mBallast.get(), compressedSize, mBufferPos, - mStreamPos - mBufferPos, aTakeLock); + mFile->WriteChunk(mBallast.get(), compressedSize, mBufferPos, + mStreamPos - mBufferPos, aTakeLock); mChunks.append(chunk); MOZ_ALWAYS_TRUE(++mChunkIndex == mChunks.length()); mBufferPos = 0; } /* static */ size_t Stream::BallastMaxSize() { return Compression::LZ4::maxCompressedSize(BUFFER_MAX); } -static bool gDumpEvents; - -void Stream::PushEvent(const char* aEvent) { - if (gDumpEvents) { - mEvents.append(strdup(aEvent)); - } -} - -void Stream::DumpEvents() { - if (gDumpEvents) { - Print("Thread Events: %d\n", Thread::Current()->Id()); - for (char* ev : mEvents) { - Print("Event: %s\n", ev); - } - } -} - /////////////////////////////////////////////////////////////////////////////// -// Recording +// File /////////////////////////////////////////////////////////////////////////////// -Recording::Recording() : mMode(IsRecording() ? WRITE : READ) { - PodZero(&mLock); - PodZero(&mStreamLock); - - if (IsReplaying()) { - gDumpEvents = TestEnv("MOZ_REPLAYING_DUMP_EVENTS"); - } -} - -// The recording format is a series of chunks. Each chunk is a ChunkDescriptor -// followed by the compressed contents of the chunk itself. -struct ChunkDescriptor { +// Information in a file index about a chunk. +struct FileIndexChunk { uint32_t /* StreamName */ mName; uint32_t mNameIndex; StreamChunkLocation mChunk; - ChunkDescriptor() { PodZero(this); } + FileIndexChunk() { PodZero(this); } - ChunkDescriptor(StreamName aName, uint32_t aNameIndex, - const StreamChunkLocation& aChunk) + FileIndexChunk(StreamName aName, uint32_t aNameIndex, + const StreamChunkLocation& aChunk) : mName((uint32_t)aName), mNameIndex(aNameIndex), mChunk(aChunk) {} }; -void Recording::NewContents(const uint8_t* aContents, size_t aSize, - InfallibleVector<Stream*>* aUpdatedStreams) { - // All other recorded threads are idle when adding new contents, so we don't - // have to worry about thread safety here. - MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); - MOZ_RELEASE_ASSERT(IsReading()); +// We expect to find this at every index in a file. +static const uint64_t MagicValue = 0xd3e7f5fae445b3ac; + +// Index of chunks in a file. There is an index at the start of the file +// (which is always empty) and at various places within the file itself. +struct FileIndex { + // This should match MagicValue. + uint64_t mMagic; + + // How many FileIndexChunk instances follow this structure. + uint32_t mNumChunks; + + // The location of the next index in the file, or zero. + uint64_t mNextIndexOffset; + + explicit FileIndex(uint32_t aNumChunks) + : mMagic(MagicValue), mNumChunks(aNumChunks), mNextIndexOffset(0) {} +}; + +bool File::Open(const char* aName, Mode aMode) { + MOZ_RELEASE_ASSERT(!mFd); + MOZ_RELEASE_ASSERT(aName); - mContents.append(aContents, aSize); + mMode = aMode; + mFd = DirectOpenFile(aName, mMode == WRITE); + + if (OpenForWriting()) { + // Write an empty index at the start of the file. + FileIndex index(0); + DirectWrite(mFd, &index, sizeof(index)); + mWriteOffset += sizeof(index); + return true; + } + + // Read in every index in the file. + ReadIndexResult result; + do { + result = ReadNextIndex(nullptr); + if (result == ReadIndexResult::InvalidFile) { + return false; + } + } while (result == ReadIndexResult::FoundIndex); + + return true; +} - size_t offset = 0; - while (offset < aSize) { - MOZ_RELEASE_ASSERT(offset + sizeof(ChunkDescriptor) <= aSize); - ChunkDescriptor* desc = (ChunkDescriptor*)(aContents + offset); - offset += sizeof(ChunkDescriptor); +void File::Close() { + if (!mFd) { + return; + } + + if (OpenForWriting()) { + Flush(); + } + + Clear(); +} + +File::ReadIndexResult File::ReadNextIndex( + InfallibleVector<Stream*>* aUpdatedStreams) { + // Unlike in the Flush() case, we don't have to worry about other threads + // attempting to read data from streams in this file while we are reading + // the new index. + MOZ_ASSERT(OpenForReading()); - Stream* stream = OpenStream((StreamName)desc->mName, desc->mNameIndex); - stream->mChunks.append(desc->mChunk); + // Read in the last index to see if there is another one. + DirectSeekFile(mFd, mLastIndexOffset + offsetof(FileIndex, mNextIndexOffset)); + uint64_t nextIndexOffset; + if (DirectRead(mFd, &nextIndexOffset, sizeof(nextIndexOffset)) != + sizeof(nextIndexOffset)) { + return ReadIndexResult::InvalidFile; + } + if (!nextIndexOffset) { + return ReadIndexResult::EndOfFile; + } + + mLastIndexOffset = nextIndexOffset; + + FileIndex index(0); + DirectSeekFile(mFd, nextIndexOffset); + if (DirectRead(mFd, &index, sizeof(index)) != sizeof(index)) { + return ReadIndexResult::InvalidFile; + } + if (index.mMagic != MagicValue) { + return ReadIndexResult::InvalidFile; + } + + MOZ_RELEASE_ASSERT(index.mNumChunks); + + size_t indexBytes = index.mNumChunks * sizeof(FileIndexChunk); + FileIndexChunk* chunks = new FileIndexChunk[index.mNumChunks]; + if (DirectRead(mFd, chunks, indexBytes) != indexBytes) { + return ReadIndexResult::InvalidFile; + } + for (size_t i = 0; i < index.mNumChunks; i++) { + const FileIndexChunk& indexChunk = chunks[i]; + Stream* stream = + OpenStream((StreamName)indexChunk.mName, indexChunk.mNameIndex); + stream->mChunks.append(indexChunk.mChunk); if (aUpdatedStreams) { aUpdatedStreams->append(stream); } + } + delete[] chunks; - MOZ_RELEASE_ASSERT(offset + desc->mChunk.mCompressedSize <= aSize); - offset += desc->mChunk.mCompressedSize; - } + return ReadIndexResult::FoundIndex; } -void Recording::Flush() { +bool File::Flush() { MOZ_ASSERT(OpenForWriting()); AutoSpinLock lock(mLock); + InfallibleVector<FileIndexChunk> newChunks; for (auto& vector : mStreams) { for (const UniquePtr<Stream>& stream : vector) { if (stream) { stream->Flush(/* aTakeLock = */ false); + for (size_t i = stream->mFlushedChunks; i < stream->mChunkIndex; i++) { + newChunks.emplaceBack(stream->mName, stream->mNameIndex, + stream->mChunks[i]); + } + stream->mFlushedChunks = stream->mChunkIndex; } } } + + if (newChunks.empty()) { + return false; + } + + // Write the new index information at the end of the file. + uint64_t indexOffset = mWriteOffset; + size_t indexBytes = newChunks.length() * sizeof(FileIndexChunk); + FileIndex index(newChunks.length()); + DirectWrite(mFd, &index, sizeof(index)); + DirectWrite(mFd, newChunks.begin(), indexBytes); + mWriteOffset += sizeof(index) + indexBytes; + + // Update the next index offset for the last index written. + MOZ_RELEASE_ASSERT(sizeof(index.mNextIndexOffset) == sizeof(indexOffset)); + DirectSeekFile(mFd, mLastIndexOffset + offsetof(FileIndex, mNextIndexOffset)); + DirectWrite(mFd, &indexOffset, sizeof(indexOffset)); + DirectSeekFile(mFd, mWriteOffset); + + mLastIndexOffset = indexOffset; + + return true; } -StreamChunkLocation Recording::WriteChunk(StreamName aName, size_t aNameIndex, - const char* aStart, - size_t aCompressedSize, - size_t aDecompressedSize, - uint64_t aStreamPos, bool aTakeLock) { +StreamChunkLocation File::WriteChunk(const char* aStart, size_t aCompressedSize, + size_t aDecompressedSize, + uint64_t aStreamPos, bool aTakeLock) { Maybe<AutoSpinLock> lock; if (aTakeLock) { lock.emplace(mLock); } StreamChunkLocation chunk; - chunk.mOffset = mContents.length() + sizeof(ChunkDescriptor); + chunk.mOffset = mWriteOffset; chunk.mCompressedSize = aCompressedSize; chunk.mDecompressedSize = aDecompressedSize; chunk.mHash = HashBytes(aStart, aCompressedSize); chunk.mStreamPos = aStreamPos; - ChunkDescriptor desc; - desc.mName = (uint32_t) aName; - desc.mNameIndex = aNameIndex; - desc.mChunk = chunk; - - mContents.append((const uint8_t*)&desc, sizeof(ChunkDescriptor)); - mContents.append(aStart, aCompressedSize); + DirectWrite(mFd, aStart, aCompressedSize); + mWriteOffset += aCompressedSize; return chunk; } -void Recording::ReadChunk(char* aDest, const StreamChunkLocation& aChunk) { +void File::ReadChunk(char* aDest, const StreamChunkLocation& aChunk) { AutoSpinLock lock(mLock); - MOZ_RELEASE_ASSERT(aChunk.mOffset + aChunk.mCompressedSize <= mContents.length()); - memcpy(aDest, mContents.begin() + aChunk.mOffset, aChunk.mCompressedSize); + DirectSeekFile(mFd, aChunk.mOffset); + size_t res = DirectRead(mFd, aDest, aChunk.mCompressedSize); + MOZ_RELEASE_ASSERT(res == aChunk.mCompressedSize); MOZ_RELEASE_ASSERT(HashBytes(aDest, aChunk.mCompressedSize) == aChunk.mHash); } -Stream* Recording::OpenStream(StreamName aName, size_t aNameIndex) { +Stream* File::OpenStream(StreamName aName, size_t aNameIndex) { AutoSpinLock lock(mLock); auto& vector = mStreams[(size_t)aName]; while (aNameIndex >= vector.length()) { vector.emplaceBack(); }
rename from toolkit/recordreplay/Recording.h rename to toolkit/recordreplay/File.h --- a/toolkit/recordreplay/Recording.h +++ b/toolkit/recordreplay/File.h @@ -1,100 +1,88 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#ifndef mozilla_recordreplay_Recording_h -#define mozilla_recordreplay_Recording_h +#ifndef mozilla_recordreplay_File_h +#define mozilla_recordreplay_File_h #include "InfallibleVector.h" #include "ProcessRecordReplay.h" #include "SpinLock.h" #include "mozilla/PodOperations.h" #include "mozilla/RecordReplay.h" #include "mozilla/UniquePtr.h" -#include "nsString.h" namespace mozilla { namespace recordreplay { -// Representation of the recording which is written to by recording processes -// and read from by replaying processes. The recording encapsulates a set of -// streams of data. While recording, these streams grow independently from one -// another, and when the recording is flushed the streams contents are collated -// into a single stream of bytes which can be saved to disk or sent to other -// processes via IPC or network connections. +// Structure managing file I/O. Each file contains an index for a set of named +// streams, whose contents are compressed and interleaved throughout the file. +// Additionally, we directly manage the file handle and all associated memory. +// This makes it easier to restore memory snapshots without getting confused +// about the state of the file handles which the process has opened. Data +// written and read from files is automatically compressed with LZ4. // -// Data in the recording is automatically compressed with LZ4. The Recording -// object is threadsafe for simultaneous read/read and write/write accesses. +// Files are used internally for any disk accesses which the record/replay +// infrastructure needs to make. Currently, this is only for accessing the +// recording file. +// +// File is threadsafe for simultaneous read/read and write/write accesses. // Stream is not threadsafe. -// A location of a chunk of a stream within a recording. +// A location of a chunk of a stream within a file. struct StreamChunkLocation { - // Offset into the recording of the start of the chunk. + // Offset into the file of the start of the chunk. uint64_t mOffset; - // Compressed size of the chunk, as stored in the recording. + // Compressed (stored) size of the chunk. uint32_t mCompressedSize; // Decompressed size of the chunk. uint32_t mDecompressedSize; // Hash of the compressed chunk data. uint32_t mHash; // Position in the stream of the start of this chunk. uint64_t mStreamPos; }; -enum class StreamName { - // Per-thread list of events. - Event, - - // Per-lock list of threads in acquire order. - Lock, +enum class StreamName { Main, Lock, Event, Count }; - // Single stream containing endpoints of recording after flushing. - Endpoint, - - // Single stream describing recording sections to skip for local replay;. - LocalReplaySkip, - - Count -}; - -class Recording; +class File; class RecordingEventSection; class Stream { - friend class Recording; + friend class File; friend class RecordingEventSection; - // Recording this stream belongs to. - Recording* mRecording; + // File this stream belongs to. + File* mFile; // Prefix name for this stream. StreamName mName; - // Index which, when combined with mName, uniquely identifies this stream in - // the recording. + // Index which, when combined to mName, uniquely identifies this stream in + // the file. size_t mNameIndex; - // All chunks of data in the stream. + // When writing, all chunks that have been flushed to disk. When reading, all + // chunks in the entire stream. InfallibleVector<StreamChunkLocation> mChunks; // Data buffer. UniquePtr<char[]> mBuffer; // The maximum number of bytes to buffer before compressing and writing to - // the recording, and the maximum number of bytes that can be decompressed at - // once. + // disk, and the maximum number of bytes that can be decompressed at once. static const size_t BUFFER_MAX = 1024 * 1024; // The capacity of mBuffer, at most BUFFER_MAX. size_t mBufferSize; // During reading, the number of accessible bytes in mBuffer. size_t mBufferLength; @@ -114,38 +102,39 @@ class Stream { // The last event in this stream, in case of an input mismatch. ThreadEvent mLastEvent; // The number of chunks that have been completely read or written. When // writing, this equals mChunks.length(). size_t mChunkIndex; + // When writing, the number of chunks in this stream when the file was last + // flushed. + size_t mFlushedChunks; + // Whether there is a RecordingEventSection instance active for this stream. bool mInRecordingEventSection; - // When replaying and MOZ_REPLAYING_DUMP_EVENTS is set, this describes all - // events in the stream we have replayed so far. - InfallibleVector<char*> mEvents; - - Stream(Recording* aRecording, StreamName aName, size_t aNameIndex) - : mRecording(aRecording), + Stream(File* aFile, StreamName aName, size_t aNameIndex) + : mFile(aFile), mName(aName), mNameIndex(aNameIndex), mBuffer(nullptr), mBufferSize(0), mBufferLength(0), mBufferPos(0), mStreamPos(0), mBallast(nullptr), mBallastSize(0), mInputBallast(nullptr), mInputBallastSize(0), mLastEvent((ThreadEvent)0), mChunkIndex(0), + mFlushedChunks(0), mInRecordingEventSection(false) {} public: StreamName Name() const { return mName; } size_t NameIndex() const { return mNameIndex; } void ReadBytes(void* aData, size_t aSize); void WriteBytes(const void* aData, size_t aSize); @@ -172,17 +161,17 @@ class Stream { template <typename T> inline void RecordOrReplayValue(T* aPtr) { RecordOrReplayBytes(aPtr, sizeof(T)); } // Note a new thread event for this stream, and make sure it is the same // while replaying as it was while recording. - void RecordOrReplayThreadEvent(ThreadEvent aEvent, const char* aExtra = nullptr); + void RecordOrReplayThreadEvent(ThreadEvent aEvent); // Replay a thread event without requiring it to be a specific event. ThreadEvent ReplayThreadEvent(); // Make sure that a value or buffer is the same while replaying as it was // while recording. void CheckInput(size_t aValue); void CheckInput(const char* aValue); @@ -195,77 +184,91 @@ class Stream { void EnsureMemory(UniquePtr<char[]>* aBuf, size_t* aSize, size_t aNeededSize, size_t aMaxSize, ShouldCopy aCopy); void EnsureInputBallast(size_t aSize); void Flush(bool aTakeLock); const char* ReadInputString(); static size_t BallastMaxSize(); - - void PushEvent(const char* aEvent); - void DumpEvents(); }; -class Recording { +class File { public: enum Mode { WRITE, READ }; friend class Stream; friend class RecordingEventSection; private: - // Whether this recording is for writing or reading. - Mode mMode = READ; + // Open file handle, or 0 if closed. + FileHandle mFd; + + // Whether this file is open for writing or reading. + Mode mMode; - // When writing, all contents that have been flushed so far. When reading, - // all known contents. When writing, existing parts of the recording are not - // modified: the recording can only grow. - InfallibleVector<uint8_t> mContents; + // When writing, the current offset into the file. + uint64_t mWriteOffset; - // All streams in this recording, indexed by stream name and name index. + // The offset of the last index read or written to the file. + uint64_t mLastIndexOffset; + + // All streams in this file, indexed by stream name and name index. typedef InfallibleVector<UniquePtr<Stream>> StreamVector; StreamVector mStreams[(size_t)StreamName::Count]; - // Lock protecting access to this recording. + // Lock protecting access to this file. SpinLock mLock; - // When writing, lock for synchronizing flushes (writer) with other threads - // writing to streams in this recording (readers). + // When writing, lock for synchronizing file flushes (writer) with other + // threads writing to streams in this file (readers). ReadWriteSpinLock mStreamLock; + void Clear() { + mFd = 0; + mMode = READ; + mWriteOffset = 0; + mLastIndexOffset = 0; + for (auto& vector : mStreams) { + vector.clear(); + } + PodZero(&mLock); + PodZero(&mStreamLock); + } + public: - Recording(); - - bool IsWriting() const { return mMode == WRITE; } - bool IsReading() const { return mMode == READ; } + File() { Clear(); } + ~File() { Close(); } - const uint8_t* Data() const { return mContents.begin(); } - size_t Size() const { return mContents.length(); } + bool Open(const char* aName, Mode aMode); + void Close(); - // Get or create a stream in this recording. + bool OpenForWriting() const { return mFd && mMode == WRITE; } + bool OpenForReading() const { return mFd && mMode == READ; } + Stream* OpenStream(StreamName aName, size_t aNameIndex); - // When reading, append additional contents to this recording. - // aUpdatedStreams is optional and filled in with streams whose contents have - // changed, and may have duplicates. - void NewContents(const uint8_t* aContents, size_t aSize, - InfallibleVector<Stream*>* aUpdatedStreams); - - // Prevent/allow other threads to write to streams in this recording. + // Prevent/allow other threads to write to streams in this file. void PreventStreamWrites() { mStreamLock.WriteLock(); } void AllowStreamWrites() { mStreamLock.WriteUnlock(); } - // Flush all streams to the recording. - void Flush(); + // Flush any changes since the last Flush() call to disk, returning whether + // there were such changes. + bool Flush(); + + enum class ReadIndexResult { InvalidFile, EndOfFile, FoundIndex }; + + // Read any data added to the file by a Flush() call. aUpdatedStreams is + // optional and filled in with streams whose contents have changed, and may + // have duplicates. + ReadIndexResult ReadNextIndex(InfallibleVector<Stream*>* aUpdatedStreams); private: - StreamChunkLocation WriteChunk(StreamName aName, size_t aNameIndex, - const char* aStart, size_t aCompressedSize, + StreamChunkLocation WriteChunk(const char* aStart, size_t aCompressedSize, size_t aDecompressedSize, uint64_t aStreamPos, bool aTakeLock); void ReadChunk(char* aDest, const StreamChunkLocation& aChunk); }; } // namespace recordreplay } // namespace mozilla -#endif // mozilla_recordreplay_Recording_h +#endif // mozilla_recordreplay_File_h
--- a/toolkit/recordreplay/HashTable.cpp +++ b/toolkit/recordreplay/HashTable.cpp @@ -108,24 +108,26 @@ class StableHashTableInfo { : mLastKey(nullptr), mLastNewHash(0), mHashGenerator(0), mCallbackStorage(nullptr), mDestroyed(false), mTable(nullptr), mCallbackHash(0) { // Use AllocateMemory, as the result will have RWX permissions. - mCallbackStorage = (uint8_t*)DirectAllocateMemory(CallbackStorageCapacity); + mCallbackStorage = + (uint8_t*)AllocateMemory(CallbackStorageCapacity, MemoryKind::Tracked); MarkValid(); } ~StableHashTableInfo() { MOZ_RELEASE_ASSERT(mHashToKey.empty()); - DirectDeallocateMemory(mCallbackStorage, CallbackStorageCapacity); + DeallocateMemory(mCallbackStorage, CallbackStorageCapacity, + MemoryKind::Tracked); UnmarkValid(); } bool IsDestroyed() { return mDestroyed; } void MarkDestroyed() { MOZ_RELEASE_ASSERT(!IsDestroyed());
--- a/toolkit/recordreplay/Lock.cpp +++ b/toolkit/recordreplay/Lock.cpp @@ -32,52 +32,49 @@ struct LockAcquires { static const size_t NoNextOwner = 0; void ReadAndNotifyNextOwner(Thread* aCurrentThread) { MOZ_RELEASE_ASSERT(IsReplaying()); if (mAcquires->AtEnd()) { mNextOwner = NoNextOwner; } else { mNextOwner = mAcquires->ReadScalar(); - if (!mNextOwner) { - Print("CRASH ReadAndNotifyNextOwner ZERO_ID\n"); - } if (mNextOwner != aCurrentThread->Id()) { Thread::Notify(mNextOwner); } } } }; // Acquires for each lock, indexed by the lock ID. static ChunkAllocator<LockAcquires> gLockAcquires; /////////////////////////////////////////////////////////////////////////////// // Locking Interface /////////////////////////////////////////////////////////////////////////////// // Table mapping native lock pointers to the associated Lock structure, for // every recorded lock in existence. -typedef std::unordered_map<NativeLock*, Lock*> LockMap; +typedef std::unordered_map<void*, Lock*> LockMap; static LockMap* gLocks; static ReadWriteSpinLock gLocksLock; static Lock* CreateNewLock(Thread* aThread, size_t aId) { LockAcquires* info = gLockAcquires.Create(aId); - info->mAcquires = gRecording->OpenStream(StreamName::Lock, aId); + info->mAcquires = gRecordingFile->OpenStream(StreamName::Lock, aId); if (IsReplaying()) { info->ReadAndNotifyNextOwner(aThread); } return new Lock(aId); } /* static */ -void Lock::New(NativeLock* aNativeLock) { +void Lock::New(void* aNativeLock) { Thread* thread = Thread::Current(); RecordingEventSection res(thread); if (!res.CanAccessEvents()) { Destroy(aNativeLock); // Clean up any old lock, as below. return; } thread->Events().RecordOrReplayThreadEvent(ThreadEvent::CreateLock); @@ -102,33 +99,33 @@ void Lock::New(NativeLock* aNativeLock) } gLocks->insert(LockMap::value_type(aNativeLock, lock)); thread->EndDisallowEvents(); } /* static */ -void Lock::Destroy(NativeLock* aNativeLock) { +void Lock::Destroy(void* aNativeLock) { Lock* lock = nullptr; { AutoWriteSpinLock ex(gLocksLock); if (gLocks) { LockMap::iterator iter = gLocks->find(aNativeLock); if (iter != gLocks->end()) { lock = iter->second; gLocks->erase(iter); } } } delete lock; } /* static */ -Lock* Lock::Find(NativeLock* aNativeLock) { +Lock* Lock::Find(void* aNativeLock) { MOZ_RELEASE_ASSERT(IsRecordingOrReplaying()); AutoReadSpinLock ex(gLocksLock); if (gLocks) { LockMap::iterator iter = gLocks->find(aNativeLock); if (iter != gLocks->end()) { // Now that we know the lock is recorded, check whether thread events @@ -144,17 +141,17 @@ Lock* Lock::Find(NativeLock* aNativeLock } return lock; } } return nullptr; } -void Lock::Enter(NativeLock* aNativeLock) { +void Lock::Enter() { Thread* thread = Thread::Current(); RecordingEventSection res(thread); if (!res.CanAccessEvents()) { return; } // Include an event in each thread's record when a lock acquire begins. This @@ -169,37 +166,35 @@ void Lock::Enter(NativeLock* aNativeLock acquires->mAcquires->WriteScalar(thread->Id()); } else { // Wait until this thread is next in line to acquire the lock, or until it // has been instructed to diverge from the recording. while (thread->Id() != acquires->mNextOwner && !thread->MaybeDivergeFromRecording()) { Thread::Wait(); } - if (!thread->HasDivergedFromRecording() && aNativeLock) { - thread->AddOwnedLock(aNativeLock); + if (!thread->HasDivergedFromRecording()) { + mOwner = thread->Id(); } } } -void Lock::Exit(NativeLock* aNativeLock) { +void Lock::Exit() { Thread* thread = Thread::Current(); if (IsReplaying() && !thread->HasDivergedFromRecording()) { - if (aNativeLock) { - thread->RemoveOwnedLock(aNativeLock); - } + mOwner = 0; // Notify the next owner before releasing the lock. LockAcquires* acquires = gLockAcquires.Get(mId); acquires->ReadAndNotifyNextOwner(thread); } } /* static */ -void Lock::LockAcquiresUpdated(size_t aLockId) { +void Lock::LockAquiresUpdated(size_t aLockId) { LockAcquires* acquires = gLockAcquires.MaybeGet(aLockId); if (acquires && acquires->mAcquires && acquires->mNextOwner == LockAcquires::NoNextOwner) { acquires->ReadAndNotifyNextOwner(Thread::Current()); } } // We use a set of Locks to record and replay the order in which atomic @@ -258,17 +253,17 @@ MOZ_EXPORT void RecordReplayInterface_In // When recording, hold a spin lock so that no other thread can access this // same atomic until this access ends. When replaying, we don't need to hold // any actual lock, as the atomic access cannot race and the Lock structure // ensures that accesses happen in the same order. if (IsRecording()) { gAtomicLockOwners[atomicId].Lock(); } - gAtomicLocks[atomicId]->Enter(nullptr); + gAtomicLocks[atomicId]->Enter(); MOZ_RELEASE_ASSERT(thread->AtomicLockId().isNothing()); thread->AtomicLockId().emplace(atomicId); } MOZ_EXPORT void RecordReplayInterface_InternalEndOrderedAtomicAccess() { MOZ_RELEASE_ASSERT(IsRecordingOrReplaying()); @@ -281,15 +276,15 @@ MOZ_EXPORT void RecordReplayInterface_In MOZ_RELEASE_ASSERT(thread->AtomicLockId().isSome()); size_t atomicId = thread->AtomicLockId().ref(); thread->AtomicLockId().reset(); if (IsRecording()) { gAtomicLockOwners[atomicId].Unlock(); } - gAtomicLocks[atomicId]->Exit(nullptr); + gAtomicLocks[atomicId]->Exit(); } } // extern "C" } // namespace recordreplay } // namespace mozilla
--- a/toolkit/recordreplay/Lock.h +++ b/toolkit/recordreplay/Lock.h @@ -5,17 +5,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef mozilla_recordreplay_Lock_h #define mozilla_recordreplay_Lock_h #include "mozilla/PodOperations.h" #include "mozilla/Types.h" -#include "Recording.h" +#include "File.h" namespace mozilla { namespace recordreplay { // Recorded Locks Overview. // // Each platform has some types used for native locks (e.g. pthread_mutex_t or // CRITICAL_SECTION). System APIs which operate on these native locks are @@ -24,43 +24,46 @@ namespace recordreplay { // recorded, and lock acquire orders will be replayed in the same order with // which they originally occurred. // Information about a recorded lock. class Lock { // Unique ID for this lock. size_t mId; + // When replaying, any thread owning this lock as part of the recording. + Atomic<size_t, SequentiallyConsistent, Behavior::DontPreserve> mOwner; + public: - explicit Lock(size_t aId) : mId(aId) { MOZ_ASSERT(aId); } + explicit Lock(size_t aId) : mId(aId), mOwner(0) { MOZ_ASSERT(aId); } size_t Id() { return mId; } // When recording, this is called after the lock has been acquired, and // records the acquire in the lock's acquire order stream. When replaying, // this is called before the lock has been acquired, and blocks the thread // until it is next in line to acquire the lock. - void Enter(NativeLock* aNativeLock); + void Enter(); // This is called before releasing the lock, allowing the next owner to // acquire it while replaying. - void Exit(NativeLock* aNativeLock); + void Exit(); // Create a new Lock corresponding to a native lock, with a fresh ID. - static void New(NativeLock* aNativeLock); + static void New(void* aNativeLock); // Destroy any Lock associated with a native lock. - static void Destroy(NativeLock* aNativeLock); + static void Destroy(void* aNativeLock); // Get the recorded Lock for a native lock if there is one, otherwise null. - static Lock* Find(NativeLock* aNativeLock); + static Lock* Find(void* aNativeLock); // Initialize locking state. static void InitializeLocks(); // Note that new data has been read into a lock's acquires stream. - static void LockAcquiresUpdated(size_t aLockId); + static void LockAquiresUpdated(size_t aLockId); }; } // namespace recordreplay } // namespace mozilla #endif // mozilla_recordreplay_Lock_h
new file mode 100644 --- /dev/null +++ b/toolkit/recordreplay/MemorySnapshot.cpp @@ -0,0 +1,1316 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MemorySnapshot.h" + +#include "ipc/ChildInternal.h" +#include "mozilla/Maybe.h" +#include "DirtyMemoryHandler.h" +#include "InfallibleVector.h" +#include "ProcessRecordReplay.h" +#include "ProcessRewind.h" +#include "SpinLock.h" +#include "SplayTree.h" +#include "Thread.h" + +#include <algorithm> +#include <mach/mach.h> +#include <mach/mach_vm.h> + +// Define to enable the countdown debugging thread. See StartCountdown(). +//#define WANT_COUNTDOWN_THREAD 1 + +namespace mozilla { +namespace recordreplay { + +/////////////////////////////////////////////////////////////////////////////// +// Memory Snapshots Overview. +// +// Snapshots are periodically saved, storing in memory enough information +// for the process to restore the contents of all tracked memory as it +// rewinds to the point the snapshot was made. There are two components to a +// snapshot: +// +// - Stack contents for each thread are completely saved for each snapshot. +// This is handled by ThreadSnapshot.cpp +// +// - Heap and static memory contents (tracked memory) are saved in memory as +// the contents of pages modified before either the the next snapshot +// or the current execution point (if this is the last snapshot). +// This is handled here. +// +// Heap memory is only tracked when allocated with TrackedMemoryKind. +// +// Snapshots of heap/static memory is modeled on the copy-on-write semantics +// used by fork. Instead of actually forking, we use write-protected memory and +// a fault handler to perform the copy-on-write, which both gives more control +// of the snapshot process and allows snapshots to be taken on platforms +// without fork (i.e. Windows). The following example shows how snapshots are +// generated: +// +// #1 Save snapshot A. The initial snapshot tabulates all allocated tracked +// memory in the process, and write-protects all of it. +// +// #2 Write pages P0 and P1. Writing to the pages trips the fault handler. The +// handler creates copies of the initial contents of P0 and P1 (P0a and P1a) +// and unprotects the pages. +// +// #3 Save snapshot B. P0a and P1a, along with any other pages modified +// between A and B, become associated with snapshot A. All modified pages +// are reprotected. +// +// #4 Write pages P1 and P2. Again, writing to the pages trips the fault +// handler and copies P1b and P2b are created and the pages are unprotected. +// +// #5 Save snapshot C. P1b and P2b become associated with snapshot B, and the +// modified pages are reprotected. +// +// If we were to then rewind from C to A, we would read and restore P1b/P2b, +// followed by P0a/P1a. All data associated with snapshots A and later is +// discarded (we can only rewind; we cannot jump forward in time). +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +// Snapshot Threads Overview. +// +// After step #3 above, the main thread has created a diff snapshot with the +// copies of the original contents of pages modified between two snapshots. +// These page copies are initially all in memory. It is the responsibility of +// the snapshot threads to do the following: +// +// 1. When rewinding to the last snapshot, snapshot threads are used to +// restore the original contents of pages using their in-memory copies. +// +// There are a fixed number of snapshot threads that are spawned when the +// first snapshot is saved, which is always at the first checkpoint. Threads are +// each responsible for distinct sets of heap memory pages +// (see AddDirtyPageToWorklist), avoiding synchronization issues between +// different snapshot threads. +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +// Memory Snapshot Structures +/////////////////////////////////////////////////////////////////////////////// + +// A region of allocated memory which should be tracked by MemoryInfo. +struct AllocatedMemoryRegion { + uint8_t* mBase; + size_t mSize; + bool mExecutable; + + AllocatedMemoryRegion() : mBase(nullptr), mSize(0), mExecutable(false) {} + + AllocatedMemoryRegion(uint8_t* aBase, size_t aSize, bool aExecutable) + : mBase(aBase), mSize(aSize), mExecutable(aExecutable) {} + + // For sorting regions by base address. + struct AddressSort { + typedef void* Lookup; + static void* getLookup(const AllocatedMemoryRegion& aRegion) { + return aRegion.mBase; + } + static ssize_t compare(void* aAddress, + const AllocatedMemoryRegion& aRegion) { + return (uint8_t*)aAddress - aRegion.mBase; + } + }; + + // For sorting regions by size, from largest to smallest. + struct SizeReverseSort { + typedef size_t Lookup; + static size_t getLookup(const AllocatedMemoryRegion& aRegion) { + return aRegion.mSize; + } + static ssize_t compare(size_t aSize, const AllocatedMemoryRegion& aRegion) { + return aRegion.mSize - aSize; + } + }; +}; + +// Information about a page which was modified between two snapshots. +struct DirtyPage { + // Base address of the page. + uint8_t* mBase; + + // Copy of the page at the first snapshot. Written by the dirty memory + // handler via HandleDirtyMemoryFault if this is in the active page set, + // otherwise accessed by snapshot threads. + uint8_t* mOriginal; + + bool mExecutable; + + DirtyPage(uint8_t* aBase, uint8_t* aOriginal, bool aExecutable) + : mBase(aBase), mOriginal(aOriginal), mExecutable(aExecutable) {} + + struct AddressSort { + typedef uint8_t* Lookup; + static uint8_t* getLookup(const DirtyPage& aPage) { return aPage.mBase; } + static ssize_t compare(uint8_t* aBase, const DirtyPage& aPage) { + return aBase - aPage.mBase; + } + }; +}; + +// A set of dirty pages that can be searched quickly. +typedef SplayTree<DirtyPage, DirtyPage::AddressSort, + AllocPolicy<MemoryKind::SortedDirtyPageSet>, 4> + SortedDirtyPageSet; + +// A set of dirty pages associated with some snapshot. +struct DirtyPageSet { + // All dirty pages in the set. Pages may be added or destroyed by the main + // thread when all other threads are idle, by the dirty memory handler when + // it is active and this is the active page set, and by the snapshot thread + // which owns this set. + InfallibleVector<DirtyPage, 256, AllocPolicy<MemoryKind::DirtyPageSet>> + mPages; +}; + +// Worklist used by each snapshot thread. +struct SnapshotThreadWorklist { + // Index into gMemoryInfo->mSnapshotWorklists of the thread. + size_t mThreadIndex; + + // Record/replay ID of the thread. + size_t mThreadId; + + // Sets of pages in the thread's worklist. Each set is for a different diff, + // with the oldest snapshots first. + InfallibleVector<DirtyPageSet, 256, AllocPolicy<MemoryKind::Generic>> mSets; +}; + +// Structure used to coordinate activity between the main thread and all +// snapshot threads. The workflow with this structure is as follows: +// +// 1. The main thread calls ActivateBegin(), marking the condition as active +// and notifying each snapshot thread. The main thread blocks in this call. +// +// 2. Each snapshot thread, maybe after waking up, checks the condition, does +// any processing it needs to (knowing the main thread is blocked) and +// then calls WaitUntilNoLongerActive(), blocking in the call. +// +// 3. Once all snapshot threads are blocked in WaitUntilNoLongerActive(), the +// main thread is unblocked from ActivateBegin(). It can then do whatever +// processing it needs to (knowing all snapshot threads are blocked) and +// then calls ActivateEnd(), blocking in the call. +// +// 4. Snapshot threads are now unblocked from WaitUntilNoLongerActive(). The +// main thread does not unblock from ActivateEnd() until all snapshot +// threads have left WaitUntilNoLongerActive(). +// +// The intent with this class is to ensure that the main thread knows exactly +// when the snapshot threads are operating and that there is no potential for +// races between them. +class SnapshotThreadCondition { + Atomic<bool, SequentiallyConsistent, Behavior::DontPreserve> mActive; + Atomic<int32_t, SequentiallyConsistent, Behavior::DontPreserve> mCount; + + public: + void ActivateBegin(); + void ActivateEnd(); + bool IsActive(); + void WaitUntilNoLongerActive(); +}; + +static const size_t NumSnapshotThreads = 8; + +// A set of free regions in the process. There are two of these, for the +// free regions in tracked and untracked memory. +class FreeRegionSet { + // Kind of memory being managed. This also describes the memory used by the + // set itself. + MemoryKind mKind; + + // Lock protecting contents of the structure. + SpinLock mLock; + + // To avoid reentrancy issues when growing the set, a chunk of pages for + // the splay tree is preallocated for use the next time the tree needs to + // expand its size. + static const size_t ChunkPages = 4; + void* mNextChunk; + + // Ensure there is a chunk available for the splay tree. + void MaybeRefillNextChunk(AutoSpinLock& aLockHeld); + + // Get the next chunk from the free region set for this memory kind. + void* TakeNextChunk(); + + struct MyAllocPolicy { + FreeRegionSet& mSet; + + template <typename T> + void free_(T* aPtr, size_t aSize) { + MOZ_CRASH(); + } + + template <typename T> + T* pod_malloc(size_t aNumElems) { + MOZ_RELEASE_ASSERT(sizeof(T) * aNumElems <= ChunkPages * PageSize); + return (T*)mSet.TakeNextChunk(); + } + + explicit MyAllocPolicy(FreeRegionSet& aSet) : mSet(aSet) {} + }; + + // All memory in gMemoryInfo->mTrackedRegions that is not in use at the + // current point in execution. + typedef SplayTree<AllocatedMemoryRegion, + AllocatedMemoryRegion::SizeReverseSort, MyAllocPolicy, + ChunkPages> + Tree; + Tree mRegions; + + void InsertLockHeld(void* aAddress, size_t aSize, AutoSpinLock& aLockHeld); + void* ExtractLockHeld(size_t aSize, AutoSpinLock& aLockHeld); + + public: + explicit FreeRegionSet(MemoryKind aKind) + : mKind(aKind), mRegions(MyAllocPolicy(*this)) {} + + // Get the single region set for a given memory kind. + static FreeRegionSet& Get(MemoryKind aKind); + + // Add a free region to the set. + void Insert(void* aAddress, size_t aSize); + + // Remove a free region of the specified size. If aAddress is specified then + // this address will be prioritized, but a different pointer may be returned. + // The resulting memory will be zeroed. + void* Extract(void* aAddress, size_t aSize); + + // Return whether a memory range intersects this set at all. + bool Intersects(void* aAddress, size_t aSize); +}; + +// Information about the current memory state. The contents of this structure +// are in untracked memory. +struct MemoryInfo { + // Whether new dirty pages or allocated regions are allowed. + bool mMemoryChangesAllowed; + + // Untracked memory regions allocated before the first checkpoint/snapshot. + // This is only accessed on the main thread, and is not a vector because of + // reentrancy issues. + static const size_t MaxInitialUntrackedRegions = 512; + AllocatedMemoryRegion mInitialUntrackedRegions[MaxInitialUntrackedRegions]; + SpinLock mInitialUntrackedRegionsLock; + + // All tracked memory in the process. This may be updated by any thread while + // holding mTrackedRegionsLock. + SplayTree<AllocatedMemoryRegion, AllocatedMemoryRegion::AddressSort, + AllocPolicy<MemoryKind::TrackedRegions>, 4> + mTrackedRegions; + InfallibleVector<AllocatedMemoryRegion, 512, + AllocPolicy<MemoryKind::TrackedRegions>> + mTrackedRegionsByAllocationOrder; + SpinLock mTrackedRegionsLock; + + // Pages from |trackedRegions| modified since the last snapshot. + // Accessed by any thread (usually the dirty memory handler) when memory + // changes are allowed, and by the main thread when memory changes are not + // allowed. + SortedDirtyPageSet mActiveDirty; + SpinLock mActiveDirtyLock; + + // All untracked memory which is available for new allocations. + FreeRegionSet mFreeUntrackedRegions; + + // Worklists for each snapshot thread. + SnapshotThreadWorklist mSnapshotWorklists[NumSnapshotThreads]; + + // Whether snapshot threads should update memory to that when the last saved + // diff was started. + SnapshotThreadCondition mSnapshotThreadsShouldRestore; + + // Whether snapshot threads should idle. + SnapshotThreadCondition mSnapshotThreadsShouldIdle; + + // Counter used by the countdown thread. + Atomic<size_t, SequentiallyConsistent, Behavior::DontPreserve> mCountdown; + + // Information for timers. + double mStartTime; + uint32_t mTimeHits[(size_t)TimerKind::Count]; + double mTimeTotals[(size_t)TimerKind::Count]; + + // Information for memory allocation. + Atomic<ssize_t, Relaxed, Behavior::DontPreserve> + mMemoryBalance[(size_t)MemoryKind::Count]; + + // Recent dirty memory faults. + void* mDirtyMemoryFaults[50]; + + MemoryInfo() + : mMemoryChangesAllowed(true), + mFreeUntrackedRegions(MemoryKind::FreeRegions), + mStartTime(CurrentTime()) { + // The singleton MemoryInfo is allocated with zeroed memory, so other + // fields do not need explicit initialization. + } +}; + +static MemoryInfo* gMemoryInfo = nullptr; + +void SetMemoryChangesAllowed(bool aAllowed) { + MOZ_RELEASE_ASSERT(gMemoryInfo->mMemoryChangesAllowed == !aAllowed); + gMemoryInfo->mMemoryChangesAllowed = aAllowed; +} + +static void EnsureMemoryChangesAllowed() { + while (!gMemoryInfo->mMemoryChangesAllowed) { + ThreadYield(); + } +} + +void StartCountdown(size_t aCount) { gMemoryInfo->mCountdown = aCount; } + +AutoCountdown::AutoCountdown(size_t aCount) { StartCountdown(aCount); } + +AutoCountdown::~AutoCountdown() { StartCountdown(0); } + +#ifdef WANT_COUNTDOWN_THREAD + +static void CountdownThreadMain(void*) { + while (true) { + if (gMemoryInfo->mCountdown && --gMemoryInfo->mCountdown == 0) { + // When debugging hangs in the child process, we can break here in lldb + // to inspect what the process is doing. + child::ReportFatalError(Nothing(), "CountdownThread activated"); + } + ThreadYield(); + } +} + +#endif // WANT_COUNTDOWN_THREAD + +/////////////////////////////////////////////////////////////////////////////// +// Profiling +/////////////////////////////////////////////////////////////////////////////// + +AutoTimer::AutoTimer(TimerKind aKind) : mKind(aKind), mStart(CurrentTime()) {} + +AutoTimer::~AutoTimer() { + if (gMemoryInfo) { + gMemoryInfo->mTimeHits[(size_t)mKind]++; + gMemoryInfo->mTimeTotals[(size_t)mKind] += CurrentTime() - mStart; + } +} + +static const char* gTimerKindNames[] = { +#define DefineTimerKindName(aKind) #aKind, + ForEachTimerKind(DefineTimerKindName) +#undef DefineTimerKindName +}; + +void DumpTimers() { + if (!gMemoryInfo) { + return; + } + Print("Times %.2fs\n", (CurrentTime() - gMemoryInfo->mStartTime) / 1000000.0); + for (size_t i = 0; i < (size_t)TimerKind::Count; i++) { + uint32_t hits = gMemoryInfo->mTimeHits[i]; + double time = gMemoryInfo->mTimeTotals[i]; + Print("%s: %d hits, %.2fs\n", gTimerKindNames[i], (int)hits, + time / 1000000.0); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Snapshot Thread Conditions +/////////////////////////////////////////////////////////////////////////////// + +void SnapshotThreadCondition::ActivateBegin() { + MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); + MOZ_RELEASE_ASSERT(!mActive); + mActive = true; + for (size_t i = 0; i < NumSnapshotThreads; i++) { + Thread::Notify(gMemoryInfo->mSnapshotWorklists[i].mThreadId); + } + while (mCount != NumSnapshotThreads) { + Thread::WaitNoIdle(); + } +} + +void SnapshotThreadCondition::ActivateEnd() { + MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); + MOZ_RELEASE_ASSERT(mActive); + mActive = false; + for (size_t i = 0; i < NumSnapshotThreads; i++) { + Thread::Notify(gMemoryInfo->mSnapshotWorklists[i].mThreadId); + } + while (mCount) { + Thread::WaitNoIdle(); + } +} + +bool SnapshotThreadCondition::IsActive() { + MOZ_RELEASE_ASSERT(!Thread::CurrentIsMainThread()); + return mActive; +} + +void SnapshotThreadCondition::WaitUntilNoLongerActive() { + MOZ_RELEASE_ASSERT(!Thread::CurrentIsMainThread()); + MOZ_RELEASE_ASSERT(mActive); + if (NumSnapshotThreads == ++mCount) { + Thread::Notify(MainThreadId); + } + while (mActive) { + Thread::WaitNoIdle(); + } + if (0 == --mCount) { + Thread::Notify(MainThreadId); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Snapshot Page Allocation +/////////////////////////////////////////////////////////////////////////////// + +// Get a page in untracked memory that can be used as a copy of a tracked page. +static uint8_t* AllocatePageCopy() { + return (uint8_t*)AllocateMemory(PageSize, MemoryKind::PageCopy); +} + +// Free a page allocated by AllocatePageCopy. +static void FreePageCopy(uint8_t* aPage) { + DeallocateMemory(aPage, PageSize, MemoryKind::PageCopy); +} + +/////////////////////////////////////////////////////////////////////////////// +// Page Fault Handling +/////////////////////////////////////////////////////////////////////////////// + +void MemoryMove(void* aDst, const void* aSrc, size_t aSize) { + MOZ_RELEASE_ASSERT((size_t)aDst % sizeof(uint32_t) == 0); + MOZ_RELEASE_ASSERT((size_t)aSrc % sizeof(uint32_t) == 0); + MOZ_RELEASE_ASSERT(aSize % sizeof(uint32_t) == 0); + MOZ_RELEASE_ASSERT((size_t)aDst <= (size_t)aSrc || + (size_t)aDst >= (size_t)aSrc + aSize); + + uint32_t* ndst = (uint32_t*)aDst; + const uint32_t* nsrc = (const uint32_t*)aSrc; + for (size_t i = 0; i < aSize / sizeof(uint32_t); i++) { + ndst[i] = nsrc[i]; + } +} + +void MemoryZero(void* aDst, size_t aSize) { + MOZ_RELEASE_ASSERT((size_t)aDst % sizeof(uint32_t) == 0); + MOZ_RELEASE_ASSERT(aSize % sizeof(uint32_t) == 0); + + // Use volatile here to avoid annoying clang optimizations. + volatile uint32_t* ndst = (uint32_t*)aDst; + for (size_t i = 0; i < aSize / sizeof(uint32_t); i++) { + ndst[i] = 0; + } +} + +// Return whether an address is in a tracked region. This excludes memory that +// is in an active new region and is not write protected. +static bool IsTrackedAddress(void* aAddress, bool* aExecutable) { + AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock); + Maybe<AllocatedMemoryRegion> region = + gMemoryInfo->mTrackedRegions.lookupClosestLessOrEqual(aAddress); + if (region.isSome() && + MemoryContains(region.ref().mBase, region.ref().mSize, aAddress)) { + if (aExecutable) { + *aExecutable = region.ref().mExecutable; + } + return true; + } + return false; +} + +bool HandleDirtyMemoryFault(uint8_t* aAddress) { + EnsureMemoryChangesAllowed(); + + bool different = false; + for (size_t i = ArrayLength(gMemoryInfo->mDirtyMemoryFaults) - 1; i; i--) { + gMemoryInfo->mDirtyMemoryFaults[i] = gMemoryInfo->mDirtyMemoryFaults[i - 1]; + if (gMemoryInfo->mDirtyMemoryFaults[i] != aAddress) { + different = true; + } + } + gMemoryInfo->mDirtyMemoryFaults[0] = aAddress; + if (!different) { + Print("WARNING: Repeated accesses to the same dirty address %p\n", + aAddress); + } + + // Round down to the base of the page. + aAddress = PageBase(aAddress); + + AutoSpinLock lock(gMemoryInfo->mActiveDirtyLock); + + // Check to see if this is already an active dirty page. Once a page has been + // marked as dirty it will be accessible until the next snapshot is taken, + // but it's possible for multiple threads to access the same protected memory + // before we have a chance to unprotect it, in which case we'll end up here + // multiple times for the page. + if (gMemoryInfo->mActiveDirty.maybeLookup(aAddress)) { + return true; + } + + // Crash if this address is not in a tracked region. + bool executable; + if (!IsTrackedAddress(aAddress, &executable)) { + return false; + } + + // Copy the page's original contents into the active dirty set, and unprotect + // it so that execution can proceed. + uint8_t* original = AllocatePageCopy(); + MemoryMove(original, aAddress, PageSize); + gMemoryInfo->mActiveDirty.insert(aAddress, + DirtyPage(aAddress, original, executable)); + DirectUnprotectMemory(aAddress, PageSize, executable); + return true; +} + +bool MemoryRangeIsTracked(void* aAddress, size_t aSize) { + for (uint8_t* ptr = PageBase(aAddress); ptr < (uint8_t*)aAddress + aSize; + ptr += PageSize) { + if (!IsTrackedAddress(ptr, nullptr)) { + return false; + } + } + return true; +} + +void UnrecoverableSnapshotFailure() { + if (gMemoryInfo) { + AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock); + DirectUnprotectMemory(PageBase(&errno), PageSize, false); + for (auto region : gMemoryInfo->mTrackedRegionsByAllocationOrder) { + DirectUnprotectMemory(region.mBase, region.mSize, region.mExecutable, + /* aIgnoreFailures = */ true); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Initial Memory Region Processing +/////////////////////////////////////////////////////////////////////////////// + +void AddInitialUntrackedMemoryRegion(uint8_t* aBase, size_t aSize) { + MOZ_RELEASE_ASSERT(!NumSnapshots()); + + if (gInitializationFailureMessage) { + return; + } + + static void* gSkippedRegion; + if (!gSkippedRegion) { + // We are allocating gMemoryInfo itself, and will directly call this + // function again shortly. + gSkippedRegion = aBase; + return; + } + MOZ_RELEASE_ASSERT(gSkippedRegion == gMemoryInfo); + + AutoSpinLock lock(gMemoryInfo->mInitialUntrackedRegionsLock); + + for (AllocatedMemoryRegion& region : gMemoryInfo->mInitialUntrackedRegions) { + if (!region.mBase) { + region.mBase = aBase; + region.mSize = aSize; + return; + } + } + + // If we end up here then MaxInitialUntrackedRegions should be larger. + MOZ_CRASH(); +} + +static void RemoveInitialUntrackedRegion(uint8_t* aBase, size_t aSize) { + MOZ_RELEASE_ASSERT(!NumSnapshots()); + AutoSpinLock lock(gMemoryInfo->mInitialUntrackedRegionsLock); + + for (AllocatedMemoryRegion& region : gMemoryInfo->mInitialUntrackedRegions) { + if (region.mBase == aBase) { + MOZ_RELEASE_ASSERT(region.mSize == aSize); + region.mBase = nullptr; + region.mSize = 0; + return; + } + } + MOZ_CRASH(); +} + +// Get information about the mapped region containing *aAddress, or the next +// mapped region afterwards if aAddress is not mapped. aAddress is updated to +// the start of that region, and aSize, aProtection, and aMaxProtection are +// updated with the size and protection status of the region. Returns false if +// there are no more mapped regions after *aAddress. +static bool QueryRegion(uint8_t** aAddress, size_t* aSize, + int* aProtection = nullptr, + int* aMaxProtection = nullptr) { + mach_vm_address_t addr = (mach_vm_address_t)*aAddress; + mach_vm_size_t nbytes; + + vm_region_basic_info_64 info; + mach_msg_type_number_t info_count = sizeof(vm_region_basic_info_64); + mach_port_t some_port; + kern_return_t rv = + mach_vm_region(mach_task_self(), &addr, &nbytes, VM_REGION_BASIC_INFO, + (vm_region_info_t)&info, &info_count, &some_port); + if (rv == KERN_INVALID_ADDRESS) { + return false; + } + MOZ_RELEASE_ASSERT(rv == KERN_SUCCESS); + + *aAddress = (uint8_t*)addr; + *aSize = nbytes; + if (aProtection) { + *aProtection = info.protection; + } + if (aMaxProtection) { + *aMaxProtection = info.max_protection; + } + return true; +} + +static void MarkThreadStacksAsUntracked() { + AutoPassThroughThreadEvents pt; + + // Thread stacks are excluded from the tracked regions. + for (size_t i = MainThreadId; i <= MaxThreadId; i++) { + Thread* thread = Thread::GetById(i); + if (!thread->StackBase()) { + continue; + } + + AddInitialUntrackedMemoryRegion(thread->StackBase(), thread->StackSize()); + + // Look for a mapped region with no access permissions immediately after + // the thread stack's allocated region, and include this in the untracked + // memory if found. This is done to avoid confusing breakpad, which will + // scan the allocated memory in this process and will not correctly + // determine stack boundaries if we track these trailing regions and end up + // marking them as readable. + + // Find the mapped region containing the end of the thread's stack. + uint8_t* base = thread->StackBase() + thread->StackSize() - 1; + size_t size; + if (!QueryRegion(&base, &size)) { + MOZ_CRASH("Could not find memory region information for thread stack"); + } + + // Sanity check the region size. Note that we don't mark this entire region + // as untracked, since it may contain TLS data which should be tracked. + MOZ_RELEASE_ASSERT(base + size >= + thread->StackBase() + thread->StackSize()); + + uint8_t* trailing = base + size; + size_t trailingSize; + int protection; + if (QueryRegion(&trailing, &trailingSize, &protection)) { + if (trailing == base + size && protection == 0) { + AddInitialUntrackedMemoryRegion(trailing, trailingSize); + } + } + } +} + +// Given an address region [*aAddress, *aAddress + *aSize], return true if +// there is any intersection with an excluded region +// [aExclude, aExclude + aExcludeSize], set *aSize to contain the subregion +// starting at aAddress which which is not excluded, and *aRemaining and +// *aRemainingSize to any additional subregion which is not excluded. +static bool MaybeExtractMemoryRegion(uint8_t* aAddress, size_t* aSize, + uint8_t** aRemaining, + size_t* aRemainingSize, uint8_t* aExclude, + size_t aExcludeSize) { + uint8_t* addrLimit = aAddress + *aSize; + + // Expand the excluded region out to the containing page boundaries. + MOZ_RELEASE_ASSERT((size_t)aExclude % PageSize == 0); + aExcludeSize = RoundupSizeToPageBoundary(aExcludeSize); + + uint8_t* excludeLimit = aExclude + aExcludeSize; + + if (excludeLimit <= aAddress || addrLimit <= aExclude) { + // No intersection. + return false; + } + + *aSize = std::max<ssize_t>(aExclude - aAddress, 0); + if (aRemaining) { + *aRemaining = excludeLimit; + *aRemainingSize = std::max<ssize_t>(addrLimit - *aRemaining, 0); + } + return true; +} + +// Set *aSize to describe the number of bytes starting at aAddress that should +// be considered tracked memory. *aRemaining and *aRemainingSize are set to any +// remaining portion of the initial region after the first excluded portion +// that is found. +static void ExtractTrackedInitialMemoryRegion(uint8_t* aAddress, size_t* aSize, + uint8_t** aRemaining, + size_t* aRemainingSize) { + // Look for the earliest untracked region which intersects the given region. + const AllocatedMemoryRegion* earliestIntersect = nullptr; + for (const AllocatedMemoryRegion& region : + gMemoryInfo->mInitialUntrackedRegions) { + size_t size = *aSize; + if (MaybeExtractMemoryRegion(aAddress, &size, nullptr, nullptr, + region.mBase, region.mSize)) { + // There was an intersection. + if (!earliestIntersect || region.mBase < earliestIntersect->mBase) { + earliestIntersect = ®ion; + } + } + } + + if (earliestIntersect) { + if (!MaybeExtractMemoryRegion(aAddress, aSize, aRemaining, aRemainingSize, + earliestIntersect->mBase, + earliestIntersect->mSize)) { + MOZ_CRASH(); + } + } else { + // If there is no intersection then the entire region is tracked. + *aRemaining = aAddress + *aSize; + *aRemainingSize = 0; + } +} + +static void AddTrackedRegion(uint8_t* aAddress, size_t aSize, + bool aExecutable) { + if (aSize) { + AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock); + gMemoryInfo->mTrackedRegions.insert( + aAddress, AllocatedMemoryRegion(aAddress, aSize, aExecutable)); + gMemoryInfo->mTrackedRegionsByAllocationOrder.emplaceBack(aAddress, aSize, + aExecutable); + } +} + +// Add any tracked subregions of [aAddress, aAddress + aSize]. +void AddInitialTrackedMemoryRegions(uint8_t* aAddress, size_t aSize, + bool aExecutable) { + while (aSize) { + uint8_t* remaining; + size_t remainingSize; + ExtractTrackedInitialMemoryRegion(aAddress, &aSize, &remaining, + &remainingSize); + + AddTrackedRegion(aAddress, aSize, aExecutable); + + aAddress = remaining; + aSize = remainingSize; + } +} + +static void UpdateNumTrackedRegionsForSnapshot(); + +// Fill in the set of tracked memory regions that are currently mapped within +// this process. +static void ProcessAllInitialMemoryRegions() { + MOZ_ASSERT(!AreThreadEventsPassedThrough()); + + { + AutoPassThroughThreadEvents pt; + for (uint8_t* addr = nullptr;;) { + size_t size; + int maxProtection; + if (!QueryRegion(&addr, &size, nullptr, &maxProtection)) { + break; + } + + // Consider all memory regions that can possibly be written to, even if + // they aren't currently writable. + if (maxProtection & VM_PROT_WRITE) { + MOZ_RELEASE_ASSERT(maxProtection & VM_PROT_READ); + AddInitialTrackedMemoryRegions(addr, size, + maxProtection & VM_PROT_EXECUTE); + } + + addr += size; + } + } + + UpdateNumTrackedRegionsForSnapshot(); + + // Write protect all tracked memory. + AutoDisallowMemoryChanges disallow; + for (const AllocatedMemoryRegion& region : + gMemoryInfo->mTrackedRegionsByAllocationOrder) { + DirectWriteProtectMemory(region.mBase, region.mSize, region.mExecutable); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Free Region Management +/////////////////////////////////////////////////////////////////////////////// + +// All memory in gMemoryInfo->mTrackedRegions that is not in use at the current +// point in execution. +static FreeRegionSet gFreeRegions(MemoryKind::Tracked); + +// The size of gMemoryInfo->mTrackedRegionsByAllocationOrder we expect to see +// at the point of the last snapshot. +static size_t gNumTrackedRegions; + +static void UpdateNumTrackedRegionsForSnapshot() { + MOZ_ASSERT(Thread::CurrentIsMainThread()); + gNumTrackedRegions = gMemoryInfo->mTrackedRegionsByAllocationOrder.length(); +} + +void FixupFreeRegionsAfterRewind() { + // All memory that has been allocated since the associated snapshot was + // reached is now free, and may be reused for new allocations. + size_t newTrackedRegions = + gMemoryInfo->mTrackedRegionsByAllocationOrder.length(); + for (size_t i = gNumTrackedRegions; i < newTrackedRegions; i++) { + const AllocatedMemoryRegion& region = + gMemoryInfo->mTrackedRegionsByAllocationOrder[i]; + gFreeRegions.Insert(region.mBase, region.mSize); + } + gNumTrackedRegions = newTrackedRegions; +} + +/* static */ FreeRegionSet& FreeRegionSet::Get(MemoryKind aKind) { + return (aKind == MemoryKind::Tracked) ? gFreeRegions + : gMemoryInfo->mFreeUntrackedRegions; +} + +void* FreeRegionSet::TakeNextChunk() { + MOZ_RELEASE_ASSERT(mNextChunk); + void* res = mNextChunk; + mNextChunk = nullptr; + return res; +} + +void FreeRegionSet::InsertLockHeld(void* aAddress, size_t aSize, + AutoSpinLock& aLockHeld) { + mRegions.insert(aSize, + AllocatedMemoryRegion((uint8_t*)aAddress, aSize, true)); +} + +void FreeRegionSet::MaybeRefillNextChunk(AutoSpinLock& aLockHeld) { + if (mNextChunk) { + return; + } + + // Look for a free region we can take the next chunk from. + size_t size = ChunkPages * PageSize; + gMemoryInfo->mMemoryBalance[(size_t)mKind] += size; + + mNextChunk = ExtractLockHeld(size, aLockHeld); + + if (!mNextChunk) { + // Allocate memory from the system. + mNextChunk = DirectAllocateMemory(nullptr, size); + RegisterAllocatedMemory(mNextChunk, size, mKind); + } +} + +void FreeRegionSet::Insert(void* aAddress, size_t aSize) { + MOZ_RELEASE_ASSERT(aAddress && aAddress == PageBase(aAddress)); + MOZ_RELEASE_ASSERT(aSize && aSize == RoundupSizeToPageBoundary(aSize)); + + AutoSpinLock lock(mLock); + + MaybeRefillNextChunk(lock); + InsertLockHeld(aAddress, aSize, lock); +} + +void* FreeRegionSet::ExtractLockHeld(size_t aSize, AutoSpinLock& aLockHeld) { + Maybe<AllocatedMemoryRegion> best = + mRegions.lookupClosestLessOrEqual(aSize, /* aRemove = */ true); + if (best.isSome()) { + MOZ_RELEASE_ASSERT(best.ref().mSize >= aSize); + uint8_t* res = best.ref().mBase; + if (best.ref().mSize > aSize) { + InsertLockHeld(res + aSize, best.ref().mSize - aSize, aLockHeld); + } + MemoryZero(res, aSize); + return res; + } + return nullptr; +} + +void* FreeRegionSet::Extract(void* aAddress, size_t aSize) { + MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress)); + MOZ_RELEASE_ASSERT(aSize && aSize == RoundupSizeToPageBoundary(aSize)); + + AutoSpinLock lock(mLock); + + if (aAddress) { + MaybeRefillNextChunk(lock); + + // We were given a point at which to try to place the allocation. Look for + // a free region which contains [aAddress, aAddress + aSize] entirely. + for (typename Tree::Iter iter = mRegions.begin(); !iter.done(); ++iter) { + uint8_t* regionBase = iter.ref().mBase; + uint8_t* regionExtent = regionBase + iter.ref().mSize; + uint8_t* addrBase = (uint8_t*)aAddress; + uint8_t* addrExtent = addrBase + aSize; + if (regionBase <= addrBase && regionExtent >= addrExtent) { + iter.removeEntry(); + if (regionBase < addrBase) { + InsertLockHeld(regionBase, addrBase - regionBase, lock); + } + if (regionExtent > addrExtent) { + InsertLockHeld(addrExtent, regionExtent - addrExtent, lock); + } + MemoryZero(aAddress, aSize); + return aAddress; + } + } + // Fall through and look for a free region at another address. + } + + // No address hint, look for the smallest free region which is larger than + // the desired allocation size. + return ExtractLockHeld(aSize, lock); +} + +bool FreeRegionSet::Intersects(void* aAddress, size_t aSize) { + AutoSpinLock lock(mLock); + for (typename Tree::Iter iter = mRegions.begin(); !iter.done(); ++iter) { + if (MemoryIntersects(iter.ref().mBase, iter.ref().mSize, aAddress, aSize)) { + return true; + } + } + return false; +} + +/////////////////////////////////////////////////////////////////////////////// +// Memory Management +/////////////////////////////////////////////////////////////////////////////// + +void RegisterAllocatedMemory(void* aBaseAddress, size_t aSize, + MemoryKind aKind) { + MOZ_RELEASE_ASSERT(aBaseAddress == PageBase(aBaseAddress)); + MOZ_RELEASE_ASSERT(aSize == RoundupSizeToPageBoundary(aSize)); + + uint8_t* aAddress = reinterpret_cast<uint8_t*>(aBaseAddress); + + if (aKind != MemoryKind::Tracked) { + if (!NumSnapshots()) { + AddInitialUntrackedMemoryRegion(aAddress, aSize); + } + } else if (NumSnapshots()) { + EnsureMemoryChangesAllowed(); + DirectWriteProtectMemory(aAddress, aSize, true); + AddTrackedRegion(aAddress, aSize, true); + } +} + +void CheckFixedMemory(void* aAddress, size_t aSize) { + MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress)); + MOZ_RELEASE_ASSERT(aSize == RoundupSizeToPageBoundary(aSize)); + + if (!NumSnapshots()) { + return; + } + + { + // The memory should already be tracked. Check each page in the allocation + // because there might be tracked regions adjacent to one another, neither + // of which entirely contains this memory. + AutoSpinLock lock(gMemoryInfo->mTrackedRegionsLock); + for (size_t offset = 0; offset < aSize; offset += PageSize) { + uint8_t* page = (uint8_t*)aAddress + offset; + Maybe<AllocatedMemoryRegion> region = + gMemoryInfo->mTrackedRegions.lookupClosestLessOrEqual(page); + if (!region.isSome() || + !MemoryContains(region.ref().mBase, region.ref().mSize, page, + PageSize)) { + MOZ_CRASH("Fixed memory is not tracked!"); + } + } + } + + // The memory should not be free. + if (gFreeRegions.Intersects(aAddress, aSize)) { + MOZ_CRASH("Fixed memory is currently free!"); + } +} + +void RestoreWritableFixedMemory(void* aAddress, size_t aSize) { + MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress)); + MOZ_RELEASE_ASSERT(aSize == RoundupSizeToPageBoundary(aSize)); + + if (!NumSnapshots()) { + return; + } + + AutoSpinLock lock(gMemoryInfo->mActiveDirtyLock); + for (size_t offset = 0; offset < aSize; offset += PageSize) { + uint8_t* page = (uint8_t*)aAddress + offset; + if (gMemoryInfo->mActiveDirty.maybeLookup(page)) { + DirectUnprotectMemory(page, PageSize, true); + } + } +} + +void* AllocateMemoryTryAddress(void* aAddress, size_t aSize, MemoryKind aKind) { + MOZ_RELEASE_ASSERT(aAddress == PageBase(aAddress)); + aSize = RoundupSizeToPageBoundary(aSize); + + if (gMemoryInfo) { + gMemoryInfo->mMemoryBalance[(size_t)aKind] += aSize; + } + + if (NumSnapshots()) { + if (void* res = FreeRegionSet::Get(aKind).Extract(aAddress, aSize)) { + return res; + } + } + + void* res = DirectAllocateMemory(aAddress, aSize); + RegisterAllocatedMemory(res, aSize, aKind); + return res; +} + +void* AllocateMemory(size_t aSize, MemoryKind aKind) { + if (!IsReplaying()) { + return DirectAllocateMemory(nullptr, aSize); + } + return AllocateMemoryTryAddress(nullptr, aSize, aKind); +} + +void DeallocateMemory(void* aAddress, size_t aSize, MemoryKind aKind) { + // Round the supplied region to the containing page boundaries. + aSize += (uint8_t*)aAddress - PageBase(aAddress); + aAddress = PageBase(aAddress); + aSize = RoundupSizeToPageBoundary(aSize); + + if (!aAddress || !aSize) { + return; + } + + if (gMemoryInfo) { + gMemoryInfo->mMemoryBalance[(size_t)aKind] -= aSize; + } + + // Memory is returned to the system before reaching the first checkpoint and + // saving the first snapshot. + if (!NumSnapshots()) { + if (IsReplaying() && aKind != MemoryKind::Tracked) { + RemoveInitialUntrackedRegion((uint8_t*)aAddress, aSize); + } + DirectDeallocateMemory(aAddress, aSize); + return; + } + + if (aKind == MemoryKind::Tracked) { + // For simplicity, all free regions must be executable, so ignore + // deallocated memory in regions that are not executable. + bool executable; + if (!IsTrackedAddress(aAddress, &executable) || !executable) { + return; + } + } + + // Mark this region as free, but do not unmap it. It will become usable for + // later allocations, but will not need to be remapped if we end up + // rewinding to a point where this memory was in use. + FreeRegionSet::Get(aKind).Insert(aAddress, aSize); +} + +/////////////////////////////////////////////////////////////////////////////// +// Snapshot Threads +/////////////////////////////////////////////////////////////////////////////// + +// While on a snapshot thread, restore the contents of all pages belonging to +// this thread which were modified since the last recorded diff snapshot. +static void SnapshotThreadRestoreLastDiffSnapshot( + SnapshotThreadWorklist* aWorklist) { + DirtyPageSet& set = aWorklist->mSets.back(); + + // Copy the original contents of all pages. + for (size_t index = 0; index < set.mPages.length(); index++) { + const DirtyPage& page = set.mPages[index]; + MOZ_RELEASE_ASSERT(page.mOriginal); + DirectUnprotectMemory(page.mBase, PageSize, page.mExecutable); + MemoryMove(page.mBase, page.mOriginal, PageSize); + DirectWriteProtectMemory(page.mBase, PageSize, page.mExecutable); + FreePageCopy(page.mOriginal); + } + + // Remove the set from the worklist, if necessary. + if (!aWorklist->mSets.empty()) { + MOZ_RELEASE_ASSERT(&set == &aWorklist->mSets.back()); + aWorklist->mSets.popBack(); + } +} + +// Start routine for a snapshot thread. +void SnapshotThreadMain(void* aArgument) { + size_t threadIndex = (size_t)aArgument; + SnapshotThreadWorklist* worklist = + &gMemoryInfo->mSnapshotWorklists[threadIndex]; + worklist->mThreadIndex = threadIndex; + + while (true) { + // If the main thread is waiting for us to restore the most recent diff, + // then do so and notify the main thread we finished. + if (gMemoryInfo->mSnapshotThreadsShouldRestore.IsActive()) { + SnapshotThreadRestoreLastDiffSnapshot(worklist); + gMemoryInfo->mSnapshotThreadsShouldRestore.WaitUntilNoLongerActive(); + } + + // Idle if the main thread wants us to. + if (gMemoryInfo->mSnapshotThreadsShouldIdle.IsActive()) { + gMemoryInfo->mSnapshotThreadsShouldIdle.WaitUntilNoLongerActive(); + } + + // Idle until notified by the main thread. + Thread::WaitNoIdle(); + } +} + +// An alternative to memcmp that can be called from any place. +static bool MemoryEquals(void* aDst, void* aSrc, size_t aSize) { + MOZ_ASSERT((size_t)aDst % sizeof(size_t) == 0); + MOZ_ASSERT((size_t)aSrc % sizeof(size_t) == 0); + MOZ_ASSERT(aSize % sizeof(size_t) == 0); + + size_t* ndst = (size_t*)aDst; + size_t* nsrc = (size_t*)aSrc; + for (size_t i = 0; i < aSize / sizeof(size_t); i++) { + if (ndst[i] != nsrc[i]) { + return false; + } + } + return true; +} + +// Add a page to the last set in some snapshot thread's worklist. This is +// called on the main thread while the snapshot thread is idle. +static void AddDirtyPageToWorklist(uint8_t* aAddress, uint8_t* aOriginal, + bool aExecutable) { + // Distribute pages to snapshot threads using the base address of a page. + // This guarantees that the same page will be consistently assigned to the + // same thread as different snapshots are taken. + MOZ_ASSERT((size_t)aAddress % PageSize == 0); + if (MemoryEquals(aAddress, aOriginal, PageSize)) { + FreePageCopy(aOriginal); + } else { + size_t pageIndex = ((size_t)aAddress / PageSize) % NumSnapshotThreads; + SnapshotThreadWorklist* worklist = + &gMemoryInfo->mSnapshotWorklists[pageIndex]; + MOZ_RELEASE_ASSERT(!worklist->mSets.empty()); + DirtyPageSet& set = worklist->mSets.back(); + set.mPages.emplaceBack(aAddress, aOriginal, aExecutable); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Snapshot Interface +/////////////////////////////////////////////////////////////////////////////// + +void InitializeMemorySnapshots() { + MOZ_RELEASE_ASSERT(gMemoryInfo == nullptr); + void* memory = AllocateMemory(sizeof(MemoryInfo), MemoryKind::Generic); + gMemoryInfo = new (memory) MemoryInfo(); + + // Mark gMemoryInfo as untracked. See AddInitialUntrackedMemoryRegion. + AddInitialUntrackedMemoryRegion(reinterpret_cast<uint8_t*>(memory), + sizeof(MemoryInfo)); +} + +void InitializeCountdownThread() { +#ifdef WANT_COUNTDOWN_THREAD + Thread::SpawnNonRecordedThread(CountdownThreadMain, nullptr); +#endif +} + +void TakeFirstMemorySnapshot() { + MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); + MOZ_RELEASE_ASSERT(gMemoryInfo->mTrackedRegions.empty()); + + // Spawn all snapshot threads. + { + AutoPassThroughThreadEvents pt; + + for (size_t i = 0; i < NumSnapshotThreads; i++) { + Thread* thread = + Thread::SpawnNonRecordedThread(SnapshotThreadMain, (void*)i); + gMemoryInfo->mSnapshotWorklists[i].mThreadId = thread->Id(); + } + } + + // All threads should have been created by now. + MarkThreadStacksAsUntracked(); + + // Fill in the tracked regions for the process. + ProcessAllInitialMemoryRegions(); +} + +void TakeDiffMemorySnapshot() { + MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); + + UpdateNumTrackedRegionsForSnapshot(); + + AutoDisallowMemoryChanges disallow; + + // Stop all snapshot threads while we modify their worklists. + gMemoryInfo->mSnapshotThreadsShouldIdle.ActivateBegin(); + + // Add a DirtyPageSet to each snapshot thread's worklist for this snapshot. + for (size_t i = 0; i < NumSnapshotThreads; i++) { + SnapshotThreadWorklist* worklist = &gMemoryInfo->mSnapshotWorklists[i]; + worklist->mSets.emplaceBack(); + } + + // Distribute remaining active dirty pages to the snapshot thread worklists. + for (SortedDirtyPageSet::Iter iter = gMemoryInfo->mActiveDirty.begin(); + !iter.done(); ++iter) { + AddDirtyPageToWorklist(iter.ref().mBase, iter.ref().mOriginal, + iter.ref().mExecutable); + DirectWriteProtectMemory(iter.ref().mBase, PageSize, + iter.ref().mExecutable); + } + + gMemoryInfo->mActiveDirty.clear(); + + // Allow snapshot threads to resume execution. + gMemoryInfo->mSnapshotThreadsShouldIdle.ActivateEnd(); +} + +void RestoreMemoryToLastSnapshot() { + MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); + MOZ_RELEASE_ASSERT(!gMemoryInfo->mMemoryChangesAllowed); + + // Restore all dirty regions that have been modified since the last + // snapshot was saved/restored. + for (SortedDirtyPageSet::Iter iter = gMemoryInfo->mActiveDirty.begin(); + !iter.done(); ++iter) { + MemoryMove(iter.ref().mBase, iter.ref().mOriginal, PageSize); + FreePageCopy(iter.ref().mOriginal); + DirectWriteProtectMemory(iter.ref().mBase, PageSize, + iter.ref().mExecutable); + } + gMemoryInfo->mActiveDirty.clear(); +} + +void RestoreMemoryToLastDiffSnapshot() { + MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); + MOZ_RELEASE_ASSERT(!gMemoryInfo->mMemoryChangesAllowed); + MOZ_RELEASE_ASSERT(gMemoryInfo->mActiveDirty.empty()); + + // Wait while the snapshot threads restore all pages modified since the diff + // snapshot was recorded. + gMemoryInfo->mSnapshotThreadsShouldRestore.ActivateBegin(); + gMemoryInfo->mSnapshotThreadsShouldRestore.ActivateEnd(); +} + +size_t GetMemoryUsage(MemoryKind aKind) { + if (!gMemoryInfo) { + return 0; + } + return gMemoryInfo->mMemoryBalance[(size_t)aKind]; +} + +} // namespace recordreplay +} // namespace mozilla
new file mode 100644 --- /dev/null +++ b/toolkit/recordreplay/MemorySnapshot.h @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_recordreplay_MemorySnapshot_h +#define mozilla_recordreplay_MemorySnapshot_h + +#include "mozilla/Types.h" +#include "ProcessRecordReplay.h" + +namespace mozilla { +namespace recordreplay { + +// Memory Snapshots Overview. +// +// As described in ProcessRewind.h, periodically snapshots are saved so that +// their state can be restored later. Memory snapshots are used to save and +// restore the contents of all heap memory: everything except thread stacks +// (see ThreadSnapshot.h for saving and restoring these) and untracked memory +// (which is not saved or restored, see ProcessRecordReplay.h). +// +// Each memory snapshot is a diff of the heap memory contents compared to the +// next one. See MemorySnapshot.cpp for how diffs are represented and computed. +// +// Rewinding must restore the exact contents of heap memory that existed when +// the target snapshot was reached. Because of this, memory that is allocated +// after a point when a snapshot has been saved will never actually be returned +// to the system. We instead keep a set of free blocks that are unused at the +// current point of execution and are available to satisfy new allocations. + +// Make sure that a block of memory in a fixed allocation is already allocated. +void CheckFixedMemory(void* aAddress, size_t aSize); + +// After marking a block of memory in a fixed allocation as non-writable, +// restore writability to any dirty pages in the range. +void RestoreWritableFixedMemory(void* aAddress, size_t aSize); + +// Allocate memory, trying to use a specific address if provided but only if +// it is free. +void* AllocateMemoryTryAddress(void* aAddress, size_t aSize, MemoryKind aKind); + +// Note a range of memory that was just allocated from the system, and the +// kind of memory allocation that was performed. +void RegisterAllocatedMemory(void* aBaseAddress, size_t aSize, + MemoryKind aKind); + +// Exclude a region of memory from snapshots, before the first snapshot has +// been taken. +void AddInitialUntrackedMemoryRegion(uint8_t* aBase, size_t aSize); + +// Return whether a range of memory is in a tracked region. This excludes +// memory that was allocated after the last snapshot and is not write +// protected. +bool MemoryRangeIsTracked(void* aAddress, size_t aSize); + +// Initialize the memory snapshots system. +void InitializeMemorySnapshots(); + +// Take the first heap memory snapshot. +void TakeFirstMemorySnapshot(); + +// Take a differential heap memory snapshot compared to the last one. +void TakeDiffMemorySnapshot(); + +// Restore all heap memory to its state when the most recent snapshot was +// taken. +void RestoreMemoryToLastSnapshot(); + +// Restore all heap memory to its state at a snapshot where a complete diff +// was saved vs. the following snapshot. This requires that no tracked heap +// memory has been changed since the last snapshot. +void RestoreMemoryToLastDiffSnapshot(); + +// Set whether to allow changes to tracked heap memory at this point. If such +// changes occur when they are not allowed then the process will crash. +void SetMemoryChangesAllowed(bool aAllowed); + +struct MOZ_RAII AutoDisallowMemoryChanges { + AutoDisallowMemoryChanges() { SetMemoryChangesAllowed(false); } + ~AutoDisallowMemoryChanges() { SetMemoryChangesAllowed(true); } +}; + +// After a SEGV on the specified address, check if the violation occurred due +// to the memory having been write protected by the snapshot mechanism. This +// function returns whether the fault has been handled and execution may +// continue. +bool HandleDirtyMemoryFault(uint8_t* aAddress); + +// For debugging, note a point where we hit an unrecoverable failure and try +// to make things easier for the debugger. +void UnrecoverableSnapshotFailure(); + +// After rewinding, mark all memory that has been allocated since the snapshot +// was taken as free. +void FixupFreeRegionsAfterRewind(); + +// When WANT_COUNTDOWN_THREAD is defined (see MemorySnapshot.cpp), set a count +// that, after a thread consumes it, causes the thread to report a fatal error. +// This is used for debugging and is a workaround for lldb often being unable +// to interrupt a running process. +void StartCountdown(size_t aCount); + +// Per StartCountdown, set a countdown and remove it on destruction. +struct MOZ_RAII AutoCountdown { + explicit AutoCountdown(size_t aCount); + ~AutoCountdown(); +}; + +// Initialize the thread consuming the countdown. +void InitializeCountdownThread(); + +// This is an alternative to memmove/memcpy that can be called in areas where +// faults in write protected memory are not allowed. It's hard to avoid dynamic +// code loading when calling memmove/memcpy directly. +void MemoryMove(void* aDst, const void* aSrc, size_t aSize); + +// Similarly, zero out a range of memory without doing anything weird with +// dynamic code loading. +void MemoryZero(void* aDst, size_t aSize); + +// Get the amount of allocated memory used by data of the specified kind. +size_t GetMemoryUsage(MemoryKind aKind); + +} // namespace recordreplay +} // namespace mozilla + +#endif // mozilla_recordreplay_MemorySnapshot_h
rename from toolkit/recordreplay/ExternalCall.cpp rename to toolkit/recordreplay/MiddlemanCall.cpp --- a/toolkit/recordreplay/ExternalCall.cpp +++ b/toolkit/recordreplay/MiddlemanCall.cpp @@ -1,480 +1,458 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "ExternalCall.h" +#include "MiddlemanCall.h" #include <unordered_map> namespace mozilla { namespace recordreplay { -/////////////////////////////////////////////////////////////////////////////// -// Replaying and External Process State -/////////////////////////////////////////////////////////////////////////////// - -typedef std::unordered_map<ExternalCallId, ExternalCall*> CallsByIdMap; -typedef std::unordered_map<const void*, ExternalCallId> CallsByValueMap; +typedef std::unordered_map<const void*, MiddlemanCall*> MiddlemanCallMap; -// State used for keeping track of external calls in either a replaying -// process or external process. -struct ExternalCallState { - // In a replaying or external process, association between ExternalCallIds - // and the associated ExternalCall, for each encountered call. - CallsByIdMap mCallsById; +// State used for keeping track of middleman calls in either a replaying +// process or middleman process. +struct MiddlemanCallState { + // In a replaying or middleman process, all middleman calls that have been + // encountered, indexed by their ID. + InfallibleVector<MiddlemanCall*> mCalls; - // In a replaying or external process, association between values produced by - // a external call and the call's ID. This is the inverse of each call's - // mValue field, except that if multiple calls have produced the same value - // this maps that value to the most recent one. - CallsByValueMap mCallsByValue; + // In a replaying or middleman process, association between values produced by + // a middleman call and the call itself. + MiddlemanCallMap mCallMap; - // In an external process, any buffers allocated for performed calls. + // In a middleman process, any buffers allocated for performed calls. InfallibleVector<void*> mAllocatedBuffers; }; -// In a replaying process, all external call state. In an external process, -// state for the call currently being processed. -static ExternalCallState* gState; +// In a replaying process, all middleman call state. In a middleman process, +// state for the child currently being processed. +static MiddlemanCallState* gState; -// In a replaying process, all external calls found in the recording that have -// not been flushed to the root replaying process. -static StaticInfallibleVector<ExternalCall*> gUnflushedCalls; +// In a middleman process, middleman call state for each child process, indexed +// by the child ID. +static StaticInfallibleVector<MiddlemanCallState*> gStatePerChild; -// In a replaying process, lock protecting external call state. In the -// external process, all accesses occur on the main thread. +// In a replaying process, lock protecting middleman call state. In the +// middleman, all accesses occur on the main thread. static Monitor* gMonitor; -void InitializeExternalCalls() { +void InitializeMiddlemanCalls() { MOZ_RELEASE_ASSERT(IsRecordingOrReplaying() || IsMiddleman()); if (IsReplaying()) { - gState = new ExternalCallState(); + gState = new MiddlemanCallState(); gMonitor = new Monitor(); } } -static void SetExternalCallValue(ExternalCall* aCall, const void* aValue) { - aCall->mValue.reset(); - aCall->mValue.emplace(aValue); +// Apply the ReplayInput phase to aCall and any calls it depends on that have +// not been sent to the middleman yet, filling aOutgoingCalls with the set of +// such calls. +static bool GatherDependentCalls( + InfallibleVector<MiddlemanCall*>& aOutgoingCalls, MiddlemanCall* aCall) { + MOZ_RELEASE_ASSERT(!aCall->mSent); + aCall->mSent = true; + + const Redirection& redirection = GetRedirection(aCall->mCallId); + + CallArguments arguments; + aCall->mArguments.CopyTo(&arguments); + + InfallibleVector<MiddlemanCall*> dependentCalls; - gState->mCallsByValue.erase(aValue); - gState->mCallsByValue.insert(CallsByValueMap::value_type(aValue, aCall->mId)); -} + MiddlemanCallContext cx(aCall, &arguments, MiddlemanCallPhase::ReplayInput); + cx.mDependentCalls = &dependentCalls; + redirection.mMiddlemanCall(cx); + if (cx.mFailed) { + if (child::CurrentRepaintCannotFail()) { + child::ReportFatalError(Nothing(), "Middleman call input failed: %s\n", + redirection.mName); + } + return false; + } -static void GatherDependentCalls( - InfallibleVector<ExternalCall*>& aOutgoingCalls, ExternalCall* aCall) { - for (ExternalCall* existing : aOutgoingCalls) { - if (existing == aCall) { - return; + for (MiddlemanCall* dependent : dependentCalls) { + if (!dependent->mSent && !GatherDependentCalls(aOutgoingCalls, dependent)) { + return false; } } aOutgoingCalls.append(aCall); - - for (ExternalCallId dependentId : aCall->mDependentCalls) { - auto iter = gState->mCallsById.find(dependentId); - MOZ_RELEASE_ASSERT(iter != gState->mCallsById.end()); - ExternalCall* dependent = iter->second; - - GatherDependentCalls(aOutgoingCalls, dependent); - } + return true; } -bool OnExternalCall(size_t aCallId, CallArguments* aArguments, bool aDiverged) { +bool SendCallToMiddleman(size_t aCallId, CallArguments* aArguments, + bool aDiverged) { MOZ_RELEASE_ASSERT(IsReplaying()); const Redirection& redirection = GetRedirection(aCallId); - MOZ_RELEASE_ASSERT(redirection.mExternalCall); - - const char* messageName = ""; - if (!strcmp(redirection.mName, "objc_msgSend")) { - messageName = aArguments->Arg<1, const char*>(); - } - - if (aDiverged) { - PrintSpew("OnExternalCall Diverged %s %s\n", redirection.mName, messageName); - } + MOZ_RELEASE_ASSERT(redirection.mMiddlemanCall); MonitorAutoLock lock(*gMonitor); - // Allocate the new ExternalCall. - ExternalCall* call = new ExternalCall(); - call->mCallId = aCallId; + // Allocate and fill in a new MiddlemanCall. + size_t id = gState->mCalls.length(); + MiddlemanCall* newCall = new MiddlemanCall(); + gState->mCalls.emplaceBack(newCall); + newCall->mId = id; + newCall->mCallId = aCallId; + newCall->mArguments.CopyFrom(aArguments); - // Save all the call's inputs. + // Perform the ReplayPreface phase on the new call. { - ExternalCallContext cx(call, aArguments, - ExternalCallPhase::SaveInput); - redirection.mExternalCall(cx); + MiddlemanCallContext cx(newCall, aArguments, + MiddlemanCallPhase::ReplayPreface); + redirection.mMiddlemanCall(cx); if (cx.mFailed) { - delete call; - if (child::CurrentRepaintCannotFail() && aDiverged) { - child::ReportFatalError("External call input failed: %s\n", + delete newCall; + gState->mCalls.popBack(); + if (child::CurrentRepaintCannotFail()) { + child::ReportFatalError(Nothing(), + "Middleman call preface failed: %s\n", redirection.mName); } return false; } } - call->ComputeId(); - - bool isNewCall = false; - auto iter = gState->mCallsById.find(call->mId); - if (iter == gState->mCallsById.end()) { - // We haven't seen this call before. - isNewCall = true; - gState->mCallsById.insert(CallsByIdMap::value_type(call->mId, call)); - } else { - // We've seen this call before, so use the old copy. - delete call; - call = iter->second; - - // Reuse this call's result if we need to restore the output. - if (aDiverged) { - ExternalCallContext cx(call, aArguments, - ExternalCallPhase::RestoreOutput); - redirection.mExternalCall(cx); - return true; - } - } - - // If we have not diverged from the recording, we already have the outputs - // we need. Run the SaveOutput phase to capture these so that we can reuse - // them later and associate any system outputs with the call. + // Other phases will not run if we have not diverged from the recording. + // Any outputs for the call have been handled by the SaveOutput hook. if (!aDiverged) { - ExternalCallContext cx(call, aArguments, - ExternalCallPhase::SaveOutput); - redirection.mExternalCall(cx); - if (isNewCall) { - gUnflushedCalls.append(call); - } return true; } - PrintSpew("OnExternalCall Send %s %s\n", redirection.mName, messageName); + // Perform the ReplayInput phase on the new call and any others it depends on. + InfallibleVector<MiddlemanCall*> outgoingCalls; + if (!GatherDependentCalls(outgoingCalls, newCall)) { + for (MiddlemanCall* call : outgoingCalls) { + call->mSent = false; + } + return false; + } - // Gather any calls this one transitively depends on. - InfallibleVector<ExternalCall*> outgoingCalls; - GatherDependentCalls(outgoingCalls, call); - - // Encode all calls that need to be performed, in the order to perform them. + // Encode all calls we are sending to the middleman. InfallibleVector<char> inputData; BufferStream inputStream(&inputData); - for (int i = outgoingCalls.length() - 1; i >= 0; i--) { - outgoingCalls[i]->EncodeInput(inputStream); + for (MiddlemanCall* call : outgoingCalls) { + call->EncodeInput(inputStream); } - // Synchronously wait for the call result. + // Perform the calls synchronously in the middleman. InfallibleVector<char> outputData; - child::SendExternalCallRequest(call->mId, - inputData.begin(), inputData.length(), - &outputData); + child::SendMiddlemanCallRequest(inputData.begin(), inputData.length(), + &outputData); + + // Decode outputs for the calls just sent, and perform the ReplayOutput phase + // on any older dependent calls we sent. + BufferStream outputStream(outputData.begin(), outputData.length()); + for (MiddlemanCall* call : outgoingCalls) { + call->DecodeOutput(outputStream); - // Decode the external call's output. - BufferStream outputStream(outputData.begin(), outputData.length()); - call->DecodeOutput(outputStream); + if (call != newCall) { + CallArguments oldArguments; + call->mArguments.CopyTo(&oldArguments); + MiddlemanCallContext cx(call, &oldArguments, + MiddlemanCallPhase::ReplayOutput); + cx.mReplayOutputIsOld = true; + GetRedirection(call->mCallId).mMiddlemanCall(cx); + } + } - ExternalCallContext cx(call, aArguments, ExternalCallPhase::RestoreOutput); - redirection.mExternalCall(cx); + // Perform the ReplayOutput phase to fill in outputs for the current call. + newCall->mArguments.CopyTo(aArguments); + MiddlemanCallContext cx(newCall, aArguments, + MiddlemanCallPhase::ReplayOutput); + redirection.mMiddlemanCall(cx); return true; } -void ProcessExternalCall(const char* aInputData, size_t aInputSize, - InfallibleVector<char>* aOutputData) { +void ProcessMiddlemanCall(size_t aChildId, const char* aInputData, + size_t aInputSize, + InfallibleVector<char>* aOutputData) { MOZ_RELEASE_ASSERT(IsMiddleman()); - gState = new ExternalCallState(); - auto& calls = gState->mCallsById; + while (aChildId >= gStatePerChild.length()) { + gStatePerChild.append(nullptr); + } + if (!gStatePerChild[aChildId]) { + gStatePerChild[aChildId] = new MiddlemanCallState(); + } + gState = gStatePerChild[aChildId]; BufferStream inputStream(aInputData, aInputSize); - ExternalCall* lastCall = nullptr; - - ExternalCallContext::ReleaseCallbackVector releaseCallbacks; + BufferStream outputStream(aOutputData); while (!inputStream.IsEmpty()) { - ExternalCall* call = new ExternalCall(); + MiddlemanCall* call = new MiddlemanCall(); call->DecodeInput(inputStream); const Redirection& redirection = GetRedirection(call->mCallId); - MOZ_RELEASE_ASSERT(redirection.mExternalCall); - - PrintSpew("ProcessExternalCall %lu %s\n", call->mId, redirection.mName); + MOZ_RELEASE_ASSERT(redirection.mMiddlemanCall); CallArguments arguments; + call->mArguments.CopyTo(&arguments); bool skipCall; { - ExternalCallContext cx(call, &arguments, ExternalCallPhase::RestoreInput); - redirection.mExternalCall(cx); - skipCall = cx.mSkipExecuting; + MiddlemanCallContext cx(call, &arguments, + MiddlemanCallPhase::MiddlemanInput); + redirection.mMiddlemanCall(cx); + skipCall = cx.mSkipCallInMiddleman; } if (!skipCall) { RecordReplayInvokeCall(redirection.mBaseFunction, &arguments); } { - ExternalCallContext cx(call, &arguments, ExternalCallPhase::SaveOutput); - cx.mReleaseCallbacks = &releaseCallbacks; - redirection.mExternalCall(cx); + MiddlemanCallContext cx(call, &arguments, + MiddlemanCallPhase::MiddlemanOutput); + redirection.mMiddlemanCall(cx); } - lastCall = call; + call->mArguments.CopyFrom(&arguments); + call->EncodeOutput(outputStream); - MOZ_RELEASE_ASSERT(calls.find(call->mId) == calls.end()); - calls.insert(CallsByIdMap::value_type(call->mId, call)); + while (call->mId >= gState->mCalls.length()) { + gState->mCalls.emplaceBack(nullptr); + } + MOZ_RELEASE_ASSERT(!gState->mCalls[call->mId]); + gState->mCalls[call->mId] = call; } - BufferStream outputStream(aOutputData); - lastCall->EncodeOutput(outputStream); - - for (const auto& callback : releaseCallbacks) { - callback(); - } - - for (auto iter = calls.begin(); iter != calls.end(); ++iter) { - delete iter->second; - } - - for (auto buffer : gState->mAllocatedBuffers) { - free(buffer); - } - - delete gState; gState = nullptr; } -void* ExternalCallContext::AllocateBytes(size_t aSize) { +void* MiddlemanCallContext::AllocateBytes(size_t aSize) { void* rv = malloc(aSize); - // In an external process, any buffers we allocate live until the calls are - // reset. In a replaying process, the buffers will live forever, to match the - // lifetime of the ExternalCall itself. + // In a middleman process, any buffers we allocate live until the calls are + // reset. In a replaying process, the buffers will either live forever + // (if they are allocated in the ReplayPreface phase, to match the lifetime + // of the MiddlemanCall itself) or will be recovered when we rewind after we + // are done with our divergence from the recording (any other phase). if (IsMiddleman()) { gState->mAllocatedBuffers.append(rv); } return rv; } -void FlushExternalCalls() { - MonitorAutoLock lock(*gMonitor); +void ResetMiddlemanCalls(size_t aChildId) { + MOZ_RELEASE_ASSERT(IsMiddleman()); - for (ExternalCall* call : gUnflushedCalls) { - InfallibleVector<char> outputData; - BufferStream outputStream(&outputData); - call->EncodeOutput(outputStream); + if (aChildId >= gStatePerChild.length()) { + return; + } - child::SendExternalCallOutput(call->mId, outputData.begin(), - outputData.length()); + gState = gStatePerChild[aChildId]; + if (!gState) { + return; } - gUnflushedCalls.clear(); -} - -/////////////////////////////////////////////////////////////////////////////// -// External Call Caching -/////////////////////////////////////////////////////////////////////////////// + for (MiddlemanCall* call : gState->mCalls) { + if (call) { + CallArguments arguments; + call->mArguments.CopyTo(&arguments); -// In root replaying processes, the outputs produced by assorted external calls -// are cached for fulfilling future external call requests. -struct ExternalCallOutput { - char* mOutput; - size_t mOutputSize; -}; - -// Protected by gMonitor. Accesses can occur on any thread. -typedef std::unordered_map<ExternalCallId, ExternalCallOutput> CallOutputMap; -static CallOutputMap* gCallOutputMap; - -void AddExternalCallOutput(ExternalCallId aId, const char* aOutput, - size_t aOutputSize) { - MonitorAutoLock lock(*gMonitor); - - if (!gCallOutputMap) { - gCallOutputMap = new CallOutputMap(); + MiddlemanCallContext cx(call, &arguments, + MiddlemanCallPhase::MiddlemanRelease); + GetRedirection(call->mCallId).mMiddlemanCall(cx); + } } - ExternalCallOutput output; - output.mOutput = new char[aOutputSize]; - memcpy(output.mOutput, aOutput, aOutputSize); - output.mOutputSize = aOutputSize; - gCallOutputMap->insert(CallOutputMap::value_type(aId, output)); -} - -bool HasExternalCallOutput(ExternalCallId aId, - InfallibleVector<char>* aOutput) { - MonitorAutoLock lock(*gMonitor); - - if (!gCallOutputMap) { - return false; + // Delete the calls in a second pass. The MiddlemanRelease phase depends on + // previous middleman calls still existing. + for (MiddlemanCall* call : gState->mCalls) { + delete call; } - auto iter = gCallOutputMap->find(aId); - if (iter == gCallOutputMap->end()) { - return false; + gState->mCalls.clear(); + for (auto buffer : gState->mAllocatedBuffers) { + free(buffer); } + gState->mAllocatedBuffers.clear(); + gState->mCallMap.clear(); - aOutput->append(iter->second.mOutput, iter->second.mOutputSize); - return true; + gState = nullptr; } /////////////////////////////////////////////////////////////////////////////// // System Values /////////////////////////////////////////////////////////////////////////////// -static ExternalCall* LookupExternalCall(const void* aThing) { - CallsByValueMap::const_iterator iter = gState->mCallsByValue.find(aThing); - if (iter != gState->mCallsByValue.end()) { - CallsByIdMap::const_iterator iter2 = gState->mCallsById.find(iter->second); - if (iter2 != gState->mCallsById.end()) { - return iter2->second; - } +static void AddMiddlemanCallValue(const void* aThing, MiddlemanCall* aCall) { + gState->mCallMap.erase(aThing); + gState->mCallMap.insert(MiddlemanCallMap::value_type(aThing, aCall)); +} + +static MiddlemanCall* LookupMiddlemanCall(const void* aThing) { + MiddlemanCallMap::const_iterator iter = gState->mCallMap.find(aThing); + if (iter != gState->mCallMap.end()) { + return iter->second; } return nullptr; } -static Maybe<const void*> GetExternalCallValue(ExternalCallId aId) { - auto iter = gState->mCallsById.find(aId); - if (iter != gState->mCallsById.end()) { - return iter->second->mValue; - } - return Nothing(); +static const void* GetMiddlemanCallValue(size_t aId) { + MOZ_RELEASE_ASSERT(IsMiddleman()); + MOZ_RELEASE_ASSERT(aId < gState->mCalls.length() && gState->mCalls[aId] && + gState->mCalls[aId]->mMiddlemanValue.isSome()); + return gState->mCalls[aId]->mMiddlemanValue.ref(); } -bool EX_SystemInput(ExternalCallContext& aCx, const void** aThingPtr) { - MOZ_RELEASE_ASSERT(aCx.AccessInput()); +bool MM_SystemInput(MiddlemanCallContext& aCx, const void** aThingPtr) { + MOZ_RELEASE_ASSERT(aCx.AccessPreface()); - bool isNull = *aThingPtr == nullptr; - aCx.ReadOrWriteInputBytes(&isNull, sizeof(isNull)); - if (isNull) { - *aThingPtr = nullptr; + if (!*aThingPtr) { + // Null values are handled by the normal argument copying logic. return true; } - ExternalCallId callId = 0; - if (aCx.mPhase == ExternalCallPhase::SaveInput) { - ExternalCall* call = LookupExternalCall(*aThingPtr); + Maybe<size_t> callId; + if (aCx.mPhase == MiddlemanCallPhase::ReplayPreface) { + // Determine any middleman call this object came from, before the pointer + // has a chance to be clobbered by another call between this and the + // ReplayInput phase. + MiddlemanCall* call = LookupMiddlemanCall(*aThingPtr); if (call) { - callId = call->mId; - MOZ_RELEASE_ASSERT(callId); - aCx.mCall->mDependentCalls.append(call->mId); + callId.emplace(call->mId); } } - aCx.ReadOrWriteInputBytes(&callId, sizeof(callId)); + aCx.ReadOrWritePrefaceBytes(&callId, sizeof(callId)); - if (aCx.mPhase == ExternalCallPhase::RestoreInput) { - if (callId) { - Maybe<const void*> value = GetExternalCallValue(callId); - MOZ_RELEASE_ASSERT(value.isSome()); - *aThingPtr = value.ref(); - } + switch (aCx.mPhase) { + case MiddlemanCallPhase::ReplayPreface: + return true; + case MiddlemanCallPhase::ReplayInput: + if (callId.isSome()) { + aCx.WriteInputScalar(callId.ref()); + aCx.mDependentCalls->append(gState->mCalls[callId.ref()]); + return true; + } + return false; + case MiddlemanCallPhase::MiddlemanInput: + if (callId.isSome()) { + size_t callIndex = aCx.ReadInputScalar(); + *aThingPtr = GetMiddlemanCallValue(callIndex); + return true; + } + return false; + default: + MOZ_CRASH("Bad phase"); } - - return callId != 0; } -static const void* MangledSystemValue(ExternalCallId aId) { - return (const void*)((size_t)aId | (1ULL << 63)); +// Pointer system values are preserved during the replay so that null tests +// and equality tests work as expected. We additionally mangle the +// pointers here by setting one of the two highest bits, depending on whether +// the pointer came from the recording or from the middleman. This avoids +// accidentally conflating pointers that happen to have the same value but +// which originate from different processes. +static const void* MangleSystemValue(const void* aValue, bool aFromRecording) { + return (const void*)((size_t)aValue | (1ULL << (aFromRecording ? 63 : 62))); } -void EX_SystemOutput(ExternalCallContext& aCx, const void** aOutput, +void MM_SystemOutput(MiddlemanCallContext& aCx, const void** aOutput, bool aUpdating) { - if (!aCx.AccessOutput()) { + if (!*aOutput) { + if (aCx.mPhase == MiddlemanCallPhase::MiddlemanOutput) { + aCx.mCall->SetMiddlemanValue(*aOutput); + } return; } - bool isNull = false; - Maybe<ExternalCallId> aliasedCall; - if (aCx.mPhase == ExternalCallPhase::SaveOutput) { - SetExternalCallValue(aCx.mCall, *aOutput); - - isNull = *aOutput == nullptr; - if (!isNull) { - ExternalCall* call = LookupExternalCall(*aOutput); - if (call) { - aliasedCall.emplace(call->mId); + switch (aCx.mPhase) { + case MiddlemanCallPhase::ReplayPreface: + if (!HasDivergedFromRecording()) { + // If we haven't diverged from the recording, use the output value saved + // in the recording. + if (!aUpdating) { + *aOutput = MangleSystemValue(*aOutput, true); + } + aCx.mCall->SetRecordingValue(*aOutput); + AddMiddlemanCallValue(*aOutput, aCx.mCall); } - } - } - aCx.ReadOrWriteOutputBytes(&isNull, sizeof(isNull)); - aCx.ReadOrWriteOutputBytes(&aliasedCall, sizeof(aliasedCall)); + break; + case MiddlemanCallPhase::MiddlemanOutput: + aCx.mCall->SetMiddlemanValue(*aOutput); + AddMiddlemanCallValue(*aOutput, aCx.mCall); + break; + case MiddlemanCallPhase::ReplayOutput: { + if (!aUpdating) { + *aOutput = MangleSystemValue(*aOutput, false); + } + aCx.mCall->SetMiddlemanValue(*aOutput); - if (aCx.mPhase == ExternalCallPhase::RestoreOutput) { - do { - if (isNull) { - *aOutput = nullptr; - break; + // Associate the value produced by the middleman with this call. If the + // call previously went through the ReplayPreface phase when we did not + // diverge from the recording, we will associate values from both the + // recording and middleman processes with this call. If a call made after + // diverging produced the same value as a call made before diverging, use + // the value saved in the recording for the first call, so that equality + // tests on the value work as expected. + MiddlemanCall* previousCall = LookupMiddlemanCall(*aOutput); + if (previousCall) { + if (previousCall->mRecordingValue.isSome()) { + *aOutput = previousCall->mRecordingValue.ref(); + } + } else { + AddMiddlemanCallValue(*aOutput, aCx.mCall); } - if (aliasedCall.isSome()) { - auto iter = gState->mCallsById.find(aliasedCall.ref()); - if (iter != gState->mCallsById.end()) { - *aOutput = iter->second; - break; - } - // If we haven't encountered the aliased call, fall through and generate - // a new value for it. Aliases might be spurious if they were derived from - // the recording and reflect a value that was released and had its memory - // reused. - } - *aOutput = MangledSystemValue(aCx.mCall->mId); - } while (false); - - SetExternalCallValue(aCx.mCall, *aOutput); + break; + } + default: + return; } } /////////////////////////////////////////////////////////////////////////////// -// ExternalCall +// MiddlemanCall /////////////////////////////////////////////////////////////////////////////// -void CallReturnRegisters::CopyFrom(CallArguments* aArguments) { - rval0 = aArguments->Rval<size_t, 0>(); - rval1 = aArguments->Rval<size_t, 1>(); - floatrval0 = aArguments->FloatRval<0>(); - floatrval1 = aArguments->FloatRval<1>(); -} - -void CallReturnRegisters::CopyTo(CallArguments* aArguments) { - aArguments->Rval<size_t, 0>() = rval0; - aArguments->Rval<size_t, 1>() = rval1; - aArguments->FloatRval<0>() = floatrval0; - aArguments->FloatRval<1>() = floatrval1; -} - -void ExternalCall::EncodeInput(BufferStream& aStream) const { +void MiddlemanCall::EncodeInput(BufferStream& aStream) const { aStream.WriteScalar(mId); aStream.WriteScalar(mCallId); - aStream.WriteScalar(mExcludeInput); + aStream.WriteBytes(&mArguments, sizeof(CallRegisterArguments)); + aStream.WriteScalar(mPreface.length()); + aStream.WriteBytes(mPreface.begin(), mPreface.length()); aStream.WriteScalar(mInput.length()); aStream.WriteBytes(mInput.begin(), mInput.length()); } -void ExternalCall::DecodeInput(BufferStream& aStream) { +void MiddlemanCall::DecodeInput(BufferStream& aStream) { mId = aStream.ReadScalar(); mCallId = aStream.ReadScalar(); - mExcludeInput = aStream.ReadScalar(); + aStream.ReadBytes(&mArguments, sizeof(CallRegisterArguments)); + size_t prefaceLength = aStream.ReadScalar(); + mPreface.appendN(0, prefaceLength); + aStream.ReadBytes(mPreface.begin(), prefaceLength); size_t inputLength = aStream.ReadScalar(); mInput.appendN(0, inputLength); aStream.ReadBytes(mInput.begin(), inputLength); } -void ExternalCall::EncodeOutput(BufferStream& aStream) const { - aStream.WriteBytes(&mReturnRegisters, sizeof(CallReturnRegisters)); +void MiddlemanCall::EncodeOutput(BufferStream& aStream) const { + aStream.WriteBytes(&mArguments, sizeof(CallRegisterArguments)); aStream.WriteScalar(mOutput.length()); aStream.WriteBytes(mOutput.begin(), mOutput.length()); } -void ExternalCall::DecodeOutput(BufferStream& aStream) { - aStream.ReadBytes(&mReturnRegisters, sizeof(CallReturnRegisters)); +void MiddlemanCall::DecodeOutput(BufferStream& aStream) { + // Only update the return value when decoding arguments, so that we don't + // clobber the call's arguments with any changes made in the middleman. + CallRegisterArguments newArguments; + aStream.ReadBytes(&newArguments, sizeof(CallRegisterArguments)); + mArguments.CopyRvalFrom(&newArguments); size_t outputLength = aStream.ReadScalar(); mOutput.appendN(0, outputLength); aStream.ReadBytes(mOutput.begin(), outputLength); } } // namespace recordreplay } // namespace mozilla
rename from toolkit/recordreplay/ExternalCall.h rename to toolkit/recordreplay/MiddlemanCall.h --- a/toolkit/recordreplay/ExternalCall.h +++ b/toolkit/recordreplay/MiddlemanCall.h @@ -1,455 +1,458 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#ifndef mozilla_recordreplay_ExternalCall_h -#define mozilla_recordreplay_ExternalCall_h +#ifndef mozilla_recordreplay_MiddlemanCall_h +#define mozilla_recordreplay_MiddlemanCall_h #include "BufferStream.h" #include "ProcessRedirect.h" #include "mozilla/Maybe.h" namespace mozilla { namespace recordreplay { -// External Calls Overview +// Middleman Calls Overview // // With few exceptions, replaying processes do not interact with the underlying // system or call the actual versions of redirected system library functions. // This is problematic after diverging from the recording, as then the diverged // thread cannot interact with its recording either. // -// External calls are used in a replaying process after diverging from the -// recording to perform calls in another process instead. We call this the -// external process; currently it is always the middleman, though this will -// change soon. +// Middleman calls are used in a replaying process after diverging from the +// recording to perform calls in the middleman process instead. Inputs are +// gathered and serialized in the replaying process, then sent to the middleman +// process. The middleman calls the function, and its outputs are serialized +// for reading by the replaying process. +// +// Calls that might need to be sent to the middleman are processed in phases, +// per the MiddlemanCallPhase enum below. The timeline of a middleman call is +// as follows: +// +// - Any redirection with a middleman call hook can potentially be sent to the +// middleman. In a replaying process, whenever such a call is encountered, +// the hook is invoked in the ReplayPreface phase to capture any input data +// that must be examined at the time of the call itself. +// +// - If the thread has not diverged from the recording, the call is remembered +// but no further action is necessary yet. // -// External call state is managed so that call results can be reused when -// possible, to minimize traffic between processes and improve efficiency. -// Conceptually, the result of performing a system call is entirely determined -// by its inputs: scalar values and the results of other system calls (and the -// version of the underlying system, which we ignore for now). This information -// uniquely identifies the call, and we can form a directed graph out of these -// calls by connecting them to the other calls they depend on. +// - If the thread has diverged from the recording, the call needs to go +// through the remaining phases. The ReplayInput phase captures any +// additional inputs to the call, potentially including values produced by +// other middleman calls. +// +// - The transitive closure of these call dependencies is produced, and all +// calls found go through the ReplayInput phase. The resulting data is sent +// to the middleman process, which goes through the MiddlemanInput phase +// to decode those inputs. // -// Each root replaying process maintains a portion of this graph. As the -// recording is replayed by other forked processes, nodes are added to the graph -// by studying the output of calls that appear in the recording itself. When -// a replaying process wants to make a system call that does not appear in the -// graph, the inputs for that call and the transitive closure of the calls it -// depends on is sent to another process running on the target platform. That -// process executes the call, saves the output and sends it back for adding to -// the graph. Other ways of updating the graph could be added in the future. +// - The middleman performs each of the calls it has been given, and their +// outputs are encoded in the MiddlemanOutput phase. These outputs are sent +// to the replaying process in a response and decoded in the ReplayOutput +// phase, which can then resume execution. +// +// - The replaying process holds onto information about calls it has sent until +// it rewinds to a point before it diverged from the recording. This rewind +// will --- without any special action required --- wipe out information on +// all calls sent to the middleman, and retain any data gathered in the +// ReplayPreface phase for calls that were made prior to the rewind target. +// +// - Information about calls and all resources held are retained in the +// middleman process are retained until a replaying process asks for them to +// be reset, which happens any time the replaying process first diverges from +// the recording. The MiddlemanRelease phase is used to release any system +// resources held. -// Inputs and outputs to external calls are handled in a reusable fashion by -// adding a ExternalCall hook to the system call's redirection. This hook is -// called in one of the following phases. -enum class ExternalCallPhase { - // When replaying, a call which can be put in the external call graph is being - // made. This can happen either before or after diverging from the recording, - // and saves all inputs to the call which are sufficient to uniquely identify - // it in the graph. - SaveInput, +// Ways of processing calls that can be sent to the middleman. +enum class MiddlemanCallPhase { + // When replaying, a call is being performed that might need to be sent to + // the middleman later. + ReplayPreface, + + // A call for which inputs have been gathered is now being sent to the + // middleman. This is separate from ReplayPreface because capturing inputs + // might need to dereference pointers that could be bogus values originating + // from the recording. Waiting to dereference these pointers until we know + // the call needs to be sent to the middleman avoids needing to understand + // the inputs to all call sites of general purpose redirections such as + // CFArrayCreate. + ReplayInput, - // In the external process, the inputs saved earlier are being restored in - // preparation for executing the call. - RestoreInput, + // In the middleman process, a call from the replaying process is being + // performed. + MiddlemanInput, + + // In the middleman process, a call from the replaying process was just + // performed, and its outputs need to be saved. + MiddlemanOutput, - // Save any outputs produced by a call. This can happen either in the - // replaying process (using outputs saved in the recording) or in the external - // process (using outputs just produced). After saving the outputs to the - // call, it can be placed in the external call graph and be used to resolve - // external calls which a diverged replaying process is making. Returned - // register values are automatically saved. - SaveOutput, + // Back in the replaying process, the outputs from a call have been received + // from the middleman. + ReplayOutput, - // In the replaying process, restore any outputs associated with an external - // call. This is only called after diverging from the recording, and allows - // diverged execution to continue afterwards. - RestoreOutput, + // In the middleman process, release any system resources held after this + // call. + MiddlemanRelease, }; -// Global identifier for an external call. The result of an external call is -// determined by its inputs, and its ID is a hash of those inputs. If there are -// hash collisions between different inputs then two different calls will have -// the same ID, and things will break (subtly or not). Valid IDs are non-zero. -typedef uintptr_t ExternalCallId; - -// Storage for the returned registers of a call which are automatically saved. -struct CallReturnRegisters { - size_t rval0, rval1; - double floatrval0, floatrval1; - - void CopyFrom(CallArguments* aArguments); - void CopyTo(CallArguments* aArguments); -}; - -struct ExternalCall { - // ID for this call. - ExternalCallId mId = 0; +struct MiddlemanCall { + // Unique ID for this call. + size_t mId; // ID of the redirection being invoked. - size_t mCallId = 0; + size_t mCallId; - // All call inputs. Written in SaveInput, read in RestoreInput. + // All register arguments and return values are preserved when sending the + // call back and forth between processes. + CallRegisterArguments mArguments; + + // Written in ReplayPrefaceInput, read in ReplayInput and MiddlemanInput. + InfallibleVector<char> mPreface; + + // Written in ReplayInput, read in MiddlemanInput. InfallibleVector<char> mInput; - // If non-zero, only the input before this extent is used to characterize this - // call and determine if it is the same as another external call. - size_t mExcludeInput = 0; - - // Calls which this depends on, written in SaveInput and read in RestoreInput. - // Any calls referenced in mInput will also be here. - InfallibleVector<ExternalCallId> mDependentCalls; - - // All call outputs. Written in SaveOutput, read in RestoreOutput. + // Written in MiddlemanOutput, read in ReplayOutput. InfallibleVector<char> mOutput; - // Values of any returned registers after the call. - CallReturnRegisters mReturnRegisters; + // In a replaying process, whether this call has been sent to the middleman. + bool mSent; - // Any system value produced by this call. In the external process this is - // the actual system value, while in the replaying process this is the value - // used during execution to represent the call's result. - Maybe<const void*> mValue; + // In a replaying process, any value associated with this call that was + // included in the recording, when the call was made before diverging from + // the recording. + Maybe<const void*> mRecordingValue; + + // In a replaying or middleman process, any value associated with this call + // that was produced by the middleman itself. + Maybe<const void*> mMiddlemanValue; + + MiddlemanCall() : mId(0), mCallId(0), mSent(false) {} void EncodeInput(BufferStream& aStream) const; void DecodeInput(BufferStream& aStream); void EncodeOutput(BufferStream& aStream) const; void DecodeOutput(BufferStream& aStream); - void ComputeId() { - MOZ_RELEASE_ASSERT(!mId); - size_t extent = mExcludeInput ? mExcludeInput : mInput.length(); - mId = HashGeneric(mCallId, HashBytes(mInput.begin(), extent)); - if (!mId) { - mId = 1; - } + void SetRecordingValue(const void* aValue) { + MOZ_RELEASE_ASSERT(mRecordingValue.isNothing()); + mRecordingValue.emplace(aValue); + } + + void SetMiddlemanValue(const void* aValue) { + MOZ_RELEASE_ASSERT(mMiddlemanValue.isNothing()); + mMiddlemanValue.emplace(aValue); } }; -// Information needed to process one of the phases of an external call. -struct ExternalCallContext { +// Information needed to process one of the phases of a middleman call, +// in either the replaying or middleman process. +struct MiddlemanCallContext { // Call being operated on. - ExternalCall* mCall; + MiddlemanCall* mCall; // Complete arguments and return value information for the call. CallArguments* mArguments; // Current processing phase. - ExternalCallPhase mPhase; + MiddlemanCallPhase mPhase; + + // During the ReplayPreface or ReplayInput phases, whether capturing input + // data has failed. In such cases the call cannot be sent to the middleman + // and, if the thread has diverged from the recording, an unhandled + // divergence and associated rewind will occur. + bool mFailed; - // During the SaveInput phase, whether capturing input data has failed. - // In such cases the call cannot be placed in the external call graph and, - // if the thread has diverged from the recording, an unhandled divergence - // will occur. - bool mFailed = false; + // This can be set in the MiddlemanInput phase to avoid performing the call + // in the middleman process. + bool mSkipCallInMiddleman; - // This can be set in the RestoreInput phase to avoid executing the call - // in the external process. - bool mSkipExecuting = false; + // During the ReplayInput phase, this can be used to fill in any middleman + // calls whose output the current one depends on. + InfallibleVector<MiddlemanCall*>* mDependentCalls; // Streams of data that can be accessed during the various phases. Streams // need to be read or written from at the same points in the phases which use // them, so that callbacks operating on these streams can be composed without // issues. - // Inputs are written during SaveInput, and read during RestoreInput. + // The preface is written during ReplayPreface, and read during both + // ReplayInput and MiddlemanInput. + Maybe<BufferStream> mPrefaceStream; + + // Inputs are written during ReplayInput, and read during MiddlemanInput. Maybe<BufferStream> mInputStream; - // Outputs are written during SaveOutput, and read during RestoreOutput. + // Outputs are written during MiddlemanOutput, and read during ReplayOutput. Maybe<BufferStream> mOutputStream; - // If we are running the SaveOutput phase in an external process, a list of - // callbacks which will release all system resources created by by the call. - typedef InfallibleVector<std::function<void()>> ReleaseCallbackVector; - ReleaseCallbackVector* mReleaseCallbacks = nullptr; + // During the ReplayOutput phase, this is set if the call was made sometime + // in the past and pointers referred to in the arguments may no longer be + // valid. + bool mReplayOutputIsOld; - ExternalCallContext(ExternalCall* aCall, CallArguments* aArguments, - ExternalCallPhase aPhase) + MiddlemanCallContext(MiddlemanCall* aCall, CallArguments* aArguments, + MiddlemanCallPhase aPhase) : mCall(aCall), mArguments(aArguments), - mPhase(aPhase) { + mPhase(aPhase), + mFailed(false), + mSkipCallInMiddleman(false), + mDependentCalls(nullptr), + mReplayOutputIsOld(false) { switch (mPhase) { - case ExternalCallPhase::SaveInput: + case MiddlemanCallPhase::ReplayPreface: + mPrefaceStream.emplace(&mCall->mPreface); + break; + case MiddlemanCallPhase::ReplayInput: + mPrefaceStream.emplace(mCall->mPreface.begin(), + mCall->mPreface.length()); mInputStream.emplace(&mCall->mInput); break; - case ExternalCallPhase::RestoreInput: + case MiddlemanCallPhase::MiddlemanInput: + mPrefaceStream.emplace(mCall->mPreface.begin(), + mCall->mPreface.length()); mInputStream.emplace(mCall->mInput.begin(), mCall->mInput.length()); break; - case ExternalCallPhase::SaveOutput: - mCall->mReturnRegisters.CopyFrom(aArguments); + case MiddlemanCallPhase::MiddlemanOutput: mOutputStream.emplace(&mCall->mOutput); break; - case ExternalCallPhase::RestoreOutput: - mCall->mReturnRegisters.CopyTo(aArguments); + case MiddlemanCallPhase::ReplayOutput: mOutputStream.emplace(mCall->mOutput.begin(), mCall->mOutput.length()); break; + case MiddlemanCallPhase::MiddlemanRelease: + break; } } void MarkAsFailed() { - MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::SaveInput); + MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::ReplayPreface || + mPhase == MiddlemanCallPhase::ReplayInput); mFailed = true; } void WriteInputBytes(const void* aBuffer, size_t aSize) { - MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::SaveInput); + MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::ReplayInput); mInputStream.ref().WriteBytes(aBuffer, aSize); } void WriteInputScalar(size_t aValue) { - MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::SaveInput); + MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::ReplayInput); mInputStream.ref().WriteScalar(aValue); } void ReadInputBytes(void* aBuffer, size_t aSize) { - MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::RestoreInput); + MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::MiddlemanInput); mInputStream.ref().ReadBytes(aBuffer, aSize); } size_t ReadInputScalar() { - MOZ_RELEASE_ASSERT(mPhase == ExternalCallPhase::RestoreInput); + MOZ_RELEASE_ASSERT(mPhase == MiddlemanCallPhase::MiddlemanInput); return mInputStream.ref().ReadScalar(); } bool AccessInput() { return mInputStream.isSome(); } - void ReadOrWriteInputBytes(void* aBuffer, size_t aSize, bool aExcludeInput = false) { + void ReadOrWriteInputBytes(void* aBuffer, size_t aSize) { switch (mPhase) { - case ExternalCallPhase::SaveInput: - // Only one buffer can be excluded, and it has to be the last input to - // the external call. - MOZ_RELEASE_ASSERT(!mCall->mExcludeInput); - if (aExcludeInput) { - mCall->mExcludeInput = mCall->mInput.length(); - } + case MiddlemanCallPhase::ReplayInput: WriteInputBytes(aBuffer, aSize); break; - case ExternalCallPhase::RestoreInput: + case MiddlemanCallPhase::MiddlemanInput: ReadInputBytes(aBuffer, aSize); break; default: MOZ_CRASH(); } } - void ReadOrWriteInputBuffer(void** aBufferPtr, size_t aSize, - bool aIncludeContents = true) { + bool AccessPreface() { return mPrefaceStream.isSome(); } + + void ReadOrWritePrefaceBytes(void* aBuffer, size_t aSize) { switch (mPhase) { - case ExternalCallPhase::SaveInput: - if (aIncludeContents) { - WriteInputBytes(*aBufferPtr, aSize); - } + case MiddlemanCallPhase::ReplayPreface: + mPrefaceStream.ref().WriteBytes(aBuffer, aSize); + break; + case MiddlemanCallPhase::ReplayInput: + case MiddlemanCallPhase::MiddlemanInput: + mPrefaceStream.ref().ReadBytes(aBuffer, aSize); break; - case ExternalCallPhase::RestoreInput: + default: + MOZ_CRASH(); + } + } + + void ReadOrWritePrefaceBuffer(void** aBufferPtr, size_t aSize) { + switch (mPhase) { + case MiddlemanCallPhase::ReplayPreface: + mPrefaceStream.ref().WriteBytes(*aBufferPtr, aSize); + break; + case MiddlemanCallPhase::ReplayInput: + case MiddlemanCallPhase::MiddlemanInput: *aBufferPtr = AllocateBytes(aSize); - if (aIncludeContents) { - ReadInputBytes(*aBufferPtr, aSize); - } + mPrefaceStream.ref().ReadBytes(*aBufferPtr, aSize); break; default: MOZ_CRASH(); } } bool AccessOutput() { return mOutputStream.isSome(); } void ReadOrWriteOutputBytes(void* aBuffer, size_t aSize) { switch (mPhase) { - case ExternalCallPhase::SaveOutput: + case MiddlemanCallPhase::MiddlemanOutput: mOutputStream.ref().WriteBytes(aBuffer, aSize); break; - case ExternalCallPhase::RestoreOutput: + case MiddlemanCallPhase::ReplayOutput: mOutputStream.ref().ReadBytes(aBuffer, aSize); break; default: MOZ_CRASH(); } } void ReadOrWriteOutputBuffer(void** aBuffer, size_t aSize) { - if (AccessInput()) { - bool isNull = *aBuffer == nullptr; - ReadOrWriteInputBytes(&isNull, sizeof(isNull)); - if (isNull) { - *aBuffer = nullptr; - } else if (mPhase == ExternalCallPhase::RestoreInput) { + if (*aBuffer) { + if (mPhase == MiddlemanCallPhase::MiddlemanInput || mReplayOutputIsOld) { *aBuffer = AllocateBytes(aSize); } - } - if (AccessOutput() && *aBuffer) { - ReadOrWriteOutputBytes(*aBuffer, aSize); + if (AccessOutput()) { + ReadOrWriteOutputBytes(*aBuffer, aSize); + } } } // Allocate some memory associated with the call, which will be released in - // the external process after fully processing a call, and will never be - // released in the replaying process. + // the replaying process on a rewind and in the middleman process when the + // call state is reset. void* AllocateBytes(size_t aSize); }; -// Notify the system about a call to a redirection with an external call hook. +// Notify the system about a call to a redirection with a middleman call hook. // aDiverged is set if the current thread has diverged from the recording and // any outputs for the call must be filled in; otherwise, they already have // been filled in using data from the recording. Returns false if the call was // unable to be processed. -bool OnExternalCall(size_t aCallId, CallArguments* aArguments, - bool aDiverged); - -// In the external process, perform one or more calls encoded in aInputData -// and encode the output of the final call in aOutputData. -void ProcessExternalCall(const char* aInputData, size_t aInputSize, - InfallibleVector<char>* aOutputData); +bool SendCallToMiddleman(size_t aCallId, CallArguments* aArguments, + bool aDiverged); -// In a replaying process, flush all new external call found in the recording -// since the last flush to the root replaying process. -void FlushExternalCalls(); +// In the middleman process, perform one or more calls encoded in aInputData +// and encode their outputs to aOutputData. The calls are associated with the +// specified child process ID. +void ProcessMiddlemanCall(size_t aChildId, const char* aInputData, + size_t aInputSize, + InfallibleVector<char>* aOutputData); -// In a root replaying process, remember the output from an external call. -void AddExternalCallOutput(ExternalCallId aId, const char* aOutput, - size_t aOutputSize); - -// In a root replaying process, fetch the output from an external call if known. -bool HasExternalCallOutput(ExternalCallId aId, InfallibleVector<char>* aOutput); +// In the middleman process, reset all call state for a child process ID. +void ResetMiddlemanCalls(size_t aChildId); /////////////////////////////////////////////////////////////////////////////// -// External Call Helpers +// Middleman Call Helpers /////////////////////////////////////////////////////////////////////////////// -// Capture a scalar argument. -template <size_t Arg> -static inline void EX_ScalarArg(ExternalCallContext& aCx) { - if (aCx.AccessInput()) { - auto& arg = aCx.mArguments->Arg<Arg, size_t>(); - aCx.ReadOrWriteInputBytes(&arg, sizeof(arg)); +// Capture the contents of an input buffer at BufferArg with element count at +// CountArg. +template <size_t BufferArg, size_t CountArg, typename ElemType = char> +static inline void MM_Buffer(MiddlemanCallContext& aCx) { + if (aCx.AccessPreface()) { + auto& buffer = aCx.mArguments->Arg<BufferArg, void*>(); + auto byteSize = aCx.mArguments->Arg<CountArg, size_t>() * sizeof(ElemType); + aCx.ReadOrWritePrefaceBuffer(&buffer, byteSize); } } -// Capture a floating point argument. -template <size_t Arg> -static inline void EX_FloatArg(ExternalCallContext& aCx) { - if (aCx.AccessInput()) { - auto& arg = aCx.mArguments->FloatArg<Arg>(); - aCx.ReadOrWriteInputBytes(&arg, sizeof(arg)); - } -} - -// Capture an input buffer at BufferArg with element count at CountArg. -// If IncludeContents is not set, the buffer's contents are not captured, -// but the buffer's pointer will be allocated with the correct size when -// restoring input. -template <size_t BufferArg, size_t CountArg, typename ElemType = char, - bool IncludeContents = true> -static inline void EX_Buffer(ExternalCallContext& aCx) { - EX_ScalarArg<CountArg>(aCx); - - if (aCx.AccessInput()) { +// Capture the contents of a fixed size input buffer. +template <size_t BufferArg, size_t ByteSize> +static inline void MM_BufferFixedSize(MiddlemanCallContext& aCx) { + if (aCx.AccessPreface()) { auto& buffer = aCx.mArguments->Arg<BufferArg, void*>(); - auto byteSize = aCx.mArguments->Arg<CountArg, size_t>() * sizeof(ElemType); - aCx.ReadOrWriteInputBuffer(&buffer, byteSize, IncludeContents); - } -} - -// Capture the contents of an optional input parameter. -template <size_t BufferArg, typename Type> -static inline void EX_InParam(ExternalCallContext& aCx) { - if (aCx.AccessInput()) { - auto& param = aCx.mArguments->Arg<BufferArg, void*>(); - bool hasParam = !!param; - aCx.ReadOrWriteInputBytes(&hasParam, sizeof(hasParam)); - if (hasParam) { - aCx.ReadOrWriteInputBuffer(¶m, sizeof(Type)); - } else { - param = nullptr; + if (buffer) { + aCx.ReadOrWritePrefaceBuffer(&buffer, ByteSize); } } } // Capture a C string argument. template <size_t StringArg> -static inline void EX_CString(ExternalCallContext& aCx) { - if (aCx.AccessInput()) { +static inline void MM_CString(MiddlemanCallContext& aCx) { + if (aCx.AccessPreface()) { auto& buffer = aCx.mArguments->Arg<StringArg, char*>(); - size_t len = (aCx.mPhase == ExternalCallPhase::SaveInput) + size_t len = (aCx.mPhase == MiddlemanCallPhase::ReplayPreface) ? strlen(buffer) + 1 : 0; - aCx.ReadOrWriteInputBytes(&len, sizeof(len)); - aCx.ReadOrWriteInputBuffer((void**)&buffer, len); + aCx.ReadOrWritePrefaceBytes(&len, sizeof(len)); + aCx.ReadOrWritePrefaceBuffer((void**)&buffer, len); } } // Capture the data written to an output buffer at BufferArg with element count // at CountArg. template <size_t BufferArg, size_t CountArg, typename ElemType> -static inline void EX_WriteBuffer(ExternalCallContext& aCx) { - EX_ScalarArg<CountArg>(aCx); - +static inline void MM_WriteBuffer(MiddlemanCallContext& aCx) { auto& buffer = aCx.mArguments->Arg<BufferArg, void*>(); auto count = aCx.mArguments->Arg<CountArg, size_t>(); aCx.ReadOrWriteOutputBuffer(&buffer, count * sizeof(ElemType)); } -// Capture the data written to an out parameter. -template <size_t BufferArg, typename Type> -static inline void EX_OutParam(ExternalCallContext& aCx) { +// Capture the data written to a fixed size output buffer. +template <size_t BufferArg, size_t ByteSize> +static inline void MM_WriteBufferFixedSize(MiddlemanCallContext& aCx) { auto& buffer = aCx.mArguments->Arg<BufferArg, void*>(); - aCx.ReadOrWriteOutputBuffer(&buffer, sizeof(Type)); + aCx.ReadOrWriteOutputBuffer(&buffer, ByteSize); } // Capture return values that are too large for register storage. -template <typename Type> -static inline void EX_OversizeRval(ExternalCallContext& aCx) { - EX_OutParam<0, Type>(aCx); +template <size_t ByteSize> +static inline void MM_OversizeRval(MiddlemanCallContext& aCx) { + MM_WriteBufferFixedSize<0, ByteSize>(aCx); } // Capture a byte count of stack argument data. template <size_t ByteSize> -static inline void EX_StackArgumentData(ExternalCallContext& aCx) { - if (aCx.AccessInput()) { +static inline void MM_StackArgumentData(MiddlemanCallContext& aCx) { + if (aCx.AccessPreface()) { auto stack = aCx.mArguments->StackAddress<0>(); - aCx.ReadOrWriteInputBytes(stack, ByteSize); + aCx.ReadOrWritePrefaceBytes(stack, ByteSize); } } -// Avoid calling a function in the external process. -static inline void EX_SkipExecuting(ExternalCallContext& aCx) { - if (aCx.mPhase == ExternalCallPhase::RestoreInput) { - aCx.mSkipExecuting = true; +// Avoid calling a function in the middleman process. +static inline void MM_SkipInMiddleman(MiddlemanCallContext& aCx) { + if (aCx.mPhase == MiddlemanCallPhase::MiddlemanInput) { + aCx.mSkipCallInMiddleman = true; } } -static inline void EX_NoOp(ExternalCallContext& aCx) {} +static inline void MM_NoOp(MiddlemanCallContext& aCx) {} -template <ExternalCallFn Fn0, ExternalCallFn Fn1, - ExternalCallFn Fn2 = EX_NoOp, ExternalCallFn Fn3 = EX_NoOp, - ExternalCallFn Fn4 = EX_NoOp, ExternalCallFn Fn5 = EX_NoOp> -static inline void EX_Compose(ExternalCallContext& aCx) { +template <MiddlemanCallFn Fn0, MiddlemanCallFn Fn1, + MiddlemanCallFn Fn2 = MM_NoOp, MiddlemanCallFn Fn3 = MM_NoOp, + MiddlemanCallFn Fn4 = MM_NoOp> +static inline void MM_Compose(MiddlemanCallContext& aCx) { Fn0(aCx); Fn1(aCx); Fn2(aCx); Fn3(aCx); Fn4(aCx); - Fn5(aCx); } -// Helper for capturing inputs that are produced by other external calls. -// Returns false in the SaveInput phase if the input system value could not -// be found. -bool EX_SystemInput(ExternalCallContext& aCx, const void** aThingPtr); +// Helper for capturing inputs that are produced by other middleman calls. +// Returns false in the ReplayInput or MiddlemanInput phases if the input +// system value could not be found. +bool MM_SystemInput(MiddlemanCallContext& aCx, const void** aThingPtr); // Helper for capturing output system values that might be consumed by other -// external calls. -void EX_SystemOutput(ExternalCallContext& aCx, const void** aOutput, +// middleman calls. +void MM_SystemOutput(MiddlemanCallContext& aCx, const void** aOutput, bool aUpdating = false); -void InitializeExternalCalls(); - } // namespace recordreplay } // namespace mozilla -#endif // mozilla_recordreplay_ExternalCall_h +#endif // mozilla_recordreplay_MiddlemanCall_h
--- a/toolkit/recordreplay/ProcessRecordReplay.cpp +++ b/toolkit/recordreplay/ProcessRecordReplay.cpp @@ -7,46 +7,43 @@ #include "ProcessRecordReplay.h" #include "ipc/ChildInternal.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/Compression.h" #include "mozilla/Maybe.h" #include "mozilla/Sprintf.h" #include "mozilla/StaticMutex.h" +#include "DirtyMemoryHandler.h" #include "Lock.h" +#include "MemorySnapshot.h" #include "ProcessRedirect.h" #include "ProcessRewind.h" #include "ValueIndex.h" #include "pratom.h" -#include <dlfcn.h> #include <fcntl.h> #include <unistd.h> -#include <mach/exc.h> -#include <mach/mach.h> -#include <mach/mach_vm.h> -#include <mach/ndr.h> -#include <sys/time.h> - namespace mozilla { namespace recordreplay { MOZ_NEVER_INLINE void BusyWait() { static volatile int value = 1; while (value) { } } /////////////////////////////////////////////////////////////////////////////// // Basic interface /////////////////////////////////////////////////////////////////////////////// -Recording* gRecording; +File* gRecordingFile; +const char* gSnapshotMemoryPrefix; +const char* gSnapshotStackPrefix; char* gInitializationFailureMessage; bool gInitialized; ProcessKind gProcessKind; char* gRecordingFilename; // Current process ID. @@ -56,43 +53,36 @@ static int gPid; static int gRecordingPid; // Whether to spew record/replay messages to stderr. static bool gSpewEnabled; // Whether this is the main child. static bool gMainChild; -// Whether we are replaying on a cloud machine. -static bool gReplayingInCloud; - -static void InitializeCrashDetector(); - extern "C" { MOZ_EXPORT void RecordReplayInterface_Initialize(int aArgc, char* aArgv[]) { // Parse command line options for the process kind and recording file. Maybe<ProcessKind> processKind; Maybe<char*> recordingFile; for (int i = 0; i < aArgc; i++) { if (!strcmp(aArgv[i], gProcessKindOption)) { MOZ_RELEASE_ASSERT(processKind.isNothing() && i + 1 < aArgc); processKind.emplace((ProcessKind)atoi(aArgv[i + 1])); } if (!strcmp(aArgv[i], gRecordingFileOption)) { MOZ_RELEASE_ASSERT(recordingFile.isNothing() && i + 1 < aArgc); recordingFile.emplace(aArgv[i + 1]); } } - MOZ_RELEASE_ASSERT(processKind.isSome()); + MOZ_RELEASE_ASSERT(processKind.isSome() && recordingFile.isSome()); gProcessKind = processKind.ref(); - if (recordingFile.isSome()) { - gRecordingFilename = strdup(recordingFile.ref()); - } + gRecordingFilename = strdup(recordingFile.ref()); switch (processKind.ref()) { case ProcessKind::Recording: gIsRecording = gIsRecordingOrReplaying = true; fprintf(stderr, "RECORDING %d %s\n", getpid(), recordingFile.ref()); break; case ProcessKind::Replaying: gIsReplaying = gIsRecordingOrReplaying = true; @@ -122,58 +112,57 @@ MOZ_EXPORT void RecordReplayInterface_In gPid = getpid(); if (TestEnv("MOZ_RECORD_REPLAY_SPEW")) { gSpewEnabled = true; } EarlyInitializeRedirections(); if (!IsRecordingOrReplaying()) { - InitializeExternalCalls(); + InitializeMiddlemanCalls(); return; } + gSnapshotMemoryPrefix = mktemp(strdup("/tmp/SnapshotMemoryXXXXXX")); + gSnapshotStackPrefix = mktemp(strdup("/tmp/SnapshotStackXXXXXX")); + InitializeCurrentTime(); - gRecording = new Recording(); - - InitializeRedirections(); + gRecordingFile = new File(); + if (gRecordingFile->Open(recordingFile.ref(), + IsRecording() ? File::WRITE : File::READ)) { + InitializeRedirections(); + } else { + gInitializationFailureMessage = strdup("Bad recording file"); + } if (gInitializationFailureMessage) { fprintf(stderr, "Initialization Failure: %s\n", gInitializationFailureMessage); } LateInitializeRedirections(); Thread::InitializeThreads(); Thread* thread = Thread::GetById(MainThreadId); MOZ_ASSERT(thread->Id() == MainThreadId); + thread->BindToCurrent(); thread->SetPassThrough(true); - // The translation layer we are running under in the cloud will intercept this - // and return a non-zero symbol address. - gReplayingInCloud = !!dlsym(RTLD_DEFAULT, "RecordReplay_ReplayingInCloud"); - + InitializeMemorySnapshots(); Thread::SpawnAllThreads(); - InitializeExternalCalls(); - if (!gReplayingInCloud) { - // The crash detector is only useful when we have a local parent process to - // report crashes to. Avoid initializing it when running in the cloud - // so that we avoid calling mach interfaces with events passed through. - InitializeCrashDetector(); - } + InitializeCountdownThread(); + SetupDirtyMemoryHandler(); + InitializeMiddlemanCalls(); Lock::InitializeLocks(); // Don't create a stylo thread pool when recording or replaying. putenv((char*)"STYLO_THREADS=1"); - child::SetupRecordReplayChannel(aArgc, aArgv); - thread->SetPassThrough(false); InitializeRewindState(); gRecordingPid = RecordReplayValue(gPid); gMainChild = IsRecording(); gInitialized = true; @@ -203,100 +192,88 @@ MOZ_EXPORT void RecordReplayInterface_In thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Bytes); thread->Events().CheckInput(aSize); thread->Events().RecordOrReplayBytes(aData, aSize); } MOZ_EXPORT void RecordReplayInterface_InternalInvalidateRecording( const char* aWhy) { if (IsRecording()) { - child::ReportFatalError("Recording invalidated: %s", aWhy); + child::ReportFatalError(Nothing(), "Recording invalidated: %s", aWhy); } else { - child::ReportFatalError("Recording invalidated while replaying: %s", aWhy); + child::ReportFatalError(Nothing(), + "Recording invalidated while replaying: %s", aWhy); } Unreachable(); } -MOZ_EXPORT void RecordReplayInterface_InternalBeginPassThroughThreadEventsWithLocalReplay() { - if (IsReplaying() && !gReplayingInCloud) { - BeginPassThroughThreadEvents(); - } -} - -MOZ_EXPORT void RecordReplayInterface_InternalEndPassThroughThreadEventsWithLocalReplay() { - // If we are replaying locally we will be skipping over a section of the - // recording while events are passed through. Include the current stream - // position in the recording so that we will know how much to skip over. - MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); - Stream* localReplayStream = gRecording->OpenStream(StreamName::LocalReplaySkip, 0); - Stream& events = Thread::Current()->Events(); - - size_t position = IsRecording() ? events.StreamPosition() : 0; - localReplayStream->RecordOrReplayScalar(&position); - - if (IsReplaying() && !ReplayingInCloud()) { - EndPassThroughThreadEvents(); - MOZ_RELEASE_ASSERT(events.StreamPosition() <= position); - size_t nbytes = position - events.StreamPosition(); - void* buf = malloc(nbytes); - events.ReadBytes(buf, nbytes); - free(buf); - MOZ_RELEASE_ASSERT(events.StreamPosition() == position); - } -} - } // extern "C" -// How many bytes have been sent from the recording to the middleman. -size_t gRecordingDataSentToMiddleman; - void FlushRecording() { MOZ_RELEASE_ASSERT(IsRecording()); MOZ_RELEASE_ASSERT(Thread::CurrentIsMainThread()); // The recording can only be flushed when we are at a checkpoint. // Save this endpoint to the recording. size_t endpoint = GetLastCheckpoint(); - Stream* endpointStream = gRecording->OpenStream(StreamName::Endpoint, 0); + Stream* endpointStream = gRecordingFile->OpenStream(StreamName::Main, 0); endpointStream->WriteScalar(endpoint); - gRecording->PreventStreamWrites(); - gRecording->Flush(); - gRecording->AllowStreamWrites(); + gRecordingFile->PreventStreamWrites(); + gRecordingFile->Flush(); + gRecordingFile->AllowStreamWrites(); +} + +// Try to load another recording index, returning whether one was found. +static bool LoadNextRecordingIndex() { + Thread::WaitForIdleThreads(); - if (gRecording->Size() > gRecordingDataSentToMiddleman) { - child::SendRecordingData(gRecordingDataSentToMiddleman, - gRecording->Data() + gRecordingDataSentToMiddleman, - gRecording->Size() - gRecordingDataSentToMiddleman); - gRecordingDataSentToMiddleman = gRecording->Size(); + InfallibleVector<Stream*> updatedStreams; + File::ReadIndexResult result = gRecordingFile->ReadNextIndex(&updatedStreams); + if (result == File::ReadIndexResult::InvalidFile) { + MOZ_CRASH("Bad recording file"); } + + bool found = result == File::ReadIndexResult::FoundIndex; + if (found) { + for (Stream* stream : updatedStreams) { + if (stream->Name() == StreamName::Lock) { + Lock::LockAquiresUpdated(stream->NameIndex()); + } + } + } + + Thread::ResumeIdleThreads(); + return found; } void HitEndOfRecording() { MOZ_RELEASE_ASSERT(IsReplaying()); MOZ_RELEASE_ASSERT(!AreThreadEventsPassedThrough()); if (Thread::CurrentIsMainThread()) { - // We should have been provided with all the data needed to run forward in - // the replay. Check to see if there is any pending data. - child::AddPendingRecordingData(); + // Load more data from the recording. The debugger is not allowed to let us + // go past the recording endpoint, so there should be more data. + bool found = LoadNextRecordingIndex(); + MOZ_RELEASE_ASSERT(found); } else { - // Non-main threads may wait until more recording data is added. + // Non-main threads may wait until more recording data is loaded by the + // main thread. Thread::Wait(); } } // When replaying, the last endpoint loaded from the recording. static size_t gRecordingEndpoint; size_t RecordingEndpoint() { MOZ_RELEASE_ASSERT(IsReplaying()); MOZ_RELEASE_ASSERT(!AreThreadEventsPassedThrough()); - Stream* endpointStream = gRecording->OpenStream(StreamName::Endpoint, 0); + Stream* endpointStream = gRecordingFile->OpenStream(StreamName::Main, 0); while (!endpointStream->AtEnd()) { gRecordingEndpoint = endpointStream->ReadScalar(); } return gRecordingEndpoint; } bool SpewEnabled() { return gSpewEnabled; } @@ -319,21 +296,18 @@ const char* ThreadEventName(ThreadEvent case ThreadEvent::CallStart : break; } size_t callId = (size_t)aEvent - (size_t)ThreadEvent::CallStart; return GetRedirection(callId).mName; } int GetRecordingPid() { return gRecordingPid; } -void ResetPid() { gPid = getpid(); } - bool IsMainChild() { return gMainChild; } void SetMainChild() { gMainChild = true; } -bool ReplayingInCloud() { return gReplayingInCloud; } /////////////////////////////////////////////////////////////////////////////// // Record/Replay Assertions /////////////////////////////////////////////////////////////////////////////// extern "C" { MOZ_EXPORT void RecordReplayInterface_InternalRecordReplayAssert( @@ -345,17 +319,17 @@ MOZ_EXPORT void RecordReplayInterface_In } // Add the asserted string to the recording. char text[1024]; VsprintfLiteral(text, aFormat, aArgs); // This must be kept in sync with Stream::RecordOrReplayThreadEvent, which // peeks at the input string written after the thread event. - thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Assert, text); + thread->Events().RecordOrReplayThreadEvent(ThreadEvent::Assert); thread->Events().CheckInput(text); } MOZ_EXPORT void RecordReplayInterface_InternalRecordReplayAssertBytes( const void* aData, size_t aSize) { Thread* thread = Thread::Current(); RecordingEventSection res(thread); if (!res.CanAccessEvents()) { @@ -420,101 +394,10 @@ MOZ_EXPORT void RecordReplayInterface_In JSContext* cx = dom::danger::GetJSContext(); JS::PersistentRootedObject* root = new JS::PersistentRootedObject(cx); *root = static_cast<JSObject*>(aJSObj); } } } // extern "C" -static mach_port_t gCrashDetectorExceptionPort; - -// See AsmJSSignalHandlers.cpp. -static const mach_msg_id_t sExceptionId = 2405; - -// This definition was generated by mig (the Mach Interface Generator) for the -// routine 'exception_raise' (exc.defs). See js/src/wasm/WasmSignalHandlers.cpp. -#pragma pack(4) -typedef struct { - mach_msg_header_t Head; - /* start of the kernel processed data */ - mach_msg_body_t msgh_body; - mach_msg_port_descriptor_t thread; - mach_msg_port_descriptor_t task; - /* end of the kernel processed data */ - NDR_record_t NDR; - exception_type_t exception; - mach_msg_type_number_t codeCnt; - int64_t code[2]; -} Request__mach_exception_raise_t; -#pragma pack() - -typedef struct { - Request__mach_exception_raise_t body; - mach_msg_trailer_t trailer; -} ExceptionRequest; - -static void CrashDetectorThread(void*) { - kern_return_t kret; - - while (true) { - ExceptionRequest request; - kret = mach_msg(&request.body.Head, MACH_RCV_MSG, 0, sizeof(request), - gCrashDetectorExceptionPort, MACH_MSG_TIMEOUT_NONE, - MACH_PORT_NULL); - Print("Crashing: %s\n", gMozCrashReason); - - kern_return_t replyCode = KERN_FAILURE; - if (kret == KERN_SUCCESS && request.body.Head.msgh_id == sExceptionId && - request.body.exception == EXC_BAD_ACCESS && request.body.codeCnt == 2) { - uint8_t* faultingAddress = (uint8_t*)request.body.code[1]; - child::MinidumpInfo info(request.body.exception, request.body.code[0], - request.body.code[1], - request.body.thread.name, - request.body.task.name); - child::ReportCrash(info, faultingAddress); - } else { - child::ReportFatalError("CrashDetectorThread mach_msg " - "returned unexpected data"); - } - - __Reply__exception_raise_t reply; - reply.Head.msgh_bits = - MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request.body.Head.msgh_bits), 0); - reply.Head.msgh_size = sizeof(reply); - reply.Head.msgh_remote_port = request.body.Head.msgh_remote_port; - reply.Head.msgh_local_port = MACH_PORT_NULL; - reply.Head.msgh_id = request.body.Head.msgh_id + 100; - reply.NDR = NDR_record; - reply.RetCode = replyCode; - mach_msg(&reply.Head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL, - MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); - } -} - -static void InitializeCrashDetector() { - MOZ_RELEASE_ASSERT(AreThreadEventsPassedThrough()); - kern_return_t kret; - - // Get a port which can send and receive data. - kret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, - &gCrashDetectorExceptionPort); - MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS); - - kret = mach_port_insert_right(mach_task_self(), gCrashDetectorExceptionPort, - gCrashDetectorExceptionPort, - MACH_MSG_TYPE_MAKE_SEND); - MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS); - - // Create a thread to block on reading the port. - Thread::SpawnNonRecordedThread(CrashDetectorThread, nullptr); - - // Set exception ports on the entire task. Unfortunately, this clobbers any - // other exception ports for the task, and forwarding to those other ports - // is not easy to get right. - kret = task_set_exception_ports( - mach_task_self(), EXC_MASK_BAD_ACCESS, gCrashDetectorExceptionPort, - EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE); - MOZ_RELEASE_ASSERT(kret == KERN_SUCCESS); -} - } // namespace recordreplay } // namespace mozilla
--- a/toolkit/recordreplay/ProcessRecordReplay.h +++ b/toolkit/recordreplay/ProcessRecordReplay.h @@ -67,20 +67,20 @@ enum class ThreadEvent : uint32_t { // The start of event IDs for redirected call events. Event IDs after this // point are platform specific. CallStart }; // Get the printable name for a thread event. const char* ThreadEventName(ThreadEvent aEvent); -class Recording; +class File; -// Recording being written to or read from. -extern Recording* gRecording; +// File used during recording and replay. +extern File* gRecordingFile; // Whether record/replay state has finished initialization. extern bool gInitialized; // If we failed to initialize, any associated message. On an initialization // failure, events will be passed through until we have connected with the // middleman, reported the failure, and crashed. extern char* gInitializationFailureMessage; @@ -101,19 +101,16 @@ void HitEndOfRecording(); // Called in a replaying process to load the last checkpoint in the recording. size_t RecordingEndpoint(); // Access the flag for whether this is the main child. The main child never // rewinds and sends graphics updates to the middleman while running forward. bool IsMainChild(); void SetMainChild(); -// Whether we are replaying a recording on a machine in the cloud. -bool ReplayingInCloud(); - // Get the process kind and recording file specified at the command line. // These are available in the middleman as well as while recording/replaying. extern ProcessKind gProcessKind; extern char* gRecordingFilename; /////////////////////////////////////////////////////////////////////////////// // Helper Functions /////////////////////////////////////////////////////////////////////////////// @@ -201,19 +198,16 @@ void InternalPrint(const char* aFormat, MOZ_MakeRecordReplayPrinter(Print, false) MOZ_MakeRecordReplayPrinter(PrintSpew, true) #undef MOZ_MakeRecordReplayPrinter // Get the ID of the process that produced the recording. int GetRecordingPid(); -// Update the current pid after a fork. -void ResetPid(); - /////////////////////////////////////////////////////////////////////////////// // Profiling /////////////////////////////////////////////////////////////////////////////// void InitializeCurrentTime(); // Get a current timestamp, in microseconds. double CurrentTime(); @@ -234,33 +228,131 @@ struct AutoTimer { private: TimerKind mKind; double mStart; }; void DumpTimers(); /////////////////////////////////////////////////////////////////////////////// +// Memory Management +/////////////////////////////////////////////////////////////////////////////// + +// In cases where memory is tracked and should be saved/restored with +// checkoints, malloc and other standard library functions suffice to allocate +// memory in the record/replay system. The routines below are used for handling +// redirections for the raw system calls underlying the standard libraries, and +// for cases where allocated memory should be untracked: the contents are +// ignored when saving/restoring checkpoints. + +// Different kinds of memory used in the system. +enum class MemoryKind { + // Memory whose contents are saved/restored with checkpoints. + Tracked, + + // All remaining memory kinds refer to untracked memory. + + // Memory not fitting into one of the categories below. + Generic, + + // Memory used for thread snapshots. + ThreadSnapshot, + + // Memory used by various parts of the memory snapshot system. + TrackedRegions, + FreeRegions, + DirtyPageSet, + SortedDirtyPageSet, + PageCopy, + + // Memory used by various parts of JS integration. + ScriptHits, + + Count +}; + +// Allocate or deallocate a block of memory of a particular kind. Allocated +// memory is initially zeroed. +void* AllocateMemory(size_t aSize, MemoryKind aKind); +void DeallocateMemory(void* aAddress, size_t aSize, MemoryKind aKind); + +// Allocation policy for managing memory of a particular kind. +template <MemoryKind Kind> +class AllocPolicy { + public: + template <typename T> + T* maybe_pod_calloc(size_t aNumElems) { + if (aNumElems & tl::MulOverflowMask<sizeof(T)>::value) { + MOZ_CRASH(); + } + // Note: AllocateMemory always returns zeroed memory. + return static_cast<T*>(AllocateMemory(aNumElems * sizeof(T), Kind)); + } + + template <typename T> + void free_(T* aPtr, size_t aSize) { + DeallocateMemory(aPtr, aSize * sizeof(T), Kind); + } + + template <typename T> + T* maybe_pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) { + T* res = maybe_pod_calloc<T>(aNewSize); + memcpy(res, aPtr, aOldSize * sizeof(T)); + free_<T>(aPtr, aOldSize); + return res; + } + + template <typename T> + T* maybe_pod_malloc(size_t aNumElems) { + return maybe_pod_calloc<T>(aNumElems); + } + + template <typename T> + T* pod_malloc(size_t aNumElems) { + return maybe_pod_malloc<T>(aNumElems); + } + + template <typename T> + T* pod_calloc(size_t aNumElems) { + return maybe_pod_calloc<T>(aNumElems); + } + + template <typename T> + T* pod_realloc(T* aPtr, size_t aOldSize, size_t aNewSize) { + return maybe_pod_realloc<T>(aPtr, aOldSize, aNewSize); + } + + void reportAllocOverflow() const {} + + MOZ_MUST_USE bool checkSimulatedOOM() const { return true; } +}; + +/////////////////////////////////////////////////////////////////////////////// // Redirection Bypassing /////////////////////////////////////////////////////////////////////////////// // The functions below bypass any redirections and give access to the system // even if events are not passed through in the current thread. These are // implemented in the various platform ProcessRedirect*.cpp files, and will // crash on errors which can't be handled internally. // Generic typedef for a system file handle. typedef size_t FileHandle; // Allocate/deallocate a block of memory directly from the system. -void* DirectAllocateMemory(size_t aSize); +void* DirectAllocateMemory(void* aAddress, size_t aSize); void DirectDeallocateMemory(void* aAddress, size_t aSize); -// Make a memory range inaccessible. -void DirectMakeInaccessible(void* aAddress, size_t aSize); +// Give a block of memory R or RX access. +void DirectWriteProtectMemory(void* aAddress, size_t aSize, bool aExecutable, + bool aIgnoreFailures = false); + +// Give a block of memory RW or RWX access. +void DirectUnprotectMemory(void* aAddress, size_t aSize, bool aExecutable, + bool aIgnoreFailures = false); // Open an existing file for reading or a new file for writing, clobbering any // existing file. FileHandle DirectOpenFile(const char* aFilename, bool aWriting); // Seek to an offset within a file open for reading. void DirectSeekFile(FileHandle aFd, uint64_t aOffset); @@ -275,26 +367,15 @@ void DirectWrite(FileHandle aFd, const v void DirectPrint(const char* aString); // Read data from a file, blocking until the read completes. size_t DirectRead(FileHandle aFd, void* aData, size_t aSize); // Create a new pipe. void DirectCreatePipe(FileHandle* aWriteFd, FileHandle* aReadFd); -typedef pthread_t NativeThreadId; - // Spawn a new thread. -NativeThreadId DirectSpawnThread(void (*aFunction)(void*), void* aArgument, - void* aStackBase, size_t aStackSize); - -// Get the current thread. -NativeThreadId DirectCurrentThread(); - -typedef pthread_mutex_t NativeLock; - -void DirectLockMutex(NativeLock* aLock, bool aPassThroughEvents = true); -void DirectUnlockMutex(NativeLock* aLock, bool aPassThroughEvents = true); +void DirectSpawnThread(void (*aFunction)(void*), void* aArgument); } // namespace recordreplay } // namespace mozilla #endif // mozilla_recordreplay_ProcessRecordReplay_h
--- a/toolkit/recordreplay/ProcessRedirect.cpp +++ b/toolkit/recordreplay/ProcessRedirect.cpp @@ -2,17 +2,17 @@ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ProcessRedirect.h" #include "InfallibleVector.h" -#include "ExternalCall.h" +#include "MiddlemanCall.h" #include "ipc/ChildInternal.h" #include "ipc/ParentInternal.h" #include "mozilla/Sprintf.h" #include <dlfcn.h> #include <string.h> #if defined(__clang__) @@ -87,46 +87,48 @@ extern "C" { return 1; } MOZ_RELEASE_ASSERT(thread->HasDivergedFromRecording()); // After we have diverged from the recording, we can't access the thread's // recording anymore. - // If the redirection has an external preamble hook, call it to see if it - // can handle this call. The external preamble hook is separate from the + // If the redirection has a middleman preamble hook, call it to see if it + // can handle this call. The middleman preamble hook is separate from the // normal preamble hook because entering the RecordingEventSection can // cause the current thread to diverge from the recording; testing for // HasDivergedFromRecording() does not work reliably in the normal preamble. - if (redirection.mExternalPreamble) { - if (CallPreambleHook(redirection.mExternalPreamble, aCallId, + if (redirection.mMiddlemanPreamble) { + if (CallPreambleHook(redirection.mMiddlemanPreamble, aCallId, aArguments)) { return 0; } } - // If the redirection has an external call hook, try to get its result - // from another process. - if (redirection.mExternalCall) { - if (OnExternalCall(aCallId, aArguments, /* aPopulateOutput = */ true)) { + // If the redirection has a middleman call hook, try to perform the call in + // the middleman instead. + if (redirection.mMiddlemanCall) { + if (SendCallToMiddleman(aCallId, aArguments, + /* aPopulateOutput = */ true)) { return 0; } } if (child::CurrentRepaintCannotFail()) { // EnsureNotDivergedFromRecording is going to force us to crash, so fail // earlier with a more helpful error message. - child::ReportFatalError("Could not perform external call: %s\n", + child::ReportFatalError(Nothing(), + "Could not perform middleman call: %s\n", redirection.mName); } // Calling any redirection which performs the standard steps will cause // debugger operations that have diverged from the recording to fail. - EnsureNotDivergedFromRecording(Some(aCallId)); + EnsureNotDivergedFromRecording(); Unreachable(); } if (IsRecording()) { // Call the original function, passing through events while we do so. // Destroy the RecordingEventSection so that we don't prevent the file // from being flushed in case we end up blocking. res.reset(); @@ -142,44 +144,39 @@ extern "C" { // Add an event for the thread. thread->Events().RecordOrReplayThreadEvent(CallIdToThreadEvent(aCallId)); // Save any output produced by the call. if (redirection.mSaveOutput) { redirection.mSaveOutput(thread->Events(), aArguments, &error); } - // Save information about any external calls encountered if we haven't - // diverged from the recording, in case we diverge and later calls + // Save information about any potential middleman calls encountered if we + // haven't diverged from the recording, in case we diverge and later calls // access data produced by this one. - if (IsReplaying() && redirection.mExternalCall) { - (void)OnExternalCall(aCallId, aArguments, /* aDiverged = */ false); + if (IsReplaying() && redirection.mMiddlemanCall) { + (void)SendCallToMiddleman(aCallId, aArguments, /* aDiverged = */ false); } RestoreError(error); return 0; } // Entry point for all redirections. When generated code jumps here, %rax holds // the CallEvent being invoked, and all other registers and stack contents are // the same as when the call was originally invoked. This fills in a // CallArguments structure with information about the call, before invoking // RecordReplayInterceptCall. extern size_t RecordReplayRedirectCall(...); __asm( "_RecordReplayRedirectCall:" - // Save rbp for backtraces. - "pushq %rbp;" - "movq %rsp, %rbp;" - - // Make space for a CallArguments struct on the stack, with a little extra - // space for alignment. - "subq $624, %rsp;" + // Make space for a CallArguments struct on the stack. + "subq $616, %rsp;" // Fill in the structure's contents. "movq %rdi, 0(%rsp);" "movq %rsi, 8(%rsp);" "movq %rdx, 16(%rsp);" "movq %rcx, 24(%rsp);" "movq %r8, 32(%rsp);" "movq %r9, 40(%rsp);" @@ -192,17 +189,17 @@ extern size_t RecordReplayRedirectCall(. // Enter the loop below. The compiler might not place this block of code // adjacent to the loop, so perform the jump explicitly. "jmp _RecordReplayRedirectCall_Loop;" // Save stack arguments into the structure. "_RecordReplayRedirectCall_Loop:" "subq $1, %rsi;" - "movq 640(%rsp, %rsi, 8), %rdx;" // Ignore the rip/rbp saved on stack. + "movq 624(%rsp, %rsi, 8), %rdx;" // Ignore the return ip on the stack. "movq %rdx, 104(%rsp, %rsi, 8);" "testq %rsi, %rsi;" "jne _RecordReplayRedirectCall_Loop;" // Place the CallEvent being made into the first argument register. "movq %rax, %rdi;" // Place the structure's address into the second argument register. @@ -221,50 +218,43 @@ extern size_t RecordReplayRedirectCall(. "movq 16(%rsp), %rdx;" "movq 24(%rsp), %rcx;" "movq 32(%rsp), %r8;" "movq 40(%rsp), %r9;" "movsd 48(%rsp), %xmm0;" "movsd 56(%rsp), %xmm1;" "movsd 64(%rsp), %xmm2;" "movq 72(%rsp), %rax;" - "addq $624, %rsp;" - "popq %rbp;" + "addq $616, %rsp;" "jmpq *%rax;" // The message has been recorded/replayed. "RecordReplayRedirectCall_done:" // Restore scalar and floating point return values. "movq 72(%rsp), %rax;" "movq 80(%rsp), %rdx;" "movsd 88(%rsp), %xmm0;" "movsd 96(%rsp), %xmm1;" // Pop the structure from the stack. - "addq $624, %rsp;" + "addq $616, %rsp;" // Return to caller. - "popq %rbp;" "ret;"); // Call a function address with the specified arguments. extern void RecordReplayInvokeCallRaw(CallArguments* aArguments, void* aFnPtr); __asm( "_RecordReplayInvokeCallRaw:" - // Save rbp for backtraces. - "pushq %rbp;" - "movq %rsp, %rbp;" - // Save function pointer in rax. "movq %rsi, %rax;" - // Save arguments on the stack, with a second copy for alignment. - "push %rdi;" + // Save arguments on the stack. This also aligns the stack. "push %rdi;" // Count how many stack arguments we need to copy. "movq $64, %rsi;" // Enter the loop below. The compiler might not place this block of code // adjacent to the loop, so perform the jump explicitly. "jmp _RecordReplayInvokeCallRaw_Loop;" @@ -291,23 +281,21 @@ extern void RecordReplayInvokeCallRaw(Ca // Call the saved function pointer. "callq *%rax;" // Pop the copied stack arguments. "addq $512, %rsp;" // Save any return values to the arguments. "pop %rdi;" - "pop %rdi;" "movq %rax, 72(%rdi);" "movq %rdx, 80(%rdi);" "movsd %xmm0, 88(%rdi);" "movsd %xmm1, 96(%rdi);" - "popq %rbp;" "ret;"); } // extern "C" MOZ_NEVER_INLINE void RecordReplayInvokeCall(void* aFunction, CallArguments* aArguments) { RecordReplayInvokeCallRaw(aArguments, aFunction); } @@ -475,19 +463,17 @@ static uint8_t* MaybeInternalJumpTarget( !strstr(startName, "CTRunGetStringIndicesPtr")) || (strstr(startName, "CGColorSpaceCreateDeviceGray") && !strstr(startName, "CGColorSpaceCreateDeviceGray_block_invoke")) || (strstr(startName, "CGColorSpaceCreateDeviceRGB") && !strstr(startName, "CGColorSpaceCreateDeviceRGB_block_invoke")) || // For these functions, there is a syscall near the beginning which // other system threads might be inside. strstr(startName, "__workq_kernreturn") || - strstr(startName, "kevent64") || - // Workaround suspected udis86 bug when disassembling this function. - strstr(startName, "CGAffineTransformMakeScale")) { + strstr(startName, "kevent64")) { PrintRedirectSpew("Failed [%p]: Vetoed by annotation\n", aIpEnd - 1); return aIpEnd - 1; } } PrintRedirectSpew("Success!\n"); return nullptr; } @@ -507,18 +493,17 @@ static void RedirectFailure(const char* } static void UnknownInstruction(const char* aName, uint8_t* aIp, size_t aNbytes) { nsCString byteData; for (size_t i = 0; i < aNbytes; i++) { byteData.AppendPrintf(" %d", (int)aIp[i]); } - RedirectFailure("Unknown instruction in %s [%p]:%s", aName, aIp, - byteData.get()); + RedirectFailure("Unknown instruction in %s:%s", aName, byteData.get()); } // Try to emit instructions to |aAssembler| with equivalent behavior to any // special jump or ip-dependent instruction at |aIp|, returning true if the // instruction was copied. static bool CopySpecialInstruction(uint8_t* aIp, ud_t* aUd, size_t aNbytes, Assembler& aAssembler) { aAssembler.NoteOriginalInstruction(aIp); @@ -551,31 +536,24 @@ static bool CopySpecialInstruction(uint8 aAssembler.CallRax(); } else if (mnemonic == UD_Ijmp) { aAssembler.Jump(target); } else { aAssembler.ConditionalJump(aUd->primary_opcode, target); } return true; } - if (op->type == UD_OP_MEM && !op->index) { - if (op->base == UD_R_RIP) { - if (op->offset == 32) { - // jmp *$offset32(%rip) - uint8_t* addr = aIp + aNbytes + op->lval.sdword; - aAssembler.MoveImmediateToRax(addr); - aAssembler.LoadRax(8); - aAssembler.JumpToRax(); - return true; - } - } else { - // Non-IP relative call or jump. - aAssembler.CopyInstruction(aIp, aNbytes); - return true; - } + if (op->type == UD_OP_MEM && op->base == UD_R_RIP && !op->index && + op->offset == 32) { + // jmp *$offset32(%rip) + uint8_t* addr = aIp + aNbytes + op->lval.sdword; + aAssembler.MoveImmediateToRax(addr); + aAssembler.LoadRax(8); + aAssembler.JumpToRax(); + return true; } } if (mnemonic == UD_Imov || mnemonic == UD_Ilea) { MOZ_RELEASE_ASSERT(!ud_insn_opr(aUd, 2)); const ud_operand* dst = ud_insn_opr(aUd, 0); const ud_operand* src = ud_insn_opr(aUd, 1); if (dst->type == UD_OP_REG && src->type == UD_OP_MEM && @@ -649,34 +627,16 @@ static bool CopySpecialInstruction(uint8 aAssembler.PushRax(); aAssembler.MoveImmediateToRax(addr); aAssembler.LoadRax(8); aAssembler.CompareRaxWithTopOfStack(); aAssembler.PopRax(); aAssembler.PopRax(); return true; } - if (dst->type == UD_OP_MEM && src->type == UD_OP_REG && - dst->base == UD_R_RIP && !dst->index && dst->offset == 32) { - // cmpq reg, $offset32(%rip) - int reg = Assembler::NormalizeRegister(src->base); - if (!reg) { - return false; - } - uint8_t* addr = aIp + aNbytes + dst->lval.sdword; - aAssembler.PushRax(); - aAssembler.MoveRegisterToRax(reg); - aAssembler.PushRax(); - aAssembler.MoveImmediateToRax(addr); - aAssembler.LoadRax(8); - aAssembler.CompareTopOfStackWithRax(); - aAssembler.PopRax(); - aAssembler.PopRax(); - return true; - } } if (mnemonic == UD_Ixchg) { const ud_operand* dst = ud_insn_opr(aUd, 0); const ud_operand* src = ud_insn_opr(aUd, 1); if (src->type == UD_OP_REG && src->size == 8 && dst->type == UD_OP_MEM && dst->base == UD_R_RIP && !dst->index && dst->offset == 32) { // xchgb reg, $offset32(%rip) @@ -766,47 +726,21 @@ static uint8_t* CopyInstructions(const c uint8_t* aIpEnd, Assembler& aAssembler) { uint8_t* ip = aIpStart; while (ip < aIpEnd) { ip += CopyInstruction(aName, ip, aAssembler); } return ip; } -static bool PreserveCallerSaveRegisters(const char* aName) { - // LLVM assumes that the call made when getting thread local variables will - // preserve registers that are normally caller save. It's not clear what ABI - // is actually assumed for this function so preserve all possible registers. - return !strcmp(aName, "tlv_get_addr"); -} - // Generate code to set %rax and enter RecordReplayRedirectCall. static uint8_t* GenerateRedirectStub(Assembler& aAssembler, size_t aCallId) { uint8_t* newFunction = aAssembler.Current(); - if (PreserveCallerSaveRegisters(GetRedirection(aCallId).mName)) { - static int registers[] = { - UD_R_RDI, UD_R_RDI /* for alignment */, UD_R_RSI, UD_R_RDX, UD_R_RCX, - UD_R_R8, UD_R_R9, UD_R_R10, UD_R_R11, - }; - for (size_t i = 0; i < ArrayLength(registers); i++) { - aAssembler.MoveRegisterToRax(registers[i]); - aAssembler.PushRax(); - } - aAssembler.MoveImmediateToRax((void*)aCallId); - uint8_t* after = aAssembler.Current() + PushImmediateBytes + JumpBytes; - aAssembler.PushImmediate(after); - aAssembler.Jump(BitwiseCast<void*>(RecordReplayRedirectCall)); - for (int i = ArrayLength(registers) - 1; i >= 0; i--) { - aAssembler.PopRegister(registers[i]); - } - aAssembler.Return(); - } else { - aAssembler.MoveImmediateToRax((void*)aCallId); - aAssembler.Jump(BitwiseCast<void*>(RecordReplayRedirectCall)); - } + aAssembler.MoveImmediateToRax((void*)aCallId); + aAssembler.Jump(BitwiseCast<void*>(RecordReplayRedirectCall)); return newFunction; } // Setup a redirection: overwrite the machine code for its base function, and // fill in its original function, to satisfy the function pointer behaviors // described in the Redirection structure. aCursor and aCursorEnd are used to // allocate executable memory for use in the redirection. static void Redirect(size_t aCallId, Redirection& aRedirection, @@ -816,18 +750,18 @@ static void Redirect(size_t aCallId, Red // is doing a best effort sort of thing, and on failure it will crash safely. // The main thing we want to avoid is corrupting the code so that it has been // redirected but might crash or behave incorrectly when executed. uint8_t* functionStart = aRedirection.mBaseFunction; uint8_t* ro = functionStart; if (!functionStart) { if (aFirstPass) { - PrintRedirectSpew("Could not find symbol %s for redirecting.\n", - aRedirection.mName); + PrintSpew("Could not find symbol %s for redirecting.\n", + aRedirection.mName); } return; } if (aRedirection.mOriginalFunction != aRedirection.mBaseFunction) { // We already redirected this function. MOZ_RELEASE_ASSERT(!aFirstPass); return;
--- a/toolkit/recordreplay/ProcessRedirect.h +++ b/toolkit/recordreplay/ProcessRedirect.h @@ -68,53 +68,67 @@ namespace recordreplay { // functions is incomplete. If a library API is not redirected then it might // behave differently between recording and replaying, or it might crash while // replaying. /////////////////////////////////////////////////////////////////////////////// // Function Redirections /////////////////////////////////////////////////////////////////////////////// -// Capture the arguments that can be passed to a redirection, and provide -// storage to specify the redirection's return value. We only need to capture -// enough argument data here for calls made directly from Gecko code, -// i.e. where events are not passed through. Calls made while events are passed -// through are performed with the same stack and register state as when they -// were initially invoked. -// -// Arguments and return value indexes refer to the register contents as passed -// to the function originally. For functions with complex or floating point -// arguments and return values, the right index to use might be different than -// expected, per the requirements of the System V x64 ABI. -struct CallArguments { - // The maximum number of stack arguments that can be captured. - static const size_t NumStackArguments = 64; +struct CallArguments; +// All argument and return value data that is stored in registers and whose +// values are preserved when calling a redirected function. +struct CallRegisterArguments { protected: size_t arg0; // 0 size_t arg1; // 8 size_t arg2; // 16 size_t arg3; // 24 size_t arg4; // 32 size_t arg5; // 40 double floatarg0; // 48 double floatarg1; // 56 double floatarg2; // 64 size_t rval0; // 72 size_t rval1; // 80 double floatrval0; // 88 double floatrval1; // 96 + // Size: 104 + + public: + void CopyFrom(const CallRegisterArguments* aArguments); + void CopyTo(CallRegisterArguments* aArguments) const; + void CopyRvalFrom(const CallRegisterArguments* aArguments); +}; + +// Capture the arguments that can be passed to a redirection, and provide +// storage to specify the redirection's return value. We only need to capture +// enough argument data here for calls made directly from Gecko code, +// i.e. where events are not passed through. Calls made while events are passed +// through are performed with the same stack and register state as when they +// were initially invoked. +// +// Arguments and return value indexes refer to the register contents as passed +// to the function originally. For functions with complex or floating point +// arguments and return values, the right index to use might be different than +// expected, per the requirements of the System V x64 ABI. +struct CallArguments : public CallRegisterArguments { + // The maximum number of stack arguments that can be captured. + static const size_t NumStackArguments = 64; + + protected: size_t stack[NumStackArguments]; // 104 // Size: 616 public: template <typename T> T& Arg(size_t aIndex) { static_assert(sizeof(T) == sizeof(size_t), "Size must match"); - static_assert(IsFloatingPoint<T>::value == false, "Use FloatArg"); + static_assert(IsFloatingPoint<T>::value == false, "FloatArg NYI"); MOZ_RELEASE_ASSERT(aIndex < 70); switch (aIndex) { case 0: return (T&)arg0; case 1: return (T&)arg1; case 2: return (T&)arg2; @@ -129,29 +143,16 @@ struct CallArguments { } } template <size_t Index, typename T> T& Arg() { return Arg<T>(Index); } - template <size_t Index> - double& FloatArg() { - static_assert(Index < 3, "Bad index"); - switch (Index) { - case 0: - return floatarg0; - case 1: - return floatarg1; - case 2: - return floatarg2; - } - } - template <size_t Offset> size_t* StackAddress() { static_assert(Offset % sizeof(size_t) == 0, "Bad stack offset"); return &stack[Offset / sizeof(size_t)]; } template <typename T, size_t Index = 0> T& Rval() { @@ -173,16 +174,34 @@ struct CallArguments { case 0: return floatrval0; case 1: return floatrval1; } } }; +inline void CallRegisterArguments::CopyFrom( + const CallRegisterArguments* aArguments) { + memcpy(this, aArguments, sizeof(CallRegisterArguments)); +} + +inline void CallRegisterArguments::CopyTo( + CallRegisterArguments* aArguments) const { + memcpy(aArguments, this, sizeof(CallRegisterArguments)); +} + +inline void CallRegisterArguments::CopyRvalFrom( + const CallRegisterArguments* aArguments) { + rval0 = aArguments->rval0; + rval1 = aArguments->rval1; + floatrval0 = aArguments->floatrval0; + floatrval1 = aArguments->floatrval1; +} + // Generic type for a system error code. typedef ssize_t ErrorType; // Signature for a function that saves some output produced by a redirection // while recording, and restores that output while replaying. aEvents is the // event stream for the current thread, aArguments specifies the arguments to // the called function, and aError specifies any system error which the call // produces. @@ -207,20 +226,20 @@ enum class PreambleResult { // events were passed through. PassThrough, }; // Signature for a function that is called on entry to a redirection and can // modify its behavior. typedef PreambleResult (*PreambleFn)(CallArguments* aArguments); -// Signature for a function that conveys data about a call to or from an -// external process. -struct ExternalCallContext; -typedef void (*ExternalCallFn)(ExternalCallContext& aCx); +// Signature for a function that conveys data about a call to or from the +// middleman process. +struct MiddlemanCallContext; +typedef void (*MiddlemanCallFn)(MiddlemanCallContext& aCx); // Information about a system library API function which is being redirected. struct Redirection { // Name of the function being redirected. const char* mName; // Address of the function which is being redirected. The code for this // function is modified so that attempts to call this function will instead @@ -236,23 +255,23 @@ struct Redirection { // If specified, will be called at the end of the redirection when events are // not being passed through to record/replay any outputs from the call. SaveOutputFn mSaveOutput; // If specified, will be called upon entry to the redirected call. PreambleFn mPreamble; - // If specified, allows this call to be made after diverging from the - // recording. See ExternalCall.h - ExternalCallFn mExternalCall; + // If specified, will be called while replaying and diverged from the + // recording to perform this call in the middleman process. + MiddlemanCallFn mMiddlemanCall; // Additional preamble that is only called while replaying and diverged from // the recording. - PreambleFn mExternalPreamble; + PreambleFn mMiddlemanPreamble; }; // Platform specific methods describing the set of redirections. size_t NumRedirections(); Redirection& GetRedirection(size_t aCallId); // Platform specific early initialization of redirections. This is done on both // recording/replaying and middleman processes, and allows OriginalCall() to @@ -375,31 +394,16 @@ static inline void RR_CStringRval(Stream // the returned buffer. rval = len ? (char*)malloc(len) : nullptr; } if (len) { aEvents.RecordOrReplayBytes(rval, len); } } -// Record/replay a fixed size rval buffer. -template <size_t ByteCount> -static inline void RR_RvalBuffer(Stream& aEvents, CallArguments* aArguments, - ErrorType* aError) { - auto& rval = aArguments->Rval<void*>(); - bool hasRval = IsRecording() && rval; - aEvents.RecordOrReplayValue(&hasRval); - if (IsReplaying()) { - rval = hasRval ? NewLeakyArray<char>(ByteCount) : nullptr; - } - if (hasRval) { - aEvents.RecordOrReplayBytes(rval, ByteCount); - } -} - // Ensure that the return value matches the specified argument. template <size_t Argument> static inline void RR_RvalIsArgument(Stream& aEvents, CallArguments* aArguments, ErrorType* aError) { auto& rval = aArguments->Rval<size_t>(); auto& arg = aArguments->Arg<Argument, size_t>(); if (IsRecording()) { MOZ_RELEASE_ASSERT(rval == arg); @@ -499,24 +503,16 @@ static inline void RR_WriteOptionalBuffe ErrorType* aError) { auto& buf = aArguments->Arg<BufferArg, void*>(); aEvents.CheckInput(!!buf); if (buf) { aEvents.RecordOrReplayBytes(buf, ByteCount); } } -// Record/replay an out parameter. -template <size_t Arg, typename Type> -static inline void RR_OutParam(Stream& aEvents, CallArguments* aArguments, - ErrorType* aError) { - RR_WriteOptionalBufferFixedSize<Arg, sizeof(Type)>(aEvents, aArguments, - aError); -} - // Record/replay the contents of a buffer at argument BufferArg with byte size // CountArg, where the call return value plus Offset indicates the amount of // data written to the buffer by the call. The return value must already have // been recorded/replayed. template <size_t BufferArg, size_t CountArg, size_t Offset = 0> static inline void RR_WriteBufferViaRval(Stream& aEvents, CallArguments* aArguments, ErrorType* aError) { @@ -587,24 +583,16 @@ static inline PreambleResult Preamble_Pa } static inline PreambleResult Preamble_WaitForever(CallArguments* aArguments) { Thread::WaitForever(); Unreachable(); return PreambleResult::PassThrough; } -static inline PreambleResult Preamble_NYI(CallArguments* aArguments) { - if (AreThreadEventsPassedThrough()) { - return PreambleResult::PassThrough; - } - MOZ_CRASH("Redirection NYI"); - return PreambleResult::Veto; -} - /////////////////////////////////////////////////////////////////////////////// // Other Redirection Interfaces /////////////////////////////////////////////////////////////////////////////// // Given an argument function aFunction, generate code for a new function that // takes one fewer argument than aFunction and then calls aFunction with all // its arguments and the aArgument value in the last argument position. //
--- a/toolkit/recordreplay/ProcessRedirectDarwin.cpp +++ b/toolkit/recordreplay/ProcessRedirectDarwin.cpp @@ -5,41 +5,39 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ProcessRedirect.h" #include "mozilla/Maybe.h" #include "HashTable.h" #include "Lock.h" +#include "MemorySnapshot.h" #include "ProcessRecordReplay.h" #include "ProcessRewind.h" #include "base/eintr_wrapper.h" +#include <dlfcn.h> +#include <fcntl.h> +#include <signal.h> + #include <bsm/audit.h> #include <bsm/audit_session.h> -#include <dirent.h> -#include <dlfcn.h> -#include <fcntl.h> #include <mach/clock.h> #include <mach/mach.h> #include <mach/mach_time.h> #include <mach/mach_vm.h> #include <mach/vm_map.h> -#include <mach-o/loader.h> -#include <signal.h> #include <sys/attr.h> #include <sys/event.h> #include <sys/mman.h> #include <sys/mount.h> #include <sys/param.h> #include <sys/stat.h> -#include <sys/syscall.h> #include <sys/time.h> -#include <sys/utsname.h> #include <time.h> #include <Carbon/Carbon.h> #include <SystemConfiguration/SystemConfiguration.h> #include <objc/objc-runtime.h> namespace mozilla { namespace recordreplay { @@ -52,31 +50,32 @@ namespace recordreplay { // normal non-redirected semantics) is needed. #define FOR_EACH_ORIGINAL_FUNCTION(MACRO) \ MACRO(__workq_kernreturn) \ MACRO(CFDataGetLength) \ MACRO(close) \ MACRO(lseek) \ MACRO(mach_absolute_time) \ MACRO(mmap) \ + MACRO(mprotect) \ + MACRO(munmap) \ MACRO(objc_msgSend) \ MACRO(open) \ MACRO(OSSpinLockLock) \ MACRO(pipe) \ MACRO(PL_HashTableDestroy) \ MACRO(pthread_cond_wait) \ MACRO(pthread_cond_timedwait) \ MACRO(pthread_cond_timedwait_relative_np) \ MACRO(pthread_create) \ MACRO(pthread_mutex_destroy) \ MACRO(pthread_mutex_init) \ MACRO(pthread_mutex_lock) \ MACRO(pthread_mutex_trylock) \ MACRO(pthread_mutex_unlock) \ - MACRO(pthread_self) \ MACRO(read) \ MACRO(start_wqthread) \ MACRO(write) #define DECLARE_ORIGINAL_FUNCTION(aName) static void* gOriginal_##aName; FOR_EACH_ORIGINAL_FUNCTION(DECLARE_ORIGINAL_FUNCTION) @@ -173,25 +172,27 @@ static void InitializeStaticClasses() { gCFConstantStringClass = objc_lookUpClass("__NSCFConstantString"); MOZ_RELEASE_ASSERT(gCFConstantStringClass); } // Capture an Objective C or CoreFoundation input to a call, which may come // either from another middleman call, or from static data in the replaying // process. -static void EX_ObjCInput(ExternalCallContext& aCx, id* aThingPtr) { - MOZ_RELEASE_ASSERT(aCx.AccessInput()); +static void MM_ObjCInput(MiddlemanCallContext& aCx, id* aThingPtr) { + MOZ_RELEASE_ASSERT(aCx.AccessPreface()); - if (EX_SystemInput(aCx, (const void**)aThingPtr)) { + if (MM_SystemInput(aCx, (const void**)aThingPtr)) { // This value came from a previous middleman call. return; } - if (aCx.mPhase == ExternalCallPhase::SaveInput) { + MOZ_RELEASE_ASSERT(aCx.AccessInput()); + + if (aCx.mPhase == MiddlemanCallPhase::ReplayInput) { // Try to determine where this object came from. // Watch for messages sent to particular classes. for (size_t i = 0; i < ArrayLength(gStaticClassNames); i++) { if (gStaticClasses[i] == (Class)*aThingPtr) { const char* className = gStaticClassNames[i]; aCx.WriteInputScalar((size_t)ObjCInputKind::StaticClass); size_t len = strlen(className) + 1; @@ -202,26 +203,26 @@ static void EX_ObjCInput(ExternalCallCon } // Watch for constant compile time strings baked into the generated code or // stored in system libraries. Be careful when accessing the pointer as in // the case where a middleman call hook for a function is missing the // pointer could have originated from the recording and its address may not // be mapped. In this case we would rather gracefully recover and fail to // paint, instead of crashing. - Dl_info info; - if (dladdr(*aThingPtr, &info) != 0) { + if (MemoryRangeIsTracked(*aThingPtr, sizeof(CFConstantString))) { CFConstantString* str = (CFConstantString*)*aThingPtr; if (str->mClass == gCFConstantStringClass && str->mLength <= 4096 && // Sanity check. - dladdr(str->mData, &info) != 0) { + MemoryRangeIsTracked(str->mData, str->mLength)) { InfallibleVector<UniChar> buffer; + NS_ConvertUTF8toUTF16 converted(str->mData, str->mLength); aCx.WriteInputScalar((size_t)ObjCInputKind::ConstantString); aCx.WriteInputScalar(str->mLength); - aCx.WriteInputBytes(str->mData, str->mLength); + aCx.WriteInputBytes(converted.get(), str->mLength * sizeof(UniChar)); return; } } aCx.MarkAsFailed(); return; } @@ -230,178 +231,120 @@ static void EX_ObjCInput(ExternalCallCon size_t len = aCx.ReadInputScalar(); UniquePtr<char[]> className(new char[len]); aCx.ReadInputBytes(className.get(), len); *aThingPtr = (id)objc_lookUpClass(className.get()); break; } case ObjCInputKind::ConstantString: { size_t len = aCx.ReadInputScalar(); - UniquePtr<char[]> cstring(new char[len]); - aCx.ReadInputBytes(cstring.get(), len); - - // When replaying in the cloud, external string references are generated - // on the fly and don't have correct contents. Parse the contents we are - // given so we can do a dynamic lookup and generate the right string. - static const char cloudPrefix[] = "RECORD_REPLAY_STRING:"; - if (len >= sizeof(cloudPrefix) && - !memcmp(cstring.get(), cloudPrefix, sizeof(cloudPrefix))) { - MOZ_RELEASE_ASSERT(cstring.get()[len - 1] == 0); - void* ptr = dlsym(RTLD_DEFAULT, cstring.get() + strlen(cloudPrefix)); - MOZ_RELEASE_ASSERT(ptr); - *aThingPtr = (id)*(CFStringRef*)ptr; - break; - } - - NS_ConvertUTF8toUTF16 converted(cstring.get(), len); UniquePtr<UniChar[]> contents(new UniChar[len]); - memcpy(contents.get(), converted.get(), len * sizeof(UniChar)); + aCx.ReadInputBytes(contents.get(), len * sizeof(UniChar)); *aThingPtr = (id)CFStringCreateWithCharacters(kCFAllocatorDefault, contents.get(), len); break; } default: MOZ_CRASH(); } } template <size_t Argument> -static void EX_CFTypeArg(ExternalCallContext& aCx) { - if (aCx.AccessInput()) { +static void MM_CFTypeArg(MiddlemanCallContext& aCx) { + if (aCx.AccessPreface()) { auto& object = aCx.mArguments->Arg<Argument, id>(); - EX_ObjCInput(aCx, &object); + MM_ObjCInput(aCx, &object); } } -static void EX_CFTypeOutput(ExternalCallContext& aCx, CFTypeRef* aOutput, +static void MM_CFTypeOutput(MiddlemanCallContext& aCx, CFTypeRef* aOutput, bool aOwnsReference) { - EX_SystemOutput(aCx, (const void**)aOutput); + MM_SystemOutput(aCx, (const void**)aOutput); - const void* value = *aOutput; - if (value && aCx.mPhase == ExternalCallPhase::SaveOutput && !IsReplaying()) { - if (!aOwnsReference) { - CFRetain(value); + if (*aOutput) { + switch (aCx.mPhase) { + case MiddlemanCallPhase::MiddlemanOutput: + if (!aOwnsReference) { + CFRetain(*aOutput); + } + break; + case MiddlemanCallPhase::MiddlemanRelease: + CFRelease(*aOutput); + break; + default: + break; } - aCx.mReleaseCallbacks->append([=]() { CFRelease(value); }); } } // For APIs using the 'Get' rule: no reference is held on the returned value. -static void EX_CFTypeRval(ExternalCallContext& aCx) { +static void MM_CFTypeRval(MiddlemanCallContext& aCx) { auto& rval = aCx.mArguments->Rval<CFTypeRef>(); - EX_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ false); + MM_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ false); } // For APIs using the 'Create' rule: a reference is held on the returned // value which must be released. -static void EX_CreateCFTypeRval(ExternalCallContext& aCx) { +static void MM_CreateCFTypeRval(MiddlemanCallContext& aCx) { auto& rval = aCx.mArguments->Rval<CFTypeRef>(); - EX_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ true); + MM_CFTypeOutput(aCx, &rval, /* aOwnsReference = */ true); } template <size_t Argument> -static void EX_CFTypeOutputArg(ExternalCallContext& aCx) { - auto& arg = aCx.mArguments->Arg<Argument, const void**>(); +static void MM_CFTypeOutputArg(MiddlemanCallContext& aCx) { + MM_WriteBufferFixedSize<Argument, sizeof(const void*)>(aCx); - if (aCx.mPhase == ExternalCallPhase::RestoreInput) { - arg = (const void**) aCx.AllocateBytes(sizeof(const void*)); - } - - EX_CFTypeOutput(aCx, arg, /* aOwnsReference = */ false); + auto arg = aCx.mArguments->Arg<Argument, const void**>(); + MM_CFTypeOutput(aCx, arg, /* aOwnsReference = */ false); } static void SendMessageToObject(const void* aObject, const char* aMessage) { CallArguments arguments; arguments.Arg<0, const void*>() = aObject; arguments.Arg<1, SEL>() = sel_registerName(aMessage); RecordReplayInvokeCall(gOriginal_objc_msgSend, &arguments); } // For APIs whose result will be released by the middleman's autorelease pool. -static void EX_AutoreleaseCFTypeRval(ExternalCallContext& aCx) { - auto& rvalReference = aCx.mArguments->Rval<const void*>(); - EX_SystemOutput(aCx, &rvalReference); - const void* rval = rvalReference; +static void MM_AutoreleaseCFTypeRval(MiddlemanCallContext& aCx) { + auto& rval = aCx.mArguments->Rval<const void*>(); + MM_SystemOutput(aCx, &rval); - if (rval && aCx.mPhase == ExternalCallPhase::SaveOutput && !IsReplaying()) { - SendMessageToObject(rval, "retain"); - aCx.mReleaseCallbacks->append([=]() { + if (rval) { + switch (aCx.mPhase) { + case MiddlemanCallPhase::MiddlemanOutput: + SendMessageToObject(rval, "retain"); + break; + case MiddlemanCallPhase::MiddlemanRelease: SendMessageToObject(rval, "autorelease"); - }); + break; + default: + break; + } } } // For functions which have an input CFType value and also have side effects on // that value, this associates the call with its own input value so that this // will be treated as a dependent for any future calls using the value. template <size_t Argument> -static void EX_UpdateCFTypeArg(ExternalCallContext& aCx) { +static void MM_UpdateCFTypeArg(MiddlemanCallContext& aCx) { auto arg = aCx.mArguments->Arg<Argument, const void*>(); - EX_CFTypeArg<Argument>(aCx); - EX_SystemOutput(aCx, &arg, /* aUpdating = */ true); + MM_CFTypeArg<Argument>(aCx); + MM_SystemOutput(aCx, &arg, /* aUpdating = */ true); } template <int Error = EAGAIN> static PreambleResult Preamble_SetError(CallArguments* aArguments) { aArguments->Rval<ssize_t>() = -1; errno = Error; return PreambleResult::Veto; } -#define ForEachFixedInputAddress(Macro) \ - Macro(kCFTypeArrayCallBacks) \ - Macro(kCFTypeDictionaryKeyCallBacks) \ - Macro(kCFTypeDictionaryValueCallBacks) - -#define ForEachFixedInput(Macro) \ - Macro(kCFAllocatorDefault) \ - Macro(kCFAllocatorNull) - -enum class FixedInput { -#define DefineEnum(Name) Name, - ForEachFixedInputAddress(DefineEnum) - ForEachFixedInput(DefineEnum) -#undef DefineEnum -}; - -static const void* GetFixedInput(FixedInput aWhich) { - switch (aWhich) { -#define FetchEnumAddress(Name) case FixedInput::Name: return &Name; - ForEachFixedInputAddress(FetchEnumAddress) -#undef FetchEnumAddress -#define FetchEnum(Name) case FixedInput::Name: return Name; - ForEachFixedInput(FetchEnum) -#undef FetchEnum - } - MOZ_CRASH("Unknown fixed input"); - return nullptr; -} - -template <size_t Arg, FixedInput Which> -static void EX_RequireFixed(ExternalCallContext& aCx) { - auto& arg = aCx.mArguments->Arg<Arg, const void*>(); - - if (aCx.AccessInput()) { - const void* value = GetFixedInput(Which); - if (aCx.mPhase == ExternalCallPhase::SaveInput) { - MOZ_RELEASE_ASSERT(arg == value || - (Which == FixedInput::kCFAllocatorDefault && - arg == nullptr)); - } else { - arg = value; - } - } -} - -template <size_t Arg> -static void EX_RequireDefaultAllocator(ExternalCallContext& aCx) { - EX_RequireFixed<0, FixedInput::kCFAllocatorDefault>(aCx); -} - /////////////////////////////////////////////////////////////////////////////// // system call redirections /////////////////////////////////////////////////////////////////////////////// static void RR_recvmsg(Stream& aEvents, CallArguments* aArguments, ErrorType* aError) { auto& msg = aArguments->Arg<1, struct msghdr*>(); @@ -430,52 +373,94 @@ static PreambleResult MiddlemanPreamble_ auto msg = aArguments->Arg<1, msghdr*>(); for (int i = 0; i < msg->msg_iovlen; i++) { totalSize += msg->msg_iov[i].iov_len; } aArguments->Rval<size_t>() = totalSize; return PreambleResult::Veto; } +static PreambleResult Preamble_mprotect(CallArguments* aArguments) { + // Ignore any mprotect calls that occur after taking a snapshot. + if (!NumSnapshots()) { + return PreambleResult::PassThrough; + } + aArguments->Rval<ssize_t>() = 0; + return PreambleResult::Veto; +} + static PreambleResult Preamble_mmap(CallArguments* aArguments) { auto& address = aArguments->Arg<0, void*>(); auto& size = aArguments->Arg<1, size_t>(); auto& prot = aArguments->Arg<2, size_t>(); auto& flags = aArguments->Arg<3, size_t>(); auto& fd = aArguments->Arg<4, size_t>(); auto& offset = aArguments->Arg<5, size_t>(); MOZ_RELEASE_ASSERT(address == PageBase(address)); - bool mappingFile = !(flags & MAP_ANON) && !AreThreadEventsPassedThrough(); - if (IsReplaying() && mappingFile) { - flags |= MAP_ANON; - prot |= PROT_WRITE; - fd = 0; - offset = 0; + // Make sure that fixed mappings do not interfere with snapshot state. + if (flags & MAP_FIXED) { + CheckFixedMemory(address, RoundupSizeToPageBoundary(size)); } - if (IsReplaying() && !AreThreadEventsPassedThrough()) { - flags &= ~MAP_SHARED; - flags |= MAP_PRIVATE; + void* memory = nullptr; + if ((flags & MAP_ANON) || + (IsReplaying() && !AreThreadEventsPassedThrough())) { + // Get an anonymous mapping for the result. + if (flags & MAP_FIXED) { + // For fixed allocations, make sure this memory region is mapped and zero. + if (!NumSnapshots()) { + // Make sure this memory region is writable. + CallFunction<int>(gOriginal_mprotect, address, size, + PROT_READ | PROT_WRITE | PROT_EXEC); + } + memset(address, 0, size); + memory = address; + } else { + memory = AllocateMemoryTryAddress( + address, RoundupSizeToPageBoundary(size), MemoryKind::Tracked); + } + } else { + // We have to call mmap itself, which can change memory protection flags + // for memory that is already allocated. If we haven't taken a snapshot + // then this is no problem, but after taking a snapshot we have to make + // sure that protection flags are what we expect them to be. + int newProt = NumSnapshots() ? (PROT_READ | PROT_EXEC) : prot; + memory = CallFunction<void*>(gOriginal_mmap, address, size, newProt, flags, + fd, offset); + + if (flags & MAP_FIXED) { + MOZ_RELEASE_ASSERT(memory == address); + RestoreWritableFixedMemory(memory, RoundupSizeToPageBoundary(size)); + } else if (memory && memory != (void*)-1) { + RegisterAllocatedMemory(memory, RoundupSizeToPageBoundary(size), + MemoryKind::Tracked); + } } - void* memory = CallFunction<void*>(gOriginal_mmap, address, size, prot, flags, - fd, offset); - - if (mappingFile) { + if (!(flags & MAP_ANON) && !AreThreadEventsPassedThrough()) { // Include the data just mapped in the recording. MOZ_RELEASE_ASSERT(memory && memory != (void*)-1); RecordReplayBytes(memory, size); } aArguments->Rval<void*>() = memory; return PreambleResult::Veto; } +static PreambleResult Preamble_munmap(CallArguments* aArguments) { + auto& address = aArguments->Arg<0, void*>(); + auto& size = aArguments->Arg<1, size_t>(); + + DeallocateMemory(address, size, MemoryKind::Tracked); + aArguments->Rval<ssize_t>() = 0; + return PreambleResult::Veto; +} + static PreambleResult MiddlemanPreamble_write(CallArguments* aArguments) { // Silently pretend that writes succeed after diverging from the recording. aArguments->Rval<size_t>() = aArguments->Arg<2, size_t>(); return PreambleResult::Veto; } static void RR_getsockopt(Stream& aEvents, CallArguments* aArguments, ErrorType* aError) { @@ -536,41 +521,47 @@ static PreambleResult MiddlemanPreamble_ break; default: MOZ_CRASH(); } aArguments->Rval<ssize_t>() = 0; return PreambleResult::Veto; } -// Record/replay a variety of sysctl-like functions. -template <size_t BufferArg> -static void RR_sysctl(Stream& aEvents, CallArguments* aArguments, - ErrorType* aError) { - auto& old = aArguments->Arg<BufferArg, void*>(); - auto& oldlenp = aArguments->Arg<BufferArg + 1, size_t*>(); +static PreambleResult Preamble___disable_threadsignal( + CallArguments* aArguments) { + // __disable_threadsignal is called when a thread finishes. During replay a + // terminated thread can cause problems such as changing access bits on + // tracked memory behind the scenes. + // + // Ideally, threads will never try to finish when we are replaying, since we + // are supposed to have control over all threads in the system and only spawn + // threads which will run forever. Unfortunately, GCD might have already + // spawned threads before we were able to install our redirections, so use a + // fallback here to keep these threads from terminating. + if (IsReplaying()) { + Thread::WaitForeverNoIdle(); + } + return PreambleResult::PassThrough; +} + +static void RR___sysctl(Stream& aEvents, CallArguments* aArguments, + ErrorType* aError) { + auto& old = aArguments->Arg<2, void*>(); + auto& oldlenp = aArguments->Arg<3, size_t*>(); aEvents.CheckInput((old ? 1 : 0) | (oldlenp ? 2 : 0)); if (oldlenp) { aEvents.RecordOrReplayValue(oldlenp); } if (old) { aEvents.RecordOrReplayBytes(old, *oldlenp); } } -static PreambleResult Preamble_sysctlbyname(CallArguments* aArguments) { - auto name = aArguments->Arg<0, const char*>(); - - // Include the environment variable being checked in an assertion, to make it - // easier to debug recording mismatches involving sysctlbyname. - RecordReplayAssert("sysctlbyname %s", name); - return PreambleResult::Redirect; -} - static PreambleResult Preamble___workq_kernreturn(CallArguments* aArguments) { // Busy-wait until initialization is complete. while (!gInitialized) { ThreadYield(); } // Make sure we know this thread exists. Thread::Current(); @@ -591,50 +582,38 @@ static PreambleResult Preamble_start_wqt RecordReplayInvokeCall(gOriginal_start_wqthread, aArguments); return PreambleResult::Veto; } /////////////////////////////////////////////////////////////////////////////// // pthreads redirections /////////////////////////////////////////////////////////////////////////////// -void DirectLockMutex(pthread_mutex_t* aMutex, bool aPassThroughEvents) { - Maybe<AutoPassThroughThreadEvents> pt; - if (aPassThroughEvents) { - pt.emplace(); - } +static void DirectLockMutex(pthread_mutex_t* aMutex) { + AutoPassThroughThreadEvents pt; ssize_t rv = CallFunction<ssize_t>(gOriginal_pthread_mutex_lock, aMutex); - if (rv != 0) { - Print("CRASH DirectLockMutex %d %d\n", rv, errno); - } MOZ_RELEASE_ASSERT(rv == 0);