Bug 1544170 Part 2 - ReplayInspector changes to support showing styles, r=loganfsmyth.
authorBrian Hackett <bhackett1024@gmail.com>
Fri, 12 Apr 2019 16:35:18 -1000
changeset 470288 c04849933658c636fa3b3f3a3521f94141c36b28
parent 470287 66ff290649f725c50c4b1815ab2294cbe1fc2f00
child 470289 5e19fc2158c1875050dd8b79062ade3d2476110d
push id35892
push userrgurzau@mozilla.com
push dateSat, 20 Apr 2019 09:55:32 +0000
treeherdermozilla-central@a092972b53f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersloganfsmyth
bugs1544170
milestone68.0a1
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
Bug 1544170 Part 2 - ReplayInspector changes to support showing styles, r=loganfsmyth. Differential Revision: https://phabricator.services.mozilla.com/D27409
devtools/server/actors/replay/debugger.js
devtools/server/actors/replay/inspector.js
devtools/server/actors/replay/replay.js
devtools/shared/builtin-modules.js
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -934,19 +934,16 @@ ReplayDebuggerObject.prototype = {
   get isGeneratorFunction() { return this._data.isGeneratorFunction; },
   get isAsyncFunction() { return this._data.isAsyncFunction; },
   get class() { return this._data.class; },
   get name() { return this._data.name; },
   get displayName() { return this._data.displayName; },
   get parameterNames() { return this._data.parameterNames; },
   get script() { return this._dbg._getScript(this._data.script); },
   get environment() { return this._dbg._getObject(this._data.environment); },
-  get boundTargetFunction() { return this.isBoundFunction ? NYI() : undefined; },
-  get boundThis() { return this.isBoundFunction ? NYI() : undefined; },
-  get boundArguments() { return this.isBoundFunction ? NYI() : undefined; },
   get isProxy() { return this._data.isProxy; },
   get proto() { return this._dbg._getObject(this._data.proto); },
 
   isExtensible() { return this._data.isExtensible; },
   isSealed() { return this._data.isSealed; },
   isFrozen() { return this._data.isFrozen; },
 
   unsafeDereference() {
@@ -1020,16 +1017,37 @@ ReplayDebuggerObject.prototype = {
     return this._dbg._convertValue(this._proxyData.target);
   },
 
   get proxyHandler() {
     this._ensureProxyData();
     return this._dbg._convertValue(this._proxyData.handler);
   },
 
+  get boundTargetFunction() {
+    if (this.isBoundFunction) {
+      return this._dbg._getObject(this._data.boundTargetFunction);
+    }
+    return undefined;
+  },
+
+  get boundThis() {
+    if (this.isBoundFunction) {
+      return this._dbg._convertValue(this._data.boundThis);
+    }
+    return undefined;
+  },
+
+  get boundArguments() {
+    if (this.isBoundFunction) {
+      return this._dbg._getObject(this._data.boundArguments);
+    }
+    return undefined;
+  },
+
   call(thisv, ...args) {
     return this.apply(thisv, args);
   },
 
   apply(thisv, args) {
     thisv = this._dbg._convertValueForChild(thisv);
     args = (args || []).map(v => this._dbg._convertValueForChild(v));
 
--- a/devtools/server/actors/replay/inspector.js
+++ b/devtools/server/actors/replay/inspector.js
@@ -34,28 +34,54 @@ function dbg() {
 
 ///////////////////////////////////////////////////////////////////////////////
 // Public Interface
 ///////////////////////////////////////////////////////////////////////////////
 
 const ReplayInspector = {
   // Return a proxy for the window in the replaying process.
   get window() {
-    return gWindow;
+    if (!gFixedProxy.window) {
+      updateFixedProxies();
+    }
+    return gFixedProxy.window;
   },
 
   // Create the InspectorUtils object to bind for other server users.
   createInspectorUtils(utils) {
-    // Overwrite some APIs that will fail if called on proxies from the
-    // replaying process.
+    return new Proxy({}, {
+      get(_, name) {
+        switch (name) {
+        case "getAllStyleSheets":
+        case "getCSSStyleRules":
+        case "getRuleLine":
+        case "getRuleColumn":
+        case "getRelativeRuleLine":
+        case "getSelectorCount":
+        case "getSelectorText":
+        case "selectorMatchesElement":
+        case "hasRulesModifiedByCSSOM":
+        case "getSpecificity":
+          return gFixedProxy.InspectorUtils[name];
+        case "hasPseudoClassLock":
+          return () => false;
+        default:
+          return utils[name];
+        }
+      },
+    });
+  },
+
+  // Create the CSSRule object to bind for other server users.
+  createCSSRule(rule) {
     return {
-      ...utils,
-      hasPseudoClassLock() { return false; },
-      getAllStyleSheets() { return []; },
-      getCSSStyleRules() { return []; },
+      ...rule,
+      isInstance(node) {
+        return gFixedProxy.CSSRule.isInstance(node);
+      },
     };
   },
 
   wrapRequireHook(requireHook) {
     return (id, require) => {
       const rv = requireHook(id, require);
       return substituteRequire(id, rv);
     };
@@ -133,23 +159,17 @@ function createSubstituteChrome(chrome) 
     }),
   };
 }
 
 function createSubstituteServices(Services) {
   return newSubstituteProxy(Services, {
     els: {
       getListenerInfoFor(node) {
-        const id = unwrapValue(node)._data.id;
-        const rv = dbg()._sendRequestAllowDiverge({
-          type: "getListenerInfoFor",
-          id,
-        });
-        const obj = dbg()._getObject(rv.id);
-        return wrapValue(obj);
+        return gFixedProxy.Services.els.getListenerInfoFor(node);
       },
     },
   });
 }
 
 function createSubstitute(id, rv) {
   switch (id) {
   case "chrome": return createSubstituteChrome(rv);
@@ -360,17 +380,17 @@ const ReplayInspectorProxyHandler = {
     ThrowError(rv.throw);
   },
 
   construct(target, args) {
     target = getTargetObject(target);
     const proxy = wrapObject(target);
 
     // Create fake MutationObservers to satisfy callers in the inspector.
-    if (proxy == gWindow.MutationObserver) {
+    if (proxy == gFixedProxy.window.MutationObserver) {
       return {
         observe: () => {},
         disconnect: () => {},
       };
     }
 
     NotAllowed();
   },
@@ -412,36 +432,36 @@ const ReplayInspectorProxyHandler = {
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 // Fixed Proxies
 ///////////////////////////////////////////////////////////////////////////////
 
 // Proxies for the window and root document are reused to ensure consistent
 // actors are used for these objects.
-const gWindowTarget = { object: {} }, gDocumentTarget = { object: {} };
-const gWindow = new Proxy(gWindowTarget, ReplayInspectorProxyHandler);
-const gDocument = new Proxy(gDocumentTarget, ReplayInspectorProxyHandler);
+const gFixedProxyTargets = {};
+const gFixedProxy = {};
 
 function initFixedProxy(proxy, target, obj) {
   target.object = obj;
   proxyMap.set(proxy, obj);
   obj._inspectorObject = proxy;
 }
 
 function updateFixedProxies() {
   dbg()._ensurePaused();
 
-  const data = dbg()._sendRequestAllowDiverge({ type: "getWindow" });
-  const dbgWindow = dbg()._getObject(data.id);
-  initFixedProxy(gWindow, gWindowTarget, dbgWindow);
-
-  const rv = getObjectProperty(dbgWindow, "document");
-  assert(rv.return instanceof ReplayDebugger.Object);
-  initFixedProxy(gDocument, gDocumentTarget, rv.return);
+  const data = dbg()._sendRequestAllowDiverge({ type: "getFixedObjects" });
+  for (const [key, value] of Object.entries(data)) {
+    if (!gFixedProxyTargets[key]) {
+      gFixedProxyTargets[key] = { object: {} };
+      gFixedProxy[key] = new Proxy(gFixedProxyTargets[key], ReplayInspectorProxyHandler);
+    }
+    initFixedProxy(gFixedProxy[key], gFixedProxyTargets[key], dbg()._getObject(value));
+  }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Utilities
 ///////////////////////////////////////////////////////////////////////////////
 
 function NYI() {
   ThrowError("Not yet implemented");
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -21,28 +21,38 @@
 // any point where such interactions might occur.
 // eslint-disable spaced-comment
 
 "use strict";
 
 const CC = Components.Constructor;
 
 // Create a sandbox with the resources we need. require() doesn't work here.
-const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
+const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")(), {
+  wantGlobalProperties: [
+    "InspectorUtils",
+    "CSSRule",
+  ],
+});
 Cu.evalInSandbox(
   "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
   "Components.utils.import('resource://gre/modules/Services.jsm');" +
   "addDebuggerToGlobal(this);",
   sandbox
 );
-const Debugger = sandbox.Debugger;
-const RecordReplayControl = sandbox.RecordReplayControl;
-const Services = sandbox.Services;
+const {
+  Debugger,
+  RecordReplayControl,
+  Services,
+  InspectorUtils,
+  CSSRule,
+} = sandbox;
 
 const dbg = new Debugger();
+const firstGlobal = dbg.makeGlobalObjectReference(sandbox);
 
 // We are interested in debugging all globals in the process.
 dbg.onNewGlobalObject = function(global) {
   try {
     dbg.addDebuggee(global);
   } catch (e) {
     // Ignore errors related to adding a same-compartment debuggee.
     // See bug 1523755.
@@ -542,19 +552,26 @@ function convertValueFromParent(value) {
     }
   }
   return value;
 }
 
 function makeDebuggeeValue(value) {
   if (isNonNullObject(value)) {
     assert(!(value instanceof Debugger.Object));
-    const global = Cu.getGlobalForObject(value);
-    const dbgGlobal = dbg.makeGlobalObjectReference(global);
-    return dbgGlobal.makeDebuggeeValue(value);
+    try {
+      const global = Cu.getGlobalForObject(value);
+      const dbgGlobal = dbg.makeGlobalObjectReference(global);
+      return dbgGlobal.makeDebuggeeValue(value);
+    } catch (e) {
+      // Sometimes the global which Cu.getGlobalForObject finds has
+      // isInvisibleToDebugger set. Wrap the object into the first global we
+      // found in this case.
+      return firstGlobal.makeDebuggeeValue(value);
+    }
   }
   return value;
 }
 
 function getDebuggeeValue(value) {
   if (value && typeof value == "object") {
     assert(value instanceof Debugger.Object);
     return value.unsafeDereference();
@@ -713,17 +730,17 @@ const gRequestHandlers = {
 
   getSource(request) {
     return getSourceData(request.id);
   },
 
   getObject(request) {
     const object = gPausedObjects.getObject(request.id);
     if (object instanceof Debugger.Object) {
-      return {
+      const rv = {
         id: request.id,
         kind: "Object",
         callable: object.callable,
         isBoundFunction: object.isBoundFunction,
         isArrowFunction: object.isArrowFunction,
         isGeneratorFunction: object.isGeneratorFunction,
         isAsyncFunction: object.isAsyncFunction,
         proto: getObjectId(object.proto),
@@ -733,16 +750,22 @@ const gRequestHandlers = {
         parameterNames: object.parameterNames,
         script: gScripts.getId(object.script),
         environment: getObjectId(object.environment),
         isProxy: object.isProxy,
         isExtensible: object.isExtensible(),
         isSealed: object.isSealed(),
         isFrozen: object.isFrozen(),
       };
+      if (rv.isBoundFunction) {
+        rv.boundTargetFunction = getObjectId(object.boundTargetFunction);
+        rv.boundThis = convertValue(object.boundThis);
+        rv.boundArguments = getObjectId(makeDebuggeeValue(object.boundArguments));
+      }
+      return rv;
     }
     if (object instanceof Debugger.Environment) {
       return {
         id: request.id,
         kind: "Environment",
         type: object.type,
         parent: getObjectId(object.parent),
         object: object.type == "declarative" ? 0 : getObjectId(object.object),
@@ -876,23 +899,29 @@ const gRequestHandlers = {
   recordingEndpoint(request) {
     return RecordReplayControl.recordingEndpoint();
   },
 
   /////////////////////////////////////////////////////////
   // Inspector Requests
   /////////////////////////////////////////////////////////
 
-  getWindow(request) {
+  getFixedObjects(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in getWindow" };
     }
 
-    // Hopefully there is exactly one window in this enumerator.
-    return { id: getObjectId(makeDebuggeeValue(getWindow())) };
+    const window = getWindow();
+    return {
+      window: getObjectId(makeDebuggeeValue(window)),
+      document: getObjectId(makeDebuggeeValue(window.document)),
+      Services: getObjectId(makeDebuggeeValue(Services)),
+      InspectorUtils: getObjectId(makeDebuggeeValue(InspectorUtils)),
+      CSSRule: getObjectId(makeDebuggeeValue(CSSRule)),
+    };
   },
 
   newDeepTreeWalker(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in newDeepTreeWalker" };
     }
 
     const walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
@@ -941,26 +970,16 @@ const gRequestHandlers = {
     const element =
       getWindow().document.elementFromPoint(request.clientX, request.clientY);
     if (!element) {
       return { id: 0 };
     }
     const obj = makeDebuggeeValue(element);
     return { id: getObjectId(obj) };
   },
-
-  getListenerInfoFor(request) {
-    if (!RecordReplayControl.maybeDivergeFromRecording()) {
-      return { throw: "Recording divergence in getListenerInfoFor" };
-    }
-
-    const node = gPausedObjects.getObject(request.id).unsafeDereference();
-    const obj = makeDebuggeeValue(Services.els.getListenerInfoFor(node) || []);
-    return { id: getObjectId(obj) };
-  },
 };
 
 // eslint-disable-next-line no-unused-vars
 function ProcessRequest(request) {
   try {
     if (gRequestHandlers[request.type]) {
       return gRequestHandlers[request.type](request);
     }
--- a/devtools/shared/builtin-modules.js
+++ b/devtools/shared/builtin-modules.js
@@ -292,17 +292,16 @@ defineLazyGetter(exports.modules, "xpcIn
 // List of all custom globals exposed to devtools modules.
 // Changes here should be mirrored to devtools/.eslintrc.
 exports.globals = {
   atob,
   Blob,
   btoa,
   console,
   CSS,
-  CSSRule,
   // Make sure `define` function exists.  This allows defining some modules
   // in AMD format while retaining CommonJS compatibility through this hook.
   // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
   // from a content document and can't access our usual loaders.  So, any
   // modules shared with the JSON Viewer should include a define wrapper:
   //
   //   // Make this available to both AMD and CJS environments
   //   define(function(require, exports, module) {
@@ -376,8 +375,15 @@ lazyGlobal("WebSocket", () => {
   return Services.appShell.hiddenDOMWindow.WebSocket;
 });
 lazyGlobal("indexedDB", () => {
   return require("devtools/shared/indexed-db").createDevToolsIndexedDB(indexedDB);
 });
 lazyGlobal("isReplaying", () => {
   return exports.modules.Debugger.recordReplayProcessKind() == "Middleman";
 });
+lazyGlobal("CSSRule", () => {
+  if (exports.modules.Debugger.recordReplayProcessKind() == "Middleman") {
+    const ReplayInspector = require("devtools/server/actors/replay/inspector");
+    return ReplayInspector.createCSSRule(CSSRule);
+  }
+  return CSSRule;
+});