From 4174e65ff509c4d8dcce39f4cd6d6510fbc38173 Mon Sep 17 00:00:00 2001 From: Mihai Alexandru Michis Date: Tue, 17 Sep 2019 06:17:05 +0300 Subject: [PATCH] Backed out changeset ece44b8f51e4 (bug 1581418) for causing xpcshell failures. --HG-- rename : devtools/client/webreplay/mochitest/browser_rr_object_preview-01.js => devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js --- .../client/webreplay/mochitest/browser.ini | 4 +- ...1.js => browser_dbg_rr_console_warp-03.js} | 54 ++- .../mochitest/browser_dbg_rr_logpoint-03.js | 38 -- .../mochitest/browser_rr_object_preview-02.js | 44 -- .../mochitest/examples/doc_events.html | 18 - .../mochitest/examples/doc_rr_objects.html | 2 +- devtools/client/webreplay/mochitest/head.js | 40 -- devtools/server/actors/object/previewers.js | 37 +- devtools/server/actors/object/utils.js | 70 ++- devtools/server/actors/replay/control.js | 12 +- devtools/server/actors/replay/debugger.js | 102 ++--- devtools/server/actors/replay/inspector.js | 35 +- devtools/server/actors/replay/replay.js | 415 +++++++----------- devtools/shared/DevToolsUtils.js | 6 - devtools/shared/builtin-modules.js | 24 +- 15 files changed, 336 insertions(+), 565 deletions(-) rename devtools/client/webreplay/mochitest/{browser_rr_object_preview-01.js => browser_dbg_rr_console_warp-03.js} (74%) delete mode 100644 devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-03.js delete mode 100644 devtools/client/webreplay/mochitest/browser_rr_object_preview-02.js delete mode 100644 devtools/client/webreplay/mochitest/examples/doc_events.html diff --git a/devtools/client/webreplay/mochitest/browser.ini b/devtools/client/webreplay/mochitest/browser.ini index 47bc4d0f0e20..4c78fab2f875 100644 --- a/devtools/client/webreplay/mochitest/browser.ini +++ b/devtools/client/webreplay/mochitest/browser.ini @@ -33,11 +33,9 @@ support-files = [browser_dbg_rr_replay-03.js] [browser_dbg_rr_console_warp-01.js] [browser_dbg_rr_console_warp-02.js] +[browser_dbg_rr_console_warp-03.js] [browser_dbg_rr_logpoint-01.js] [browser_dbg_rr_logpoint-02.js] -[browser_dbg_rr_logpoint-03.js] [browser_rr_inspector-01.js] [browser_rr_inspector-02.js] [browser_rr_inspector-03.js] -[browser_rr_object_preview-01.js] -[browser_rr_object_preview-02.js] diff --git a/devtools/client/webreplay/mochitest/browser_rr_object_preview-01.js b/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js similarity index 74% rename from devtools/client/webreplay/mochitest/browser_rr_object_preview-01.js rename to devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js index d263f74d5f81..ad760a5d8aa7 100644 --- a/devtools/client/webreplay/mochitest/browser_rr_object_preview-01.js +++ b/devtools/client/webreplay/mochitest/browser_dbg_rr_console_warp-03.js @@ -6,13 +6,51 @@ "use strict"; +const BrowserTest = { + gTestPath, + ok, + is, + registerCleanupFunction, + waitForExplicitFinish, + BrowserTestUtils, +}; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/head.js", + BrowserTest +); + +async function checkMessageObjectContents(msg, expected, expandList = []) { + const oi = msg.querySelector(".tree"); + const node = oi.querySelector(".tree-node"); + BrowserTest.expandObjectInspectorNode(node); + + for (const label of expandList) { + const labelNode = await waitFor(() => + BrowserTest.findObjectInspectorNode(oi, label) + ); + BrowserTest.expandObjectInspectorNode(labelNode); + } + + const properties = await waitFor(() => { + const nodes = BrowserTest.getObjectInspectorNodes(oi); + if (nodes && nodes.length > 1) { + return [...nodes].map(n => n.textContent); + } + return null; + }); + + expected.forEach(s => { + ok(properties.find(v => v.includes(s)), `Object contents include "${s}"`); + }); +} + function checkJumpIcon(msg) { const jumpIcon = msg.querySelector(".jump-definition"); ok(jumpIcon, "Found a jump icon"); } -// Test the objects produced by console.log() calls and by evaluating various -// expressions in the console after time warping. +// Test evaluating various expressions in the console after time warping. add_task(async function() { const dbg = await attachRecordingDebugger("doc_rr_objects.html", { waitForRecording: true, @@ -25,24 +63,18 @@ add_task(async function() { 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) [ {…}, {…}, 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(20) [ {…}, {…}, {…},"); - await waitForMessage(hud, "WeakMap(20) { {…} → {…}, {…} → {…},"); + 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"); - await waitForMessage(hud, "Date"); - - // Note: this message has an associated stack but we don't have an easy way to - // check its contents as BrowserTest.checkMessageStack requires the stack to - // be collapsed. - await waitForMessage(hud, 'RangeError: "foo"'); msg = await waitForMessage(hud, "function bar()"); checkJumpIcon(msg); diff --git a/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-03.js b/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-03.js deleted file mode 100644 index 21a143d3074b..000000000000 --- a/devtools/client/webreplay/mochitest/browser_dbg_rr_logpoint-03.js +++ /dev/null @@ -1,38 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ -/* eslint-disable no-undef */ - -"use strict"; - -// Test event logpoints when replaying. -add_task(async function() { - await pushPref("devtools.debugger.features.log-event-breakpoints", true); - - const dbg = await attachRecordingDebugger("doc_events.html", { - waitForRecording: true, - }); - - const console = await getDebuggerSplitConsole(dbg); - const hud = console.hud; - - await dbg.actions.addEventListenerBreakpoints(["event.mouse.mousedown"]); - - const msg = await waitForMessage(hud, "mousedown"); - - // The message's inline preview should contain useful properties. - const regexps = [ - /target: HTMLDivElement/, - /clientX: \d+/, - /clientY: \d+/, - /layerX: \d+/, - /layerY: \d+/, - ]; - for (const regexp of regexps) { - ok(regexp.test(msg.textContent), `Message text includes ${regexp}`); - } - - // When expanded, other properties should be visible. - await checkMessageObjectContents(msg, ["altKey: false", "bubbles: true"]); - - await shutdownDebugger(dbg); -}); diff --git a/devtools/client/webreplay/mochitest/browser_rr_object_preview-02.js b/devtools/client/webreplay/mochitest/browser_rr_object_preview-02.js deleted file mode 100644 index be2f533bc136..000000000000 --- a/devtools/client/webreplay/mochitest/browser_rr_object_preview-02.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ -/* eslint-disable no-undef */ - -"use strict"; - -// Inspecting objects can lead to uncaught rejections when shutting down. -PromiseTestUtils.whitelistRejectionsGlobally( - /can't be sent as the connection just closed/ -); - -function findNode(dbg, text) { - for (let index = 0; ; index++) { - const elem = findElement(dbg, "scopeNode", index); - if (elem && elem.innerText == text) { - return elem; - } - } -} - -function toggleNode(dbg, text) { - return toggleObjectInspectorNode(findNode(dbg, text)); -} - -// Test that objects show up correctly in the scope pane. -add_task(async function() { - const dbg = await attachRecordingDebugger("doc_rr_objects.html", { - waitForRecording: true, - }); - - const console = await getDebuggerSplitConsole(dbg); - const hud = console.hud; - - await warpToMessage(hud, dbg, "Done"); - - // We should be able to expand the window and see its properties. - await toggleNode(dbg, ""); - findNode(dbg, "bar()"); - findNode(dbg, "baz()"); - - await shutdownDebugger(dbg); -}); diff --git a/devtools/client/webreplay/mochitest/examples/doc_events.html b/devtools/client/webreplay/mochitest/examples/doc_events.html deleted file mode 100644 index 6df895f3b2a3..000000000000 --- a/devtools/client/webreplay/mochitest/examples/doc_events.html +++ /dev/null @@ -1,18 +0,0 @@ - -
Hello World!
- diff --git a/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html b/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html index cbb3256be11c..c3cc7bcfae47 100644 --- a/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html +++ b/devtools/client/webreplay/mochitest/examples/doc_rr_objects.html @@ -29,7 +29,7 @@ for (let i = 0; i < 20; i++) { } var h = /abc/gi; var i = new Date(); -var j = RangeError("foo"); +var j = RangeError(); var k = document.getElementById("foo"); var l = bar; console.log(a); diff --git a/devtools/client/webreplay/mochitest/head.js b/devtools/client/webreplay/mochitest/head.js index 3fd797665d78..d2116ae03b9a 100644 --- a/devtools/client/webreplay/mochitest/head.js +++ b/devtools/client/webreplay/mochitest/head.js @@ -189,46 +189,6 @@ async function warpToMessage(hud, dbg, text) { } } -// For tests that need webconsole test features. -const BrowserTest = { - gTestPath, - ok, - is, - registerCleanupFunction, - waitForExplicitFinish, - BrowserTestUtils, -}; - -Services.scriptloader.loadSubScript( - "chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/head.js", - BrowserTest -); - -async function checkMessageObjectContents(msg, expected, expandList = []) { - const oi = msg.querySelector(".tree"); - const node = oi.querySelector(".tree-node"); - BrowserTest.expandObjectInspectorNode(node); - - for (const label of expandList) { - const labelNode = await waitFor(() => - BrowserTest.findObjectInspectorNode(oi, label) - ); - BrowserTest.expandObjectInspectorNode(labelNode); - } - - const properties = await waitFor(() => { - const nodes = BrowserTest.getObjectInspectorNodes(oi); - if (nodes && nodes.length > 1) { - return [...nodes].map(n => n.textContent); - } - return null; - }); - - expected.forEach(s => { - ok(properties.find(v => v.includes(s)), `Object contents include "${s}"`); - }); -} - const { PromiseTestUtils } = ChromeUtils.import( "resource://testing-common/PromiseTestUtils.jsm" ); diff --git a/devtools/server/actors/object/previewers.js b/devtools/server/actors/object/previewers.js index eae22b51847c..7d8e0c99ce4c 100644 --- a/devtools/server/actors/object/previewers.js +++ b/devtools/server/actors/object/previewers.js @@ -132,10 +132,7 @@ const previewers = { RegExp: [ function({ obj, hooks }, grip) { - const str = DevToolsUtils.callPropertyOnObject(obj, "toString"); - if (typeof str != "string") { - return false; - } + const str = ObjectUtils.getRegExpString(obj); grip.displayString = hooks.createValueGrip(str); return true; @@ -144,7 +141,7 @@ const previewers = { Date: [ function({ obj, hooks }, grip) { - const time = DevToolsUtils.callPropertyOnObject(obj, "getTime"); + const time = ObjectUtils.getDateTime(obj); if (typeof time != "number") { return false; } @@ -207,10 +204,7 @@ const previewers = { Set: [ function(objectActor, grip) { - const size = DevToolsUtils.getProperty(objectActor.obj, "size"); - if (typeof size != "number") { - return false; - } + const size = ObjectUtils.getContainerSize(objectActor.obj); grip.preview = { kind: "ArrayLike", @@ -262,10 +256,7 @@ const previewers = { Map: [ function(objectActor, grip) { - const size = DevToolsUtils.getProperty(objectActor.obj, "size"); - if (typeof size != "number") { - return false; - } + const size = ObjectUtils.getContainerSize(objectActor.obj); grip.preview = { kind: "MapLike", @@ -568,21 +559,11 @@ previewers.Object = [ case "SyntaxError": case "TypeError": case "URIError": - const name = DevToolsUtils.getProperty(obj, "name"); - const msg = DevToolsUtils.getProperty(obj, "message"); - const stack = DevToolsUtils.getProperty(obj, "stack"); - const fileName = DevToolsUtils.getProperty(obj, "fileName"); - const lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); - const columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); - grip.preview = { - kind: "Error", - name: hooks.createValueGrip(name), - message: hooks.createValueGrip(msg), - stack: hooks.createValueGrip(stack), - fileName: hooks.createValueGrip(fileName), - lineNumber: hooks.createValueGrip(lineNumber), - columnNumber: hooks.createValueGrip(columnNumber), - }; + grip.preview = { kind: "Error" }; + const properties = ObjectUtils.getErrorProperties(obj); + Object.keys(properties).forEach(p => { + grip.preview[p] = hooks.createValueGrip(properties[p]); + }); return true; default: return false; diff --git a/devtools/server/actors/object/utils.js b/devtools/server/actors/object/utils.js index dbfe7e83be32..359c2e39f5fa 100644 --- a/devtools/server/actors/object/utils.js +++ b/devtools/server/actors/object/utils.js @@ -198,13 +198,17 @@ function getArrayLength(object) { throw new Error("Expected an array, got a " + object.class); } - // Real arrays have a reliable `length` own property. When replaying, always - // get the length property, as we can't invoke getters on the proxy returned - // by unsafeDereference(). - if (object.class === "Array" || isReplaying) { + // Real arrays have a reliable `length` own property. + if (object.class === "Array") { return DevToolsUtils.getProperty(object, "length"); } + // When replaying, we use a special API to get typed array lengths. We can't + // invoke getters on the proxy returned by unsafeDereference(). + if (isReplaying) { + return object.getTypedArrayLength(); + } + // For typed arrays, `DevToolsUtils.getProperty` is not reliable because the `length` // getter could be shadowed by an own property, and `getOwnPropertyNames` is // unnecessarily slow. Obtain the `length` getter safely and call it manually. @@ -213,6 +217,25 @@ function getArrayLength(object) { return getter.call(object.unsafeDereference()); } +/** + * Returns the number of elements in a Set or Map. + * + * @param object Debugger.Object + * The debuggee object of the Set or Map. + * @return Number + */ +function getContainerSize(object) { + if (object.class != "Set" && object.class != "Map") { + throw new Error(`Expected a set/map, got a ${object.class}`); + } + + if (isReplaying) { + return object.getContainerSize(); + } + + return DevToolsUtils.getProperty(object, "size"); +} + /** * Returns true if the parameter is suitable to be an array index. * @@ -256,6 +279,41 @@ function getStorageLength(object) { return DevToolsUtils.getProperty(object, "length"); } +// Get the string representation of a Debugger.Object for a RegExp. +function getRegExpString(object) { + if (isReplaying) { + return object.getRegExpString(); + } + + return DevToolsUtils.callPropertyOnObject(object, "toString"); +} + +// Get the time associated with a Debugger.Object for a Date. +function getDateTime(object) { + if (isReplaying) { + return object.getDateTime(); + } + + return DevToolsUtils.callPropertyOnObject(object, "getTime"); +} + +// Get the properties of a Debugger.Object for an Error which are needed to +// preview the object. +function getErrorProperties(object) { + if (isReplaying) { + return object.getErrorProperties(); + } + + return { + name: DevToolsUtils.getProperty(object, "name"), + message: DevToolsUtils.getProperty(object, "message"), + stack: DevToolsUtils.getProperty(object, "stack"), + fileName: DevToolsUtils.getProperty(object, "fileName"), + lineNumber: DevToolsUtils.getProperty(object, "lineNumber"), + columnNumber: DevToolsUtils.getProperty(object, "columnNumber"), + }; +} + module.exports = { getPromiseState, makeDebuggeeValueIfNeeded, @@ -267,5 +325,9 @@ module.exports = { isStorage, getArrayLength, getStorageLength, + getContainerSize, isArrayIndex, + getRegExpString, + getDateTime, + getErrorProperties, }; diff --git a/devtools/server/actors/replay/control.js b/devtools/server/actors/replay/control.js index 2dabc46376f0..49cea0ccb867 100644 --- a/devtools/server/actors/replay/control.js +++ b/devtools/server/actors/replay/control.js @@ -1482,14 +1482,10 @@ async function evaluateLogpoint({ point, text, condition, callback }) { return { kind: "hitLogpoint", text, condition, skipPauseData }; }, onFinished(child, { pauseData, result, resultData, restoredSnapshot }) { - if (restoredSnapshot) { - if (!skipPauseData) { - // Gathering pause data sometimes triggers a snapshot restore. - skipPauseData = true; - sendAsyncManifest(manifest); - } else { - callback(point, ["Recording divergence evaluating logpoint"]); - } + if (restoredSnapshot && !skipPauseData) { + // Gathering pause data sometimes triggers a snapshot restore. + skipPauseData = true; + sendAsyncManifest(manifest); } else { if (result) { if (!skipPauseData) { diff --git a/devtools/server/actors/replay/debugger.js b/devtools/server/actors/replay/debugger.js index a6132efc7275..a39f8c3bebde 100644 --- a/devtools/server/actors/replay/debugger.js +++ b/devtools/server/actors/replay/debugger.js @@ -26,12 +26,6 @@ ChromeUtils.defineModuleGetter( "resource://devtools/shared/execution-point-utils.js" ); -loader.lazyRequireGetter( - this, - "ReplayInspector", - "devtools/server/actors/replay/inspector" -); - /////////////////////////////////////////////////////////////////////////////// // ReplayDebugger /////////////////////////////////////////////////////////////////////////////// @@ -125,8 +119,7 @@ ReplayPool.prototype = { } this.getObject(data.id)._preview = { ...preview, - properties: mapify(preview.properties), - callResults: mapify(preview.callResults), + enumerableOwnProperties: mapify(preview.enumerableOwnProperties), }; } @@ -1114,12 +1107,6 @@ ReplayDebuggerFrame.prototype = { // ReplayDebuggerObject /////////////////////////////////////////////////////////////////////////////// -// See replay.js -const PropertyLevels = { - BASIC: 1, - FULL: 2, -}; - function ReplayDebuggerObject(pool, data) { this._dbg = pool.dbg; this._pool = pool; @@ -1186,30 +1173,18 @@ ReplayDebuggerObject.prototype = { }, unsafeDereference() { - if (this.class == "Array") { - // ReplayInspector converts arrays to objects in this process, which we - // don't want to happen. - return null; - } - - return ReplayInspector.wrapObject(this); + // Direct access to the referent is not currently available. + return null; }, getOwnPropertyNames() { - if (this._preview && this._preview.level >= PropertyLevels.FULL) { - // The preview will include all properties of the object. - return this.getEnumerableOwnPropertyNamesForPreview(); - } this._ensureProperties(); return [...this._properties.keys()]; }, getEnumerableOwnPropertyNamesForPreview() { - if (this._preview && this._preview.level >= PropertyLevels.BASIC) { - if (!this._preview.properties) { - return []; - } - return [...this._preview.properties.keys()]; + if (this._preview && this._preview.enumerableOwnProperties) { + return [...this._preview.enumerableOwnProperties.keys()]; } return this.getOwnPropertyNames(); }, @@ -1228,10 +1203,20 @@ ReplayDebuggerObject.prototype = { getOwnPropertyDescriptor(name) { name = name.toString(); - if (this._preview && this._preview.properties) { - const desc = this._preview.properties.get(name); - if (desc || this._preview.level == PropertyLevels.FULL) { - return this._convertPropertyDescriptor(desc); + if (this._preview) { + if (this._preview.enumerableOwnProperties) { + const desc = this._preview.enumerableOwnProperties.get(name); + if (desc) { + return this._convertPropertyDescriptor(desc); + } + } + if (name == "length") { + return this._convertPropertyDescriptor(this._preview.lengthProperty); + } + if (name == "displayName") { + return this._convertPropertyDescriptor( + this._preview.displayNameProperty + ); } } this._ensureProperties(); @@ -1245,7 +1230,7 @@ ReplayDebuggerObject.prototype = { return; } const id = this._data.id; - const { properties } = this._dbg._sendRequestAllowDiverge( + const properties = this._dbg._sendRequestAllowDiverge( { type: "getObjectProperties", id }, [] ); @@ -1294,19 +1279,6 @@ ReplayDebuggerObject.prototype = { }); }, - replayHasCallResult(name) { - return ( - this._preview && - this._preview.callResults && - this._preview.callResults.has(name) - ); - }, - - replayCallResult(name) { - const value = this._preview.callResults.get(name); - return this._pool.convertValue(value); - }, - unwrap() { if (!this.isProxy) { return this; @@ -1348,10 +1320,7 @@ ReplayDebuggerObject.prototype = { }, apply(thisv, args) { - if (this._pool != this._dbg._pool) { - return undefined; - } - + assert(this._pool == this._dbg._pool); thisv = this._dbg._convertValueForChild(thisv); args = (args || []).map(v => this._dbg._convertValueForChild(v)); @@ -1393,20 +1362,25 @@ ReplayDebuggerObject.prototype = { return this._data.typedArrayLength; }, - makeDebuggeeValue(obj) { - if (obj instanceof ReplayDebuggerObject) { - return obj; - } - const rv = ReplayInspector.unwrapObject(obj); - if (rv) { - return rv; - } - ThrowError("Can't make debuggee value"); - return null; // For eslint + getContainerSize() { + return this._data.containerSize; }, - replayIsInstance(name) { - return this._data.isInstance == name; + getRegExpString() { + return this._data.regExpString; + }, + + getDateTime() { + return this._data.dateTime; + }, + + getErrorProperties() { + return this._data.errorProperties; + }, + + makeDebuggeeValue(obj) { + assert(obj instanceof ReplayDebuggerObject); + return obj; }, preventExtensions: NotAllowed, diff --git a/devtools/server/actors/replay/inspector.js b/devtools/server/actors/replay/inspector.js index eb1d41a3f59e..d0eb395d6585 100644 --- a/devtools/server/actors/replay/inspector.js +++ b/devtools/server/actors/replay/inspector.js @@ -76,6 +76,16 @@ const ReplayInspector = { ); }, + // Create the CSSRule object to bind for other server users. + createCSSRule(rule) { + return { + ...rule, + isInstance(node) { + return gFixedProxy.CSSRule.isInstance(node); + }, + }; + }, + wrapRequireHook(requireHook) { return (id, require) => { const rv = requireHook(id, require); @@ -99,31 +109,8 @@ const ReplayInspector = { getDebuggerObject(node) { return unwrapValue(node); }, - - // For use by ReplayDebugger. - wrapObject, - unwrapObject(obj) { - return proxyMap.get(obj); - }, }; -// Objects we need to override isInstance for. -const gOverrideIsInstance = ["CSSRule", "Event"]; - -for (const name of gOverrideIsInstance) { - ReplayInspector[`create${name}`] = original => ({ - ...original, - isInstance(obj) { - const unwrapped = proxyMap.get(obj); - if (!unwrapped) { - return original.isInstance(obj); - } - assert(unwrapped instanceof ReplayDebugger.Object); - return unwrapped.replayIsInstance(name); - }, - }); -} - /////////////////////////////////////////////////////////////////////////////// // Require Substitutions /////////////////////////////////////////////////////////////////////////////// @@ -297,7 +284,6 @@ function unwrapValue(value) { } function getObjectProperty(obj, name) { - assert(obj._pool == dbg()._pool); const rv = dbg()._sendRequestAllowDiverge({ type: "getObjectPropertyValue", id: obj._data.id, @@ -307,7 +293,6 @@ function getObjectProperty(obj, name) { } function setObjectProperty(obj, name, value) { - assert(obj._pool == dbg()._pool); const rv = dbg()._sendRequestAllowDiverge({ type: "setObjectPropertyValue", id: obj._data.id, diff --git a/devtools/server/actors/replay/replay.js b/devtools/server/actors/replay/replay.js index efaa89c707db..e89a6abd7087 100644 --- a/devtools/server/actors/replay/replay.js +++ b/devtools/server/actors/replay/replay.js @@ -359,12 +359,12 @@ Services.obs.addObserver( // Message arguments are preserved as debuggee values. if (apiMessage.arguments) { contents.arguments = apiMessage.arguments.map(v => { - return makeConvertedDebuggeeValue(v); + return convertValue(makeDebuggeeValue(v)); }); contents.argumentsData = new PreviewedObjects(); contents.arguments.forEach(v => - contents.argumentsData.addValue(v, PropertyLevels.FULL) + contents.argumentsData.addValue(v, true) ); ClearPausedState(); @@ -549,35 +549,36 @@ function findAllScriptHits(script, frameIndex, offsets, startpoint, endpoint) { return allHits; } -function findChangeFrames(checkpoint, which, kind) { +function findChangeFrames(checkpoint, which, kind, frameIndex, maybeScript) { const hits = RecordReplayControl.findChangeFrames(checkpoint, which); - return hits.map(({ script, progress, frameIndex }) => ({ - checkpoint, - progress, - position: { kind, script, frameIndex }, - })); + return hits + .filter( + hit => + hit.frameIndex == frameIndex && + (!maybeScript || hit.script == maybeScript) + ) + .map(({ script, progress }) => ({ + checkpoint, + progress, + position: { kind, script, frameIndex }, + })); } function findFrameSteps({ targetPoint, breakpointOffsets }) { const { checkpoint, - position: { script: targetScript, frameIndex: targetIndex }, + position: { script, frameIndex: targetIndex }, } = targetPoint; - const potentialStepsFilter = point => { - const { frameIndex, script } = point.position; - return frameIndex == targetIndex && script == targetScript; - }; - // Find the entry point of the frame whose steps contain |targetPoint|. let entryPoint; if (targetPoint.position.kind == "EnterFrame") { entryPoint = targetPoint; } else { const entryHits = [ - ...findChangeFrames(checkpoint, 0, "EnterFrame"), - ...findChangeFrames(checkpoint, 2, "EnterFrame"), - ].filter(potentialStepsFilter); + ...findChangeFrames(checkpoint, 0, "EnterFrame", targetIndex, script), + ...findChangeFrames(checkpoint, 2, "EnterFrame", targetIndex, script), + ]; // Find the last frame entry or resume for the frame's script preceding the // target point. Since frames do not span checkpoints the hit must be in the @@ -592,8 +593,12 @@ function findFrameSteps({ targetPoint, breakpointOffsets }) { } // Find the exit point of the frame. - const exitHits = findChangeFrames(checkpoint, 1, "OnPop").filter( - potentialStepsFilter + const exitHits = findChangeFrames( + checkpoint, + 1, + "OnPop", + targetIndex, + script ); const exitPoint = findClosestPoint( exitHits, @@ -606,14 +611,17 @@ function findFrameSteps({ targetPoint, breakpointOffsets }) { // frame index and happen between the entry and exit points. Any EnterFrame // points for immediate callees of the frame are also included. const breakpointHits = findAllScriptHits( - targetScript, + script, targetIndex, breakpointOffsets, checkpoint, checkpoint + 1 ); - const enterFrameHits = findChangeFrames(checkpoint, 0, "EnterFrame").filter( - point => point.position.frameIndex == targetIndex + 1 + const enterFrameHits = findChangeFrames( + checkpoint, + 0, + "EnterFrame", + targetIndex + 1 ); const steps = breakpointHits.concat(enterFrameHits).filter(point => { return pointPrecedes(entryPoint, point) && pointPrecedes(point, exitPoint); @@ -628,9 +636,13 @@ function findFrameSteps({ targetPoint, breakpointOffsets }) { } function findEventFrameEntry({ checkpoint, progress }) { - return findChangeFrames(checkpoint, 0, "EnterFrame").filter(point => { - return point.progress == progress + 1; - })[0]; + const entryHits = findChangeFrames(checkpoint, 0, "EnterFrame", 0); + for (const hit of entryHits) { + if (hit.progress == progress + 1) { + return hit; + } + } + return null; } /////////////////////////////////////////////////////////////////////////////// @@ -899,10 +911,6 @@ function makeDebuggeeValue(value) { return value; } -function makeConvertedDebuggeeValue(value) { - return convertValue(makeDebuggeeValue(value)); -} - function getDebuggeeValue(value) { if (value && typeof value == "object") { assert(value instanceof Debugger.Object); @@ -1047,10 +1055,10 @@ const gManifestStartHandlers = { } else { result = [getDebuggeeValue(rv.throw)]; } - result = result.map(v => makeConvertedDebuggeeValue(v)); + result = result.map(v => convertValue(makeDebuggeeValue(v))); const resultData = new PreviewedObjects(); - result.forEach(v => resultData.addValue(v, PropertyLevels.FULL)); + result.forEach(v => resultData.addValue(v, true)); RecordReplayControl.manifestFinished({ result, resultData, pauseData }); }, @@ -1331,6 +1339,7 @@ function unknownObjectProperties(why) { ]; } +// eslint-disable-next-line complexity function getObjectData(id) { const object = gPausedObjects.getObject(id); if (object instanceof Debugger.Object) { @@ -1376,10 +1385,55 @@ function getObjectData(id) { if (object.errorColumnNumber) { rv.errorColumnNumber = object.errorColumnNumber; } - if (CSSRule.isInstance(object.unsafeDereference())) { - rv.isInstance = "CSSRule"; - } else if (Event.isInstance(object.unsafeDereference())) { - rv.isInstance = "Event"; + + const raw = object.unsafeDereference(); + switch (object.class) { + case "Uint8Array": + case "Uint8ClampedArray": + case "Uint16Array": + case "Uint32Array": + case "Int8Array": + case "Int16Array": + case "Int32Array": + case "Float32Array": + case "Float64Array": { + const typedProto = Object.getPrototypeOf(Uint8Array.prototype); + const { get } = Object.getOwnPropertyDescriptor(typedProto, "length"); + rv.typedArrayLength = get.call(raw); + break; + } + case "Set": { + const { get } = Object.getOwnPropertyDescriptor(Set.prototype, "size"); + rv.containerSize = get.call(raw); + break; + } + case "Map": { + const { get } = Object.getOwnPropertyDescriptor(Map.prototype, "size"); + rv.containerSize = get.call(raw); + break; + } + case "RegExp": + rv.regExpString = RegExp.prototype.toString.call(raw); + break; + case "Date": + rv.dateTime = Date.prototype.getTime.call(raw); + break; + case "Error": + case "EvalError": + case "RangeError": + case "ReferenceError": + case "SyntaxError": + case "TypeError": + case "URIError": + rv.errorProperties = { + name: raw.name, + message: raw.message, + stack: raw.stack, + fileName: raw.fileName, + lineNumber: raw.lineNumber, + columnNumber: raw.columnNumber, + }; + break; } return rv; } @@ -1397,21 +1451,7 @@ function getObjectData(id) { throwError(`Unknown object kind: ${object}`); } -// Return whether to avoid operating on an object due to the likelihood of a -// recording divergence or other bad behavior. -function isBlacklisted(object) { - // Enumerate a Storage object's properties requires the content process to - // synchronously communicate with the UI process, which it can't do. - return object.class == "Storage"; -} - function getObjectProperties(object) { - const rv = Object.create(null); - - if (isBlacklisted(object)) { - return rv; - } - let names; try { names = object.getOwnPropertyNames(); @@ -1419,24 +1459,11 @@ function getObjectProperties(object) { return unknownObjectProperties(e.toString()); } + const rv = Object.create(null); names.forEach(name => { - // Workaround this test-only getter not reporting exceptions properly. - if (name == "SpecialPowers_wrappedObject") { - return; - } - let desc; try { desc = object.getOwnPropertyDescriptor(name); - if (!desc) { - desc = { - name, - desc: { - value: `Unexpected missing property ${name}`, - enumerable: true, - }, - }; - } } catch (e) { desc = { name, desc: { value: "Unknown: " + e, enumerable: true } }; } @@ -1459,24 +1486,24 @@ function getObjectContainerContents(object) { switch (object.class) { case "Set": { const iter = Cu.waiveXrays(Set.prototype.values.call(raw)); - return [...iter].map(v => makeConvertedDebuggeeValue(v)); + return [...iter].map(v => convertValue(makeDebuggeeValue(v))); } case "Map": { const iter = Cu.waiveXrays(Map.prototype.entries.call(raw)); return [...iter].map(([k, v]) => [ - makeConvertedDebuggeeValue(k), - makeConvertedDebuggeeValue(v), + convertValue(makeDebuggeeValue(k)), + convertValue(makeDebuggeeValue(v)), ]); } case "WeakSet": { const keys = ChromeUtils.nondeterministicGetWeakSetKeys(raw); - return keys.map(k => makeConvertedDebuggeeValue(Cu.waiveXrays(k))); + return keys.map(k => convertValue(makeDebuggeeValue(Cu.waiveXrays(k)))); } case "WeakMap": { const keys = ChromeUtils.nondeterministicGetWeakMapKeys(raw); return keys.map(k => [ - makeConvertedDebuggeeValue(k), - makeConvertedDebuggeeValue(WeakMap.prototype.get.call(raw, k)), + convertValue(makeDebuggeeValue(k)), + convertValue(makeDebuggeeValue(WeakMap.prototype.get.call(raw, k))), ]); } default: @@ -1513,13 +1540,6 @@ function getWindow() { // object. const OBJECT_PREVIEW_MAX_ITEMS = 10; -// Levels at which property information can be included in previews. -// If not specified, minimal properties are included. -const PropertyLevels = { - BASIC: 1, // Include enough properties to show an inline preview. - FULL: 2, // Include enough properties to allow the object to be expanded. -}; - // 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() { @@ -1528,211 +1548,96 @@ function PreviewedObjects() { } PreviewedObjects.prototype = { - addValue(value, level) { + addValue(value, includeProperties) { if (value && typeof value == "object" && value.object) { - this.addObject(value.object, level); + this.addObject(value.object, includeProperties); } }, - // eslint-disable-next-line complexity - addObject(id, level) { + 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); - if (!this.objects[id]) { - let ownPropertyNamesCount = 0; - try { - ownPropertyNamesCount = object.getOwnPropertyNames().length; - } catch (e) {} + const properties = getObjectProperties(object); + const propertyEntries = Object.entries(properties); + if (needObject) { this.objects[id] = { data: getObjectData(id), - preview: { ownPropertyNamesCount, level }, + preview: { + ownPropertyNamesCount: propertyEntries.length, + }, }; - } else { + const preview = this.objects[id].preview; - if ((preview.level | 0) >= (level | 0)) { - return; + + // 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; } - preview.level = level; } - const { data, preview } = this.objects[id]; + if (needProperties) { + const preview = this.objects[id].preview; - // If this is a DOM object identified with isInstance, the previewer might - // need additional properties. - if (level == PropertyLevels.BASIC && data.isInstance) { - preview.level = level = PropertyLevels.FULL; - } - - // Add intrinsic properties that are always included. - switch (object.class) { - case "Array": - case "Uint8Array": - case "Uint8ClampedArray": - case "Uint16Array": - case "Uint32Array": - case "Int8Array": - case "Int16Array": - case "Int32Array": - case "Float32Array": - case "Float64Array": - this.addObjectPropertyValue(object, "length"); - break; - case "Function": - this.addObjectPropertyValue(object, "displayName"); - break; - } - - if (!level) { - return; - } - - const properties = Object.entries(getObjectProperties(object)); - - // For an inline 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. When doing a full property enumeration, include all properties. - let enumerablePropertyCount = 0; - for (const [name, desc] of properties) { - if (level == PropertyLevels.FULL || desc.enumerable) { - this.addObjectProperty(object, name, desc); - if (level == PropertyLevels.BASIC) { + // 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. - let containerContents = getObjectContainerContents(object); - if (containerContents) { - if (level == PropertyLevels.BASIC) { - containerContents = containerContents.slice( + // 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 = containerContents; - preview.containerContents.forEach(v => this.addContainerValue(v)); - } - - switch (object.class) { - case "RegExp": - this.addObjectCall(object, "toString"); - break; - case "Date": - this.addObjectCall(object, "getTime"); - break; - case "Set": - case "Map": - this.addObjectPropertyValue(object, "size"); - break; - case "Error": - case "EvalError": - case "RangeError": - case "ReferenceError": - case "SyntaxError": - case "TypeError": - case "URIError": - this.addObjectPropertyValue(object, "name"); - this.addObjectPropertyValue(object, "message"); - this.addObjectPropertyValue(object, "stack"); - this.addObjectPropertyValue(object, "fileName"); - this.addObjectPropertyValue(object, "lineNumber"); - this.addObjectPropertyValue(object, "columnNumber"); - break; - } - - // Search the prototype chain for getter properties and fill in their values - // if we are getting all properties of the object. - if (level == PropertyLevels.FULL) { - let { proto } = object; - while (proto) { - let names = []; - try { - names = proto.getOwnPropertyNames(); - } catch (e) {} - - for (const name of names) { - let desc = null; - try { - desc = proto.getOwnPropertyDescriptor(name); - } catch (e) {} - - if (desc && desc.get) { - this.addObjectPropertyValue(object, name); - } - } - - proto = proto.proto; + preview.containerContents.forEach(v => this.addContainerValue(v)); } } }, - addObjectPropertyValue(object, name) { - try { - const value = makeConvertedDebuggeeValue( - object.unsafeDereference()[name] - ); - - this.addObjectProperty(object, name, { value, enumerable: true }); - } catch (e) {} - }, - - addObjectProperty(object, name, desc) { - const id = gPausedObjects.getId(object); - const preview = this.objects[id].preview; - - if (!preview.properties) { - preview.properties = Object.create(null); - } - if (name in preview.properties) { - return; - } - - this.addPropertyDescriptor(desc); - preview.properties[name] = desc; - }, - - addObjectCall(object, name) { - const id = gPausedObjects.getId(object); - const preview = this.objects[id].preview; - - if (!preview.callResults) { - preview.callResults = Object.create(null); - } - if (name in preview.callResults) { - return; - } - - try { - const value = makeConvertedDebuggeeValue( - object.unsafeDereference()[name]() - ); - - this.addValue(value); - preview.callResults[name] = value; - } catch (e) {} - }, - - addPropertyDescriptor(desc, level) { + addPropertyDescriptor(desc, includeProperties) { if (desc.value) { - this.addValue(desc.value, level); + this.addValue(desc.value, includeProperties); } if (desc.get) { - this.addObject(desc.get, level); + this.addObject(desc.get, includeProperties); } if (desc.set) { - this.addObject(desc.set, level); + this.addObject(desc.set, includeProperties); } }, @@ -1757,7 +1662,7 @@ PreviewedObjects.prototype = { const names = getEnvironmentNames(env); this.environments[id] = { data, names }; - names.forEach(({ value }) => this.addValue(value, PropertyLevels.BASIC)); + names.forEach(({ value }) => this.addValue(value, true)); this.addObject(data.callee); this.addEnvironment(data.parent); @@ -1807,14 +1712,14 @@ function getPauseData() { metadata: script.getOffsetMetadata(dbgFrame.offset), }); addScript(frame.script); - rv.addValue(frame.this, PropertyLevels.BASIC); + rv.addValue(frame.this, true); if (frame.arguments) { for (const arg of frame.arguments) { - rv.addValue(arg, PropertyLevels.BASIC); + rv.addValue(arg, true); } } - rv.addObject(frame.callee, PropertyLevels.NONE); - rv.addEnvironment(frame.environment, PropertyLevels.BASIC); + rv.addObject(frame.callee, false); + rv.addEnvironment(frame.environment, true); } return rv; @@ -1903,7 +1808,7 @@ const gRequestHandlers = { getObjectProperties(request) { divergeFromRecording(); const object = gPausedObjects.getObject(request.id); - return { properties: getObjectProperties(object) }; + return getObjectProperties(object); }, getObjectContainerContents(request) { @@ -1915,11 +1820,6 @@ const gRequestHandlers = { divergeFromRecording(); const obj = gPausedObjects.getObject(request.id); const thisv = convertValueFromParent(request.thisv); - - if (thisv instanceof Debugger.Object && isBlacklisted(thisv)) { - return { return: "Can't call method on blacklisted object" }; - } - const args = request.args.map(v => convertValueFromParent(v)); const rv = obj.apply(thisv, args); return convertCompletionValue(rv); @@ -1993,6 +1893,7 @@ const gRequestHandlers = { document: getObjectId(makeDebuggeeValue(window.document)), Services: getObjectId(makeDebuggeeValue(Services)), InspectorUtils: getObjectId(makeDebuggeeValue(InspectorUtils)), + CSSRule: getObjectId(makeDebuggeeValue(CSSRule)), }; }, @@ -2008,13 +1909,9 @@ const gRequestHandlers = { divergeFromRecording(); const object = gPausedObjects.getObject(request.id); - if (isBlacklisted(object)) { - return { return: "Can't get blacklisted object property" }; - } - try { const rv = object.unsafeDereference()[request.name]; - return { return: makeConvertedDebuggeeValue(rv) }; + return { return: convertValue(makeDebuggeeValue(rv)) }; } catch (e) { return { throw: "" + e }; } diff --git a/devtools/shared/DevToolsUtils.js b/devtools/shared/DevToolsUtils.js index fc5ca5f4300d..828ae53045be 100644 --- a/devtools/shared/DevToolsUtils.js +++ b/devtools/shared/DevToolsUtils.js @@ -887,12 +887,6 @@ errorOnFlag(exports, "wantVerbose"); // where unsafeDereference will return an opaque security wrapper to the // referent. function callPropertyOnObject(object, name, ...args) { - // When replaying, the result of the call may already be known, which avoids - // having to communicate with the replaying process. - if (isReplaying && args.length == 0 && object.replayHasCallResult(name)) { - return object.replayCallResult(name); - } - // Find the property. let descriptor; let proto = object; diff --git a/devtools/shared/builtin-modules.js b/devtools/shared/builtin-modules.js index 66e73d19aab2..85ff6a370321 100644 --- a/devtools/shared/builtin-modules.js +++ b/devtools/shared/builtin-modules.js @@ -340,6 +340,7 @@ exports.globals = { NodeFilter, DOMRect, Element, + Event, FileReader, FormData, isWorker: false, @@ -401,19 +402,10 @@ lazyGlobal("indexedDB", () => { lazyGlobal("isReplaying", () => { return exports.modules.Debugger.recordReplayProcessKind() == "Middleman"; }); - -// Globals which the ReplayInspector provides an alternate implementation for. -const inspectorGlobals = { - CSSRule, - Event, -}; - -for (const [name, value] of Object.entries(inspectorGlobals)) { - lazyGlobal(name, () => { - if (exports.modules.Debugger.recordReplayProcessKind() == "Middleman") { - const ReplayInspector = require("devtools/server/actors/replay/inspector"); - return ReplayInspector[`create${name}`](value); - } - return value; - }); -} +lazyGlobal("CSSRule", () => { + if (exports.modules.Debugger.recordReplayProcessKind() == "Middleman") { + const ReplayInspector = require("devtools/server/actors/replay/inspector"); + return ReplayInspector.createCSSRule(CSSRule); + } + return CSSRule; +});