diff --git a/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js b/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js index b2e5fc53d14c..ad760a5d8aa7 100644 --- a/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js +++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js @@ -45,6 +45,11 @@ async function checkMessageObjectContents(msg, expected, expandList = []) { }); } +function checkJumpIcon(msg) { + const jumpIcon = msg.querySelector(".jump-definition"); + ok(jumpIcon, "Found a jump icon"); +} + // Test evaluating various expressions in the console after time warping. add_task(async function() { const dbg = await attachRecordingDebugger("doc_rr_objects.html", { @@ -54,13 +59,32 @@ add_task(async function() { const { threadFront, toolbox } = dbg; const console = await toolbox.selectTool("webconsole"); const hud = console.hud; + let msg; + + await waitForMessage(hud, "Array(20) [ 0, 1, 2, 3, 4, 5,"); + await waitForMessage(hud, "Uint8Array(20) [ 0, 1, 2, 3, 4, 5,"); + await waitForMessage(hud, "Set(22) [ null, null, 0, 1, 2, 3, 4, 5,"); + await waitForMessage( + hud, + "Map(21) { {…} → {…}, 0 → 1, 1 → 2, 2 → 3, 3 → 4, 4 → 5," + ); + await waitForMessage(hud, "WeakSet(10)"); + await waitForMessage(hud, "WeakMap(10)"); + await waitForMessage( + hud, + "Object { a: 0, a0: 0, a1: 1, a2: 2, a3: 3, a4: 4," + ); + await waitForMessage(hud, "/abc/gi"); + + msg = await waitForMessage(hud, "function bar()"); + checkJumpIcon(msg); await warpToMessage(hud, dbg, "Done"); const requests = await threadFront.debuggerRequests(); requests.forEach(({ request, stack }) => { - if (request.type != "pauseData") { + if (request.type != "pauseData" && request.type != "repaint") { dump(`Unexpected debugger request stack:\n${stack}\n`); ok(false, `Unexpected debugger request while paused: ${request.type}`); } @@ -80,8 +104,6 @@ f(); ); await BrowserTest.checkMessageStack(hud, "Error: there", [3, 5]); - let msg; - BrowserTest.execute(hud, "Array(1, 2, 3)"); msg = await waitForMessage(hud, "Array(3) [ 1, 2, 3 ]"); await checkMessageObjectContents(msg, ["0: 1", "1: 2", "2: 3", "length: 3"]); @@ -97,9 +119,9 @@ f(); "byteOffset: 0", ]); - BrowserTest.execute(hud, `RegExp("abc", "g")`); - msg = await waitForMessage(hud, "/abc/g"); - await checkMessageObjectContents(msg, ["global: true", `source: "abc"`]); + BrowserTest.execute(hud, `RegExp("abd", "g")`); + msg = await waitForMessage(hud, "/abd/g"); + await checkMessageObjectContents(msg, ["global: true", `source: "abd"`]); BrowserTest.execute(hud, "new Set([1, 2, 3])"); msg = await waitForMessage(hud, "Set(3) [ 1, 2, 3 ]"); @@ -133,5 +155,9 @@ f(); [""] ); + BrowserTest.execute(hud, "baz"); + msg = await waitForMessage(hud, "function baz()"); + checkJumpIcon(msg); + await shutdownDebugger(dbg); }); diff --git a/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html b/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html index e8b9cfacd1d3..c3cc7bcfae47 100644 --- a/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html +++ b/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html @@ -31,8 +31,28 @@ var h = /abc/gi; var i = new Date(); var j = RangeError(); var k = document.getElementById("foo"); +var l = bar; +console.log(a); +console.log(b); +console.log(c); +console.log(d); +console.log(e); +console.log(f); +console.log(g); +console.log(h); +console.log(i); +console.log(j); +console.log(l); console.log("Done"); window.setTimeout(recordingFinished); } foo(); + +function bar() { +console.log("bar"); +} + +function baz() { +console.log("baz"); +} diff --git a/devtools/server/actors/object/previewers.js b/devtools/server/actors/object/previewers.js index 289db8224612..7c9305c94eb6 100644 --- a/devtools/server/actors/object/previewers.js +++ b/devtools/server/actors/object/previewers.js @@ -119,16 +119,18 @@ const previewers = { grip.userDisplayName = hooks.createValueGrip(userDisplayName.value); } + let script; const dbgGlobal = hooks.getGlobalDebugObject(); if (dbgGlobal) { - const script = dbgGlobal.makeDebuggeeValue(obj.unsafeDereference()) - .script; - if (script) { - grip.location = { - url: script.url, - line: script.startLine, - }; - } + script = dbgGlobal.makeDebuggeeValue(obj.unsafeDereference()).script; + } else { + script = obj.script; + } + if (script) { + grip.location = { + url: script.url, + line: script.startLine, + }; } return true; diff --git a/devtools/server/actors/replay/control.js b/devtools/server/actors/replay/control.js index 06309067a107..9cd3f137c176 100644 --- a/devtools/server/actors/replay/control.js +++ b/devtools/server/actors/replay/control.js @@ -1434,10 +1434,10 @@ async function findLogpointHits( contents() { return { kind: "hitLogpoint", text, condition }; }, - onFinished(child, { data, result }) { + onFinished(child, { pauseData, result, resultData }) { if (result) { - addPauseData(point, data, /* trackCached */ true); - callback(point, gDebugger._convertCompletionValue(result)); + addPauseData(point, pauseData, /* trackCached */ true); + callback(point, result, resultData); } child.divergedFromRecording = true; }, diff --git a/devtools/server/actors/replay/debugger.js b/devtools/server/actors/replay/debugger.js index d0321dc72b49..24ebb2ef0e4b 100644 --- a/devtools/server/actors/replay/debugger.js +++ b/devtools/server/actors/replay/debugger.js @@ -37,6 +37,142 @@ const Direction = { NONE: "NONE", }; +// Pool of ReplayDebugger things that are grouped together and can refer to each +// other. Many things --- frames, objects, environments --- are specific to +// a pool and cannot be used in any other context. Normally a pool is associated +// with some point at which the debugger paused, but they may also be associated +// with the values in a console or logpoint message. +function ReplayPool(dbg, pauseData) { + this.dbg = dbg; + + // All ReplayDebuggerFramees that have been created for this pool, indexed by + // their index (zero is the oldest frame, with the index increasing for newer + // frames). + this.frames = []; + + // All ReplayDebuggerObjects and ReplayDebuggerEnvironments that are + // associated with this pool, indexed by their id. + this.objects = []; + + if (pauseData) { + this.addPauseData(pauseData); + } +} + +ReplayPool.prototype = { + getObject(id) { + if (id && !this.objects[id]) { + if (this != this.dbg._pool) { + return null; + } + const data = this.dbg._sendRequest({ type: "getObject", id }); + this.addObject(data); + } + return this.objects[id]; + }, + + addObject(data) { + switch (data.kind) { + case "Object": + this.objects[data.id] = new ReplayDebuggerObject(this, data); + break; + case "Environment": + this.objects[data.id] = new ReplayDebuggerEnvironment(this, data); + break; + default: + ThrowError("Unknown object kind"); + } + }, + + getFrame(index) { + if (index == NewestFrameIndex) { + if (this.frames.length) { + return this.frames[this.frames.length - 1]; + } + } else { + assert(index < this.frames.length); + if (this.frames[index]) { + return this.frames[index]; + } + } + + assert(this == this.dbg._pool); + const data = this.dbg._sendRequest({ type: "getFrame", index }); + + if (index == NewestFrameIndex) { + if ("index" in data) { + index = data.index; + } else { + // There are no frames on the stack. + return null; + } + } + + this.frames[index] = new ReplayDebuggerFrame(this, data); + return this.frames[index]; + }, + + addPauseData(pauseData) { + for (const { data, preview } of Object.values(pauseData.objects)) { + if (!this.objects[data.id]) { + this.addObject(data); + } + this.getObject(data.id)._preview = { + ...preview, + enumerableOwnProperties: mapify(preview.enumerableOwnProperties), + }; + } + + for (const { data, names } of Object.values(pauseData.environments)) { + if (!this.objects[data.id]) { + this.addObject(data); + } + this.getObject(data.id)._setNames(names); + } + + if (pauseData.frames) { + for (const frame of pauseData.frames) { + this.frames[frame.index] = new ReplayDebuggerFrame(this, frame); + } + } + }, + + convertValue(value) { + if (isNonNullObject(value)) { + if (value.object) { + return this.getObject(value.object); + } + switch (value.special) { + case "undefined": + return undefined; + case "Infinity": + return Infinity; + case "-Infinity": + return -Infinity; + case "NaN": + return NaN; + case "0": + return -0; + } + } + return value; + }, + + convertCompletionValue(value) { + if ("return" in value) { + return { return: this.convertValue(value.return) }; + } + if ("throw" in value) { + return { + throw: this.convertValue(value.throw), + stack: value.stack, + }; + } + ThrowError("Unexpected completion value"); + return null; // For eslint + }, +}; + function ReplayDebugger() { const existing = RecordReplayControl.registerReplayDebugger(this); if (existing) { @@ -54,16 +190,8 @@ function ReplayDebugger() { // All breakpoint positions and handlers installed by this debugger. this._breakpoints = []; - // All ReplayDebuggerFramees that have been created while paused at the - // current position, indexed by their index (zero is the oldest frame, with - // the index increasing for newer frames). These are invalidated when - // unpausing. - this._frames = []; - - // All ReplayDebuggerObjects and ReplayDebuggerEnvironments that have been - // created while paused at the current position, indexed by their id. These - // are invalidated when unpausing. - this._objects = []; + // The current pool of pause-local state. + this._pool = new ReplayPool(this); // All ReplayDebuggerScripts and ReplayDebuggerScriptSources that have been // created, indexed by their id. These stay valid even after unpausing. @@ -375,19 +503,15 @@ ReplayDebugger.prototype = { } }, - // Clear out all data that becomes invalid when the child unpauses. + // Reset the per-pause pool when the child unpauses. _invalidateAfterUnpause() { - this._frames.forEach(frame => frame._invalidate()); - this._frames.length = 0; - - this._objects.forEach(obj => obj._invalidate()); - this._objects.length = 0; + this._pool = new ReplayPool(this); }, // Fill in the debugger with (hopefully) all data the client/server need to // pause at the current location. _capturePauseData() { - if (this._frames.length) { + if (this._pool.frames.length) { return; } @@ -407,26 +531,7 @@ ReplayDebugger.prototype = { } } - for (const { data, preview } of Object.values(pauseData.objects)) { - if (!this._objects[data.id]) { - this._addObject(data); - } - this._getObject(data.id)._preview = { - ...preview, - enumerableOwnProperties: mapify(preview.enumerableOwnProperties), - }; - } - - for (const { data, names } of Object.values(pauseData.environments)) { - if (!this._objects[data.id]) { - this._addObject(data); - } - this._getObject(data.id)._setNames(names); - } - - for (const frame of pauseData.frames) { - this._frames[frame.index] = new ReplayDebuggerFrame(this, frame); - } + this._pool.addPauseData(pauseData); }, _virtualConsoleLog(position, text, condition, callback) { @@ -581,70 +686,11 @@ ReplayDebugger.prototype = { // Object methods ///////////////////////////////////////////////////////// - _getObject(id) { - if (id && !this._objects[id]) { - const data = this._sendRequest({ type: "getObject", id }); - this._addObject(data); - } - return this._objects[id]; - }, - - _addObject(data) { - switch (data.kind) { - case "Object": - this._objects[data.id] = new ReplayDebuggerObject(this, data); - break; - case "Environment": - this._objects[data.id] = new ReplayDebuggerEnvironment(this, data); - break; - default: - ThrowError("Unknown object kind"); - } - }, - - // Convert a value we received from the child. - _convertValue(value) { - if (isNonNullObject(value)) { - if (value.object) { - return this._getObject(value.object); - } - if (value.snapshot) { - return new ReplayDebuggerObjectSnapshot(this, value.snapshot); - } - switch (value.special) { - case "undefined": - return undefined; - case "Infinity": - return Infinity; - case "-Infinity": - return -Infinity; - case "NaN": - return NaN; - case "0": - return -0; - } - } - return value; - }, - - _convertCompletionValue(value) { - if ("return" in value) { - return { return: this._convertValue(value.return) }; - } - if ("throw" in value) { - return { - throw: this._convertValue(value.throw), - stack: value.stack, - }; - } - ThrowError("Unexpected completion value"); - return null; // For eslint - }, - // Convert a value for sending to the child. _convertValueForChild(value) { if (isNonNullObject(value)) { assert(value instanceof ReplayDebuggerObject); + assert(value._pool == this._pool); return { object: value._data.id }; } else if ( value === undefined || @@ -662,35 +708,8 @@ ReplayDebugger.prototype = { // Frame methods ///////////////////////////////////////////////////////// - _getFrame(index) { - if (index == NewestFrameIndex) { - if (this._frames.length) { - return this._frames[this._frames.length - 1]; - } - } else { - assert(index < this._frames.length); - if (this._frames[index]) { - return this._frames[index]; - } - } - - const data = this._sendRequest({ type: "getFrame", index }); - - if (index == NewestFrameIndex) { - if ("index" in data) { - index = data.index; - } else { - // There are no frames on the stack. - return null; - } - } - - this._frames[index] = new ReplayDebuggerFrame(this, data); - return this._frames[index]; - }, - getNewestFrame() { - return this._getFrame(NewestFrameIndex); + return this._pool.getFrame(NewestFrameIndex); }, ///////////////////////////////////////////////////////// @@ -701,10 +720,13 @@ ReplayDebugger.prototype = { // Console API message arguments need conversion to debuggee values, but // other contents of the message can be left alone. if (message.messageType == "ConsoleAPI" && message.arguments) { + // Each console message has its own pool of referenced objects. + const pool = new ReplayPool(this, message.argumentsData); for (let i = 0; i < message.arguments.length; i++) { - message.arguments[i] = this._convertValue(message.arguments[i]); + message.arguments[i] = pool.convertValue(message.arguments[i]); } } + return message; }, @@ -827,7 +849,11 @@ ReplayDebuggerScript.prototype = { { kind: "Break", script: this._data.id, offset }, text, condition, - callback + (point, result, resultData) => { + const pool = new ReplayPool(this._dbg, resultData); + const converted = pool.convertCompletionValue(result); + callback(point, converted); + } ); }, @@ -892,27 +918,24 @@ ReplayDebuggerScriptSource.prototype = { // ReplayDebuggerFrame /////////////////////////////////////////////////////////////////////////////// -function ReplayDebuggerFrame(dbg, data) { - this._dbg = dbg; +function ReplayDebuggerFrame(pool, data) { + this._dbg = pool.dbg; + this._pool = pool; this._data = data; if (this._data.arguments) { - this._arguments = this._data.arguments.map(a => this._dbg._convertValue(a)); + this._arguments = this._data.arguments.map(a => this._pool.convertValue(a)); } } ReplayDebuggerFrame.prototype = { - _invalidate() { - this._data = null; - }, - get type() { return this._data.type; }, get callee() { - return this._dbg._getObject(this._data.callee); + return this._pool.getObject(this._data.callee); }, get environment() { - return this._dbg._getObject(this._data.environment); + return this._pool.getObject(this._data.environment); }, get generator() { return this._data.generator; @@ -921,7 +944,7 @@ ReplayDebuggerFrame.prototype = { return this._data.constructing; }, get this() { - return this._dbg._convertValue(this._data.this); + return this._pool.convertValue(this._data.this); }, get script() { return this._dbg._getScript(this._data.script); @@ -938,6 +961,7 @@ ReplayDebuggerFrame.prototype = { }, eval(text, options) { + assert(this._pool == this._dbg._pool); const rv = this._dbg._sendRequestAllowDiverge( { type: "frameEvaluate", @@ -947,7 +971,7 @@ ReplayDebuggerFrame.prototype = { }, { throw: "Recording divergence in frameEvaluate" } ); - return this._dbg._convertCompletionValue(rv); + return this._pool.convertCompletionValue(rv); }, _positionMatches(position, kind) { @@ -1001,7 +1025,7 @@ ReplayDebuggerFrame.prototype = { const result = this._dbg._sendRequest({ type: "popFrameResult" }); handler.call( this._dbg.getNewestFrame(), - this._dbg._convertCompletionValue(result) + this._pool.convertCompletionValue(result) ); }, { @@ -1022,7 +1046,7 @@ ReplayDebuggerFrame.prototype = { // This is the oldest frame. return null; } - return this._dbg._getFrame(this._data.index - 1); + return this._pool.getFrame(this._data.index - 1); }, get implementation() { @@ -1035,8 +1059,9 @@ ReplayDebuggerFrame.prototype = { // ReplayDebuggerObject /////////////////////////////////////////////////////////////////////////////// -function ReplayDebuggerObject(dbg, data) { - this._dbg = dbg; +function ReplayDebuggerObject(pool, data) { + this._dbg = pool.dbg; + this._pool = pool; this._data = data; this._preview = null; this._properties = null; @@ -1044,13 +1069,6 @@ function ReplayDebuggerObject(dbg, data) { } ReplayDebuggerObject.prototype = { - _invalidate() { - this._data = null; - this._preview = null; - this._properties = null; - this._containerContents = null; - }, - toString() { const id = this._data ? this._data.id : "INVALID"; return `ReplayDebugger.Object #${id}`; @@ -1087,13 +1105,13 @@ ReplayDebuggerObject.prototype = { return this._dbg._getScript(this._data.script); }, get environment() { - return this._dbg._getObject(this._data.environment); + return this._pool.getObject(this._data.environment); }, get isProxy() { return this._data.isProxy; }, get proto() { - return this._dbg._getObject(this._data.proto); + return this._pool.getObject(this._data.proto); }, isExtensible() { @@ -1159,6 +1177,10 @@ ReplayDebuggerObject.prototype = { _ensureProperties() { if (!this._properties) { + if (this._pool != this._dbg._pool) { + this._properties = mapify([]); + return; + } const id = this._data.id; const properties = this._dbg._sendRequestAllowDiverge( { type: "getObjectProperties", id }, @@ -1174,13 +1196,13 @@ ReplayDebuggerObject.prototype = { } const rv = Object.assign({}, desc); if ("value" in desc) { - rv.value = this._dbg._convertValue(desc.value); + rv.value = this._pool.convertValue(desc.value); } if ("get" in desc) { - rv.get = this._dbg._getObject(desc.get); + rv.get = this._pool.getObject(desc.get); } if ("set" in desc) { - rv.set = this._dbg._getObject(desc.set); + rv.set = this._pool.getObject(desc.set); } return rv; }, @@ -1191,6 +1213,7 @@ ReplayDebuggerObject.prototype = { contents = this._preview.containerContents; } else { if (!this._containerContents) { + assert(this._pool == this._dbg._pool); const id = this._data.id; this._containerContents = this._dbg._sendRequestAllowDiverge( { type: "getObjectContainerContents", id }, @@ -1202,9 +1225,9 @@ ReplayDebuggerObject.prototype = { return contents.map(value => { // Watch for [key, value] pairs in maps. if (value.length == 2) { - return value.map(v => this._dbg._convertValue(v)); + return value.map(v => this._pool.convertValue(v)); } - return this._dbg._convertValue(value); + return this._pool.convertValue(value); }); }, @@ -1212,34 +1235,34 @@ ReplayDebuggerObject.prototype = { if (!this.isProxy) { return this; } - return this._dbg._convertValue(this._data.proxyUnwrapped); + return this._pool.convertValue(this._data.proxyUnwrapped); }, get proxyTarget() { - return this._dbg._convertValue(this._data.proxyTarget); + return this._pool.convertValue(this._data.proxyTarget); }, get proxyHandler() { - return this._dbg._convertValue(this._data.proxyHandler); + return this._pool.convertValue(this._data.proxyHandler); }, get boundTargetFunction() { if (this.isBoundFunction) { - return this._dbg._getObject(this._data.boundTargetFunction); + return this._pool.getObject(this._data.boundTargetFunction); } return undefined; }, get boundThis() { if (this.isBoundFunction) { - return this._dbg._convertValue(this._data.boundThis); + return this._pool.convertValue(this._data.boundThis); } return undefined; }, get boundArguments() { if (this.isBoundFunction) { - return this._dbg._getObject(this._data.boundArguments); + return this._pool.getObject(this._data.boundArguments); } return undefined; }, @@ -1249,6 +1272,7 @@ ReplayDebuggerObject.prototype = { }, apply(thisv, args) { + assert(this._pool == this._dbg._pool); thisv = this._dbg._convertValueForChild(thisv); args = (args || []).map(v => this._dbg._convertValueForChild(v)); @@ -1261,7 +1285,7 @@ ReplayDebuggerObject.prototype = { }, { throw: "Recording divergence in objectApply" } ); - return this._dbg._convertCompletionValue(rv); + return this._pool.convertCompletionValue(rv); }, get allocationSite() { @@ -1322,52 +1346,29 @@ ReplayDebuggerObject.prototype = { ReplayDebugger.Object = ReplayDebuggerObject; -/////////////////////////////////////////////////////////////////////////////// -// ReplayDebuggerObjectSnapshot -/////////////////////////////////////////////////////////////////////////////// - -// Create an object based on snapshot data which can be consulted without -// communicating with the child process. This uses data provided by the child -// process in the same format as for normal ReplayDebuggerObjects, except that -// it does not contain references to any other objects. -function ReplayDebuggerObjectSnapshot(dbg, data) { - this._dbg = dbg; - this._data = data; - this._properties = new Map(); - data.properties.forEach(({ name, desc }) => { - this._properties.set(name, desc); - }); -} - -ReplayDebuggerObjectSnapshot.prototype = ReplayDebuggerObject.prototype; - /////////////////////////////////////////////////////////////////////////////// // ReplayDebuggerEnvironment /////////////////////////////////////////////////////////////////////////////// -function ReplayDebuggerEnvironment(dbg, data) { - this._dbg = dbg; +function ReplayDebuggerEnvironment(pool, data) { + this._dbg = pool.dbg; + this._pool = pool; this._data = data; this._names = null; } ReplayDebuggerEnvironment.prototype = { - _invalidate() { - this._data = null; - this._names = null; - }, - get type() { return this._data.type; }, get parent() { - return this._dbg._getObject(this._data.parent); + return this._pool.getObject(this._data.parent); }, get object() { - return this._dbg._getObject(this._data.object); + return this._pool.getObject(this._data.object); }, get callee() { - return this._dbg._getObject(this._data.callee); + return this._pool.getObject(this._data.callee); }, get optimizedOut() { return this._data.optimizedOut; @@ -1376,12 +1377,13 @@ ReplayDebuggerEnvironment.prototype = { _setNames(names) { this._names = {}; names.forEach(({ name, value }) => { - this._names[name] = this._dbg._convertValue(value); + this._names[name] = this._pool.convertValue(value); }); }, _ensureNames() { if (!this._names) { + assert(this._pool == this._dbg._pool); const names = this._dbg._sendRequestAllowDiverge( { type: "getEnvironmentNames", diff --git a/devtools/server/actors/replay/inspector.js b/devtools/server/actors/replay/inspector.js index a1d490319929..b262c0f2ddbf 100644 --- a/devtools/server/actors/replay/inspector.js +++ b/devtools/server/actors/replay/inspector.js @@ -30,6 +30,10 @@ function dbg() { return _dbg; } +function dbgObject(id) { + return dbg()._pool.getObject(id); +} + /////////////////////////////////////////////////////////////////////////////// // Public Interface /////////////////////////////////////////////////////////////////////////////// @@ -96,7 +100,7 @@ const ReplayInspector = { clientX: event.clientX, clientY: event.clientY, }); - const obj = dbg()._getObject(rv.id); + const obj = dbgObject(rv.id); return wrapValue(obj); }, @@ -139,7 +143,7 @@ function createSubstituteChrome(chrome) { const data = dbg()._sendRequestAllowDiverge({ type: "newDeepTreeWalker", }); - const obj = dbg()._getObject(data.id); + const obj = dbgObject(data.id); return wrapObject(obj); }, }, @@ -155,10 +159,10 @@ function createSubstituteChrome(chrome) { // Objects are considered dead if we have unpaused since creating them // and they are not one of the fixed proxies. This prevents the // inspector from trying to continue using them. - if (!unwrapped._data) { + if (unwrapped._pool != dbg()._pool) { updateFixedProxies(); unwrapped = proxyMap.get(node); - return !unwrapped._data; + return unwrapped._pool != dbg()._pool; } return false; }, @@ -266,7 +270,7 @@ function unwrapValue(value) { if (value instanceof Object) { const rv = dbg()._sendRequest({ type: "createObject" }); - const newobj = dbg()._getObject(rv.id); + const newobj = dbgObject(rv.id); Object.entries(value).forEach(([name, propvalue]) => { const unwrapped = unwrapValue(propvalue); @@ -284,7 +288,7 @@ function getObjectProperty(obj, name) { id: obj._data.id, name, }); - return dbg()._convertCompletionValue(rv); + return dbg()._pool.convertCompletionValue(rv); } function setObjectProperty(obj, name, value) { @@ -294,7 +298,7 @@ function setObjectProperty(obj, name, value) { name, value: dbg()._convertValueForChild(value), }); - return dbg()._convertCompletionValue(rv); + return dbg()._pool.convertCompletionValue(rv); } function getTargetObject(target) { @@ -479,11 +483,7 @@ function updateFixedProxies() { ReplayInspectorProxyHandler ); } - initFixedProxy( - gFixedProxy[key], - gFixedProxyTargets[key], - dbg()._getObject(value) - ); + initFixedProxy(gFixedProxy[key], gFixedProxyTargets[key], dbgObject(value)); } } diff --git a/devtools/server/actors/replay/replay.js b/devtools/server/actors/replay/replay.js index e6ce7a5e4e67..d6902058d901 100644 --- a/devtools/server/actors/replay/replay.js +++ b/devtools/server/actors/replay/replay.js @@ -269,13 +269,10 @@ Services.obs.addObserver( { observe(subject, topic, data) { assert(topic == "devtools-html-content"); - const { uri, offset, contents } = JSON.parse(data); + const { uri, contents } = JSON.parse(data); if (gHtmlContent.has(uri)) { const existing = gHtmlContent.get(uri); - if (existing.content.length == offset) { - assert(!existing.complete); - existing.content = existing.content + contents; - } + existing.content = existing.content + contents; } else { gHtmlContent.set(uri, { content: contents, @@ -287,52 +284,6 @@ Services.obs.addObserver( "devtools-html-content" ); -/////////////////////////////////////////////////////////////////////////////// -// Object Snapshots -/////////////////////////////////////////////////////////////////////////////// - -// Snapshots are generated for objects that might be inspected at times when we -// are not paused at the point where the snapshot was originally taken. The -// snapshot data is provided to the server, which can use it to provide limited -// answers to the client about the object's contents, without having to consult -// a child process. - -function snapshotObjectProperty([name, desc]) { - // Only capture primitive properties in object snapshots. - if ("value" in desc && !convertedValueIsObject(desc.value)) { - return { name, desc }; - } - return { name, desc: { value: "" } }; -} - -function makeObjectSnapshot(object) { - assert(object instanceof Debugger.Object); - - // Include properties that would be included in a normal object's data packet, - // except do not allow inspection of any other referenced objects. - // In particular, don't set the prototype so that the object inspector will - // not attempt to crawl the object's prototype chain. - return { - kind: "Object", - callable: object.callable, - isBoundFunction: object.isBoundFunction, - isArrowFunction: object.isArrowFunction, - isGeneratorFunction: object.isGeneratorFunction, - isAsyncFunction: object.isAsyncFunction, - class: object.class, - name: object.name, - displayName: object.displayName, - parameterNames: object.parameterNames, - isProxy: object.isProxy, - isExtensible: object.isExtensible(), - isSealed: object.isSealed(), - isFrozen: object.isFrozen(), - properties: Object.entries(getObjectProperties(object)).map( - snapshotObjectProperty - ), - }; -} - /////////////////////////////////////////////////////////////////////////////// // Console Message State /////////////////////////////////////////////////////////////////////////////// @@ -343,13 +294,7 @@ const gConsoleMessages = []; // Any new console messages since the last checkpoint. const gNewConsoleMessages = []; -function newConsoleMessage(messageType, executionPoint, contents) { - if (!executionPoint) { - executionPoint = currentScriptedExecutionPoint(); - } - - contents.messageType = messageType; - contents.executionPoint = executionPoint; +function newConsoleMessage(contents) { gConsoleMessages.push(contents); if (gManifest.kind == "resume") { @@ -379,11 +324,16 @@ Services.console.registerListener({ // If there is a warp target associated with the error, use that. This // will take users to the point where the error was originally generated, // rather than where it was reported to the console. - const executionPoint = gWarpTargetPoints[message.timeWarpTarget]; + let executionPoint = gWarpTargetPoints[message.timeWarpTarget]; + if (!executionPoint) { + executionPoint = currentScriptedExecutionPoint(); + } const contents = JSON.parse(JSON.stringify(message)); contents.stack = convertStack(message.stack); - newConsoleMessage("PageError", executionPoint, contents); + contents.executionPoint = executionPoint; + contents.messageType = "PageError"; + newConsoleMessage(contents); } }, }); @@ -396,21 +346,30 @@ Services.obs.addObserver( observe(message, topic, data) { const apiMessage = message.wrappedJSObject; - const contents = {}; + const contents = { messageType: "ConsoleAPI" }; for (const id in apiMessage) { if (id != "wrappedJSObject" && id != "arguments") { contents[id] = JSON.parse(JSON.stringify(apiMessage[id])); } } + contents.executionPoint = currentScriptedExecutionPoint(); + // Message arguments are preserved as debuggee values. if (apiMessage.arguments) { contents.arguments = apiMessage.arguments.map(v => { - return convertValue(makeDebuggeeValue(v), { snapshot: true }); + return convertValue(makeDebuggeeValue(v)); }); + + contents.argumentsData = new PreviewedObjects(); + contents.arguments.forEach(v => + contents.argumentsData.addValue(v, true) + ); + + ClearPausedState(); } - newConsoleMessage("ConsoleAPI", null, contents); + newConsoleMessage(contents); }, }, "console-api-log-event" @@ -822,11 +781,8 @@ function getObjectId(obj) { } // Convert a value for sending to the parent. -function convertValue(value, options) { +function convertValue(value) { if (value instanceof Debugger.Object) { - if (options && options.snapshot) { - return { snapshot: makeObjectSnapshot(value) }; - } return { object: getObjectId(value) }; } if ( @@ -841,17 +797,13 @@ function convertValue(value, options) { return value; } -function convertedValueIsObject(value) { - return isNonNullObject(value) && "object" in value; -} - -function convertCompletionValue(value, options) { +function convertCompletionValue(value) { if ("return" in value) { - return { return: convertValue(value.return, options) }; + return { return: convertValue(value.return) }; } if ("throw" in value) { return { - throw: convertValue(value.throw, options), + throw: convertValue(value.throw), stack: convertSavedFrameToPlainObject(value.stack), }; } @@ -1030,12 +982,16 @@ const gManifestStartHandlers = { const displayName = formatDisplayName(frame); const rv = frame.evalWithBindings(text, { displayName }); - const converted = convertCompletionValue(rv, { snapshot: true }); - const data = getPauseData(); - data.paintData = RecordReplayControl.repaint(); + const pauseData = getPauseData(); + pauseData.paintData = RecordReplayControl.repaint(); + ClearPausedState(); - RecordReplayControl.manifestFinished({ result: converted, data }); + const result = convertCompletionValue(rv); + const resultData = new PreviewedObjects(); + resultData.addCompletionValue(result, true); + + RecordReplayControl.manifestFinished({ result, resultData, pauseData }); }, }; @@ -1076,12 +1032,14 @@ function currentScriptedExecutionPoint() { if (!numFrames) { return null; } - const frame = getFrameData(numFrames - 1); + + const index = numFrames - 1; + const frame = scriptFrameForIndex(index); return currentExecutionPoint({ kind: "OnStep", - script: frame.script, + script: gScripts.getId(frame.script), offset: frame.offset, - frameIndex: frame.index, + frameIndex: index, }); } @@ -1495,6 +1453,143 @@ function getWindow() { // object. const OBJECT_PREVIEW_MAX_ITEMS = 10; +// A collection of objects which we can send up to the server, along with +// property information so that the server can show a preview for the object. +function PreviewedObjects() { + this.objects = {}; + this.environments = {}; +} + +PreviewedObjects.prototype = { + addValue(value, includeProperties) { + if (value && typeof value == "object" && value.object) { + this.addObject(value.object, includeProperties); + } + }, + + addObject(id, includeProperties) { + if (!id) { + return; + } + + // If includeProperties is set then previewing the object requires knowledge + // of its enumerable properties. + const needObject = !this.objects[id]; + const needProperties = + includeProperties && + (needObject || !this.objects[id].preview.enumerableOwnProperties); + + if (!needObject && !needProperties) { + return; + } + + const object = gPausedObjects.getObject(id); + assert(object instanceof Debugger.Object); + + const properties = getObjectProperties(object); + const propertyEntries = Object.entries(properties); + + if (needObject) { + this.objects[id] = { + data: getObjectData(id), + preview: { + ownPropertyNamesCount: propertyEntries.length, + }, + }; + + const preview = this.objects[id].preview; + + // Add some properties (if present) which the server might ask for + // even when it isn't interested in the rest of the properties. + if (properties.length) { + preview.lengthProperty = properties.length; + } + if (properties.displayName) { + preview.displayNameProperty = properties.displayName; + } + } + + if (needProperties) { + const preview = this.objects[id].preview; + + // The server is only interested in enumerable properties, and at most + // OBJECT_PREVIEW_MAX_ITEMS of them. Limiting the properties we send to + // only those the server needs avoids having to send the contents of huge + // objects like Windows, most of which will not be used. + const enumerableOwnProperties = Object.create(null); + let enumerablePropertyCount = 0; + for (const [name, desc] of propertyEntries) { + if (desc.enumerable) { + enumerableOwnProperties[name] = desc; + this.addPropertyDescriptor(desc, false); + if (++enumerablePropertyCount == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + preview.enumerableOwnProperties = enumerableOwnProperties; + + // The server is interested in at most OBJECT_PREVIEW_MAX_ITEMS items in + // set and map containers. + const containerContents = getObjectContainerContents(object); + if (containerContents) { + preview.containerContents = containerContents.slice( + 0, + OBJECT_PREVIEW_MAX_ITEMS + ); + preview.containerContents.forEach(v => this.addContainerValue(v)); + } + } + }, + + addPropertyDescriptor(desc, includeProperties) { + if (desc.value) { + this.addValue(desc.value, includeProperties); + } + if (desc.get) { + this.addObject(desc.get, includeProperties); + } + if (desc.set) { + this.addObject(desc.set, includeProperties); + } + }, + + addContainerValue(value) { + // Watch for [key, value] pairs in maps. + if (value.length == 2) { + value.forEach(v => this.addValue(v)); + } else { + this.addValue(value); + } + }, + + addCompletionValue(value, includeProperties) { + if ("return" in value) { + this.addValue(value.return, includeProperties); + } else if ("throw" in value) { + this.addValue(value.throw, includeProperties); + } + }, + + addEnvironment(id) { + if (!id || this.environments[id]) { + return; + } + + const env = gPausedObjects.getObject(id); + assert(env instanceof Debugger.Environment); + + const data = getObjectData(id); + const names = getEnvironmentNames(env); + this.environments[id] = { data, names }; + + names.forEach(({ value }) => this.addValue(value, true)); + + this.addObject(data.callee); + this.addEnvironment(data.parent); + }, +}; + // When the replaying process pauses, the server needs to inspect a lot of state // around frames, objects, etc. in order to fill in all the information the // client needs to update the UI for the pause location. Done naively, this @@ -1514,133 +1609,11 @@ function getPauseData() { return {}; } - const rv = { - frames: [], - scripts: {}, - offsetMetadata: [], - objects: {}, - environments: {}, - }; + const rv = new PreviewedObjects(); - function addValue(value, includeProperties) { - if (value && typeof value == "object" && value.object) { - addObject(value.object, includeProperties); - } - } - - function addObject(id, includeProperties) { - if (!id) { - return; - } - - // If includeProperties is set then previewing the object requires knowledge - // of its enumerable properties. - const needObject = !rv.objects[id]; - const needProperties = - includeProperties && - (needObject || !rv.objects[id].preview.enumerableOwnProperties); - - if (!needObject && !needProperties) { - return; - } - - const object = gPausedObjects.getObject(id); - assert(object instanceof Debugger.Object); - - const properties = getObjectProperties(object); - const propertyEntries = Object.entries(properties); - - if (needObject) { - rv.objects[id] = { - data: getObjectData(id), - preview: { - ownPropertyNamesCount: propertyEntries.length, - }, - }; - - const preview = rv.objects[id].preview; - - // Add some properties (if present) which the server might ask for - // even when it isn't interested in the rest of the properties. - if (properties.length) { - preview.lengthProperty = properties.length; - } - if (properties.displayName) { - preview.displayNameProperty = properties.displayName; - } - } - - if (needProperties) { - const preview = rv.objects[id].preview; - - // The server is only interested in enumerable properties, and at most - // OBJECT_PREVIEW_MAX_ITEMS of them. Limiting the properties we send to - // only those the server needs avoids having to send the contents of huge - // objects like Windows, most of which will not be used. - const enumerableOwnProperties = Object.create(null); - let enumerablePropertyCount = 0; - for (const [name, desc] of propertyEntries) { - if (desc.enumerable) { - enumerableOwnProperties[name] = desc; - addPropertyDescriptor(desc, false); - if (++enumerablePropertyCount == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - } - preview.enumerableOwnProperties = enumerableOwnProperties; - - // The server is interested in at most OBJECT_PREVIEW_MAX_ITEMS items in - // set and map containers. - const containerContents = getObjectContainerContents(object); - if (containerContents) { - preview.containerContents = containerContents.slice( - 0, - OBJECT_PREVIEW_MAX_ITEMS - ); - preview.containerContents.forEach(v => addContainerValue(v)); - } - } - } - - function addPropertyDescriptor(desc, includeProperties) { - if (desc.value) { - addValue(desc.value, includeProperties); - } - if (desc.get) { - addObject(desc.get, includeProperties); - } - if (desc.set) { - addObject(desc.set, includeProperties); - } - } - - function addContainerValue(value) { - // Watch for [key, value] pairs in maps. - if (value.length == 2) { - value.forEach(v => addValue(v)); - } else { - addValue(value); - } - } - - function addEnvironment(id) { - if (!id || rv.environments[id]) { - return; - } - - const env = gPausedObjects.getObject(id); - assert(env instanceof Debugger.Environment); - - const data = getObjectData(id); - const names = getEnvironmentNames(env); - rv.environments[id] = { data, names }; - - names.forEach(({ value }) => addValue(value, true)); - - addObject(data.callee); - addEnvironment(data.parent); - } + rv.frames = []; + rv.scripts = {}; + rv.offsetMetadata = []; // eslint-disable-next-line no-shadow function addScript(id) { @@ -1660,14 +1633,14 @@ function getPauseData() { metadata: script.getOffsetMetadata(dbgFrame.offset), }); addScript(frame.script); - addValue(frame.this, true); + rv.addValue(frame.this, true); if (frame.arguments) { for (const arg of frame.arguments) { - addValue(arg, true); + rv.addValue(arg, true); } } - addObject(frame.callee, false); - addEnvironment(frame.environment, true); + rv.addObject(frame.callee, false); + rv.addEnvironment(frame.environment, true); } return rv; @@ -1687,11 +1660,6 @@ function divergeFromRecording() { } const gRequestHandlers = { - repaint() { - divergeFromRecording(); - return RecordReplayControl.repaint(); - }, - ///////////////////////////////////////////////////////// // Debugger Requests ///////////////////////////////////////////////////////// @@ -1809,7 +1777,7 @@ const gRequestHandlers = { divergeFromRecording(); const frame = scriptFrameForIndex(request.index); const rv = frame.eval(request.text, request.options); - return convertCompletionValue(rv, request.convertOptions); + return convertCompletionValue(rv); }, popFrameResult(request) {