Bug 1098391 - Best effort to load debugger server dependencies lazily. r=jryans
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 22 Dec 2014 10:48:00 -0500
changeset 248538 0f6c7aaaee350eeee7599473d091cd2be3a7d96d
parent 248537 66ec04bc10eb1c7c8bdce5fdd4049056882e288b
child 248539 d68aaa3f8e5c4586e4f126e65a551e1237563600
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1098391
milestone37.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 1098391 - Best effort to load debugger server dependencies lazily. r=jryans
toolkit/devtools/DevToolsUtils.js
toolkit/devtools/Loader.jsm
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/actors/script.js
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/main.js
toolkit/devtools/worker-loader.js
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -4,17 +4,16 @@
 
 "use strict";
 
 /* General utilities used throughout devtools. */
 
 var { Ci, Cu, Cc, components } = require("chrome");
 var Services = require("Services");
 var promise = require("promise");
-var { setTimeout } = require("Timer");
 
 /**
  * Turn the error |aError| into a string, without fail.
  */
 exports.safeErrorString = function safeErrorString(aError) {
   try {
     let errorString = aError.toString();
     if (typeof errorString == "string") {
@@ -116,17 +115,17 @@ exports.zip = function zip(a, b) {
   return pairs;
 };
 
 /**
  * Waits for the next tick in the event loop to execute a callback.
  */
 exports.executeSoon = function executeSoon(aFn) {
   if (isWorker) {
-    setTimeout(aFn, 0);
+    require("Timer").setTimeout(aFn, 0);
   } else {
     Services.tm.mainThread.dispatch({
       run: exports.makeInfallible(aFn)
     }, Ci.nsIThread.DISPATCH_NORMAL);
   }
 };
 
 /**
@@ -146,17 +145,17 @@ exports.waitForTick = function waitForTi
  *
  * @param number aDelay
  *        The amount of time to wait, in milliseconds.
  * @return Promise
  *         A promise that is resolved after the specified amount of time passes.
  */
 exports.waitForTime = function waitForTime(aDelay) {
   let deferred = promise.defer();
-  setTimeout(deferred.resolve, aDelay);
+  require("Timer").setTimeout(deferred.resolve, aDelay);
   return deferred.promise;
 };
 
 /**
  * Like Array.prototype.forEach, but doesn't cause jankiness when iterating over
  * very large arrays by yielding to the browser and continuing execution on the
  * next tick.
  *
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -5,63 +5,70 @@
 "use strict";
 
 /**
  * Manages the addon-sdk loader instance used to load the developer tools.
  */
 
 let { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
-// addDebuggerToGlobal only allows adding the Debugger object to a global. The
-// this object is not guaranteed to be a global (in particular on B2G, due to
-// compartment sharing), so add the Debugger object to a sandbox instead.
-let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')());
-Cu.evalInSandbox(
-  "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
-  "addDebuggerToGlobal(this);",
-  sandbox
-);
-let Debugger = sandbox.Debugger;
-
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
-
-let xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
 
 let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader;
 let promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
 this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider",
                          "SrcdirProvider"];
 
 /**
  * Providers are different strategies for loading the devtools.
  */
 
-let Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
-
 let loaderModules = {
-  "Debugger": Debugger,
   "Services": Object.create(Services),
-  "Timer": Object.create(Timer),
   "toolkit/loader": loader,
-  "xpcInspector": xpcInspector,
   "promise": promise,
   "PromiseDebugging": PromiseDebugging
 };
-try {
-  let { indexedDB } = Cu.Sandbox(this, {wantGlobalProperties:["indexedDB"]});
-  loaderModules.indexedDB = indexedDB;
-} catch(e) {
+XPCOMUtils.defineLazyGetter(loaderModules, "Debugger", () => {
+  // addDebuggerToGlobal only allows adding the Debugger object to a global. The
+  // this object is not guaranteed to be a global (in particular on B2G, due to
+  // compartment sharing), so add the Debugger object to a sandbox instead.
+  let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')());
+  Cu.evalInSandbox(
+    "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
+    "addDebuggerToGlobal(this);",
+    sandbox
+  );
+  return sandbox.Debugger;
+});
+XPCOMUtils.defineLazyGetter(loaderModules, "Timer", () => {
+  let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
+  // Do not return Cu.import result, as SDK loader would freeze Timer.jsm globals...
+  return {
+    setTimeout,
+    clearTimeout
+  };
+});
+XPCOMUtils.defineLazyGetter(loaderModules, "xpcInspector", () => {
+  return Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
+});
+XPCOMUtils.defineLazyGetter(loaderModules, "indexedDB", () => {
   // On xpcshell, we can't instantiate indexedDB without crashing
-}
+  try {
+    return Cu.Sandbox(this, {wantGlobalProperties:["indexedDB"]}).indexedDB;
+  } catch(e) {
+    return {};
+  }
+});
 
 let sharedGlobalBlacklist = ["sdk/indexed-db"];
 
 // Used when the tools should be loaded from the Firefox package itself (the default)
 function BuiltinProvider() {}
 BuiltinProvider.prototype = {
   load: function() {
     this.loader = new loader.Loader({
@@ -351,25 +358,28 @@ DevToolsLoader.prototype = {
     this._provider = provider;
 
     // Pass through internal loader settings specific to this loader instance
     this._provider.invisibleToDebugger = this.invisibleToDebugger;
     this._provider.globals = {
       isWorker: false,
       reportError: Cu.reportError,
       btoa: btoa,
-      console: console,
       _Iterator: Iterator,
       loader: {
         lazyGetter: this.lazyGetter,
         lazyImporter: this.lazyImporter,
         lazyServiceGetter: this.lazyServiceGetter,
         lazyRequireGetter: this.lazyRequireGetter
       },
     };
+    // Lazy define console in order to load Console.jsm only when it is used
+    XPCOMUtils.defineLazyGetter(this._provider.globals, "console", () => {
+      return Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console;
+    });
 
     this._provider.load();
     this.require = loader.Require(this._provider.loader, { id: "devtools" });
 
     if (this._mainid) {
       this.main(this._mainid);
     }
   },
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -5,25 +5,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Cc, Ci, Cu } = require("chrome");
 const Services = require("Services");
 const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common");
 const { DebuggerServer } = require("devtools/server/main");
-const { dumpProtocolSpec } = require("devtools/server/protocol");
 const makeDebugger = require("./utils/make-debugger");
-const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 
-DevToolsUtils.defineLazyGetter(this, "StyleSheetActor", () => {
-  return require("devtools/server/actors/stylesheets").StyleSheetActor;
-});
+loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true);
 
-DevToolsUtils.defineLazyGetter(this, "ppmm", () => {
+loader.lazyGetter(this, "ppmm", () => {
   return Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
 });
 
 /* Root actor for the remote debugging protocol. */
 
 /**
  * Create a remote debugging protocol root actor.
  *
@@ -397,17 +393,19 @@ RootActor.prototype = {
   onEcho: function (aRequest) {
     /*
      * Request packets are frozen. Copy aRequest, so that
      * DebuggerServerConnection.onPacket can attach a 'from' property.
      */
     return Cu.cloneInto(aRequest, {});
   },
 
-  onProtocolDescription: dumpProtocolSpec,
+  onProtocolDescription: function () {
+    return require("devtools/server/protocol").dumpProtocolSpec();
+  },
 
   /* Support for DebuggerServer.addGlobalActor. */
   _createExtraActors: createExtraActors,
   _appendExtraActors: appendExtraActors,
 
   /* ThreadActor hooks. */
 
   /**
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -8,26 +8,32 @@
 
 const Services = require("Services");
 const { Cc, Ci, Cu, components, ChromeWorker } = require("chrome");
 const { ActorPool, getOffsetColumn } = require("devtools/server/actors/common");
 const { DebuggerServer } = require("devtools/server/main");
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const { dbg_assert, dumpn, update, fetch } = DevToolsUtils;
 const { dirname, joinURI } = require("devtools/toolkit/path");
-const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
 const promise = require("promise");
 const PromiseDebugging = require("PromiseDebugging");
-const Debugger = require("Debugger");
 const xpcInspector = require("xpcInspector");
 const mapURIToAddonID = require("./utils/map-uri-to-addon-id");
 const ScriptStore = require("./utils/ScriptStore");
 
 const { defer, resolve, reject, all } = require("devtools/toolkit/deprecated-sync-thenables");
-const { CssLogic } = require("devtools/styleinspector/css-logic");
+
+loader.lazyGetter(this, "Debugger", () => {
+  let Debugger = require("Debugger");
+  hackDebugger(Debugger);
+  return Debugger;
+});
+loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
+loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
+loader.lazyRequireGetter(this, "CssLogic", "devtools/styleinspector/css-logic", true);
 
 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
       "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
       "Float64Array"];
 
 // Number of items to preview in objects, arrays, maps, sets, lists,
 // collections, etc.
 let OBJECT_PREVIEW_MAX_ITEMS = 10;
@@ -39,28 +45,28 @@ let OBJECT_PREVIEW_MAX_ITEMS = 10;
  * See dom/webidl/PromiseDebugging.webidl
  *
  * @returns Object
  *          An object of one of the following forms:
  *          - { state: "pending" }
  *          - { state: "fulfilled", value }
  *          - { state: "rejected", reason }
  */
-Debugger.Object.prototype.getPromiseState = function () {
-  if (this.class != "Promise") {
+function getPromiseState(obj) {
+  if (obj.class != "Promise") {
     throw new Error(
       "Can't call `getPromiseState` on `Debugger.Object`s that don't " +
       "refer to Promise objects.");
   }
 
-  const state = PromiseDebugging.getState(this.unsafeDereference());
+  const state = PromiseDebugging.getState(obj.unsafeDereference());
   return {
     state: state.state,
-    value: this.makeDebuggeeValue(state.value),
-    reason: this.makeDebuggeeValue(state.reason)
+    value: obj.makeDebuggeeValue(state.value),
+    reason: obj.makeDebuggeeValue(state.reason)
   };
 };
 
 /**
  * A BreakpointActorMap is a map from locations to instances of BreakpointActor.
  */
 function BreakpointActorMap() {
   this._size = 0;
@@ -3240,17 +3246,17 @@ let stringifiers = {
     const code = DevToolsUtils.getProperty(obj, "code");
     const name = DevToolsUtils.getProperty(obj, "name") || "<unknown>";
 
     return '[Exception... "' + message + '" ' +
            'code: "' + code +'" ' +
            'nsresult: "0x' + result + ' (' + name + ')"]';
   },
   Promise: obj => {
-    const { state, value, reason } = obj.getPromiseState();
+    const { state, value, reason } = getPromiseState(obj);
     let statePreview = state;
     if (state != "pending") {
       const settledValue = state === "fulfilled" ? value : reason;
       statePreview += ": " + (typeof settledValue === "object" && settledValue !== null
                                 ? stringify(settledValue)
                                 : settledValue);
     }
     return "Promise (" + statePreview + ")";
@@ -3288,17 +3294,17 @@ ObjectActor.prototype = {
       "extensible": this.obj.isExtensible(),
       "frozen": this.obj.isFrozen(),
       "sealed": this.obj.isSealed()
     };
 
     if (this.obj.class != "DeadObject") {
       // Expose internal Promise state.
       if (this.obj.class == "Promise") {
-        const { state, value, reason } = this.obj.getPromiseState();
+        const { state, value, reason } = getPromiseState(this.obj);
         g.promiseState = { state };
         if (state == "fulfilled") {
           g.promiseState.value = this.threadActor.createValueGrip(value);
         } else if (state == "rejected") {
           g.promiseState.reason = this.threadActor.createValueGrip(reason);
         }
       }
 
@@ -4974,54 +4980,64 @@ EnvironmentActor.prototype = {
 
 EnvironmentActor.prototype.requestTypes = {
   "assign": EnvironmentActor.prototype.onAssign,
   "bindings": EnvironmentActor.prototype.onBindings
 };
 
 exports.EnvironmentActor = EnvironmentActor;
 
-/**
- * Override the toString method in order to get more meaningful script output
- * for debugging the debugger.
- */
-Debugger.Script.prototype.toString = function() {
-  let output = "";
-  if (this.url) {
-    output += this.url;
-  }
-  if (typeof this.staticLevel != "undefined") {
-    output += ":L" + this.staticLevel;
-  }
-  if (typeof this.startLine != "undefined") {
-    output += ":" + this.startLine;
-    if (this.lineCount && this.lineCount > 1) {
-      output += "-" + (this.startLine + this.lineCount - 1);
-    }
-  }
-  if (this.strictMode) {
-    output += ":strict";
-  }
-  return output;
-};
-
-/**
- * Helper property for quickly getting to the line number a stack frame is
- * currently paused at.
- */
-Object.defineProperty(Debugger.Frame.prototype, "line", {
-  configurable: true,
-  get: function() {
-    if (this.script) {
-      return this.script.getOffsetLine(this.offset);
-    } else {
-      return null;
-    }
-  }
-});
+function hackDebugger(Debugger) {
+  // TODO: Improve native code instead of hacking on top of it
+
+  /**
+   * Override the toString method in order to get more meaningful script output
+   * for debugging the debugger.
+   */
+  Debugger.Script.prototype.toString = function() {
+    let output = "";
+    if (this.url) {
+      output += this.url;
+    }
+    if (typeof this.staticLevel != "undefined") {
+      output += ":L" + this.staticLevel;
+    }
+    if (typeof this.startLine != "undefined") {
+      output += ":" + this.startLine;
+      if (this.lineCount && this.lineCount > 1) {
+        output += "-" + (this.startLine + this.lineCount - 1);
+      }
+    }
+    if (typeof this.startLine != "undefined") {
+      output += ":" + this.startLine;
+      if (this.lineCount && this.lineCount > 1) {
+        output += "-" + (this.startLine + this.lineCount - 1);
+      }
+    }
+    if (this.strictMode) {
+      output += ":strict";
+    }
+    return output;
+  };
+
+  /**
+   * Helper property for quickly getting to the line number a stack frame is
+   * currently paused at.
+   */
+  Object.defineProperty(Debugger.Frame.prototype, "line", {
+    configurable: true,
+    get: function() {
+      if (this.script) {
+        return this.script.getOffsetLine(this.offset);
+      } else {
+        return null;
+      }
+    }
+  });
+}
 
 
 /**
  * Creates an actor for handling chrome debugging. ChromeDebuggerActor is a
  * thin wrapper over ThreadActor, slightly changing some of its behavior.
  *
  * @param aConnection object
  *        The DebuggerServerConnection with which this ChromeDebuggerActor
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -5,38 +5,35 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let { Ci, Cu } = require("chrome");
 let Services = require("Services");
 let { ActorPool, createExtraActors, appendExtraActors } = require("devtools/server/actors/common");
 let { RootActor } = require("devtools/server/actors/root");
-let { AddonThreadActor, ThreadActor } = require("devtools/server/actors/script");
 let { DebuggerServer } = require("devtools/server/main");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dbg_assert } = DevToolsUtils;
 let makeDebugger = require("./utils/make-debugger");
 let mapURIToAddonID = require("./utils/map-uri-to-addon-id");
 
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true);
+loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 
 // Assumptions on events module:
 // events needs to be dispatched synchronously,
 // by calling the listeners in the order or registration.
-XPCOMUtils.defineLazyGetter(this, "events", () => {
-  return require("sdk/event/core");
-});
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
 
-XPCOMUtils.defineLazyGetter(this, "StyleSheetActor", () => {
-  return require("devtools/server/actors/stylesheets").StyleSheetActor;
-});
+loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true);
 
 function getWindowID(window) {
   return window.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindowUtils)
                .currentInnerWindowID;
 }
 
 /**
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -28,17 +28,16 @@ DevToolsUtils.defineLazyGetter(this, "De
 // On B2G, `this` != Global scope, so `Ci` won't be binded on `this`
 // (i.e. this.Ci is undefined) Then later, when using loadSubScript,
 // Ci,... won't be defined for sub scripts.
 this.Ci = Ci;
 this.Cc = Cc;
 this.CC = CC;
 this.Cu = Cu;
 this.Cr = Cr;
-this.Debugger = Debugger;
 this.Services = Services;
 this.ActorPool = ActorPool;
 this.DevToolsUtils = DevToolsUtils;
 this.dumpn = dumpn;
 this.dumpv = dumpv;
 this.dbg_assert = dbg_assert;
 
 // Overload `Components` to prevent SDK loader exception on Components
@@ -71,17 +70,18 @@ function loadSubScript(aURL)
                    (e.fileName ? "at " + e.fileName + " : " + e.lineNumber + "\n" : "") +
                    e + " - " + e.stack + "\n";
     dump(errorStr);
     reportError(errorStr);
     throw e;
   }
 }
 
-let events = require("sdk/event/core");
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+
 let {defer, resolve, reject, all} = require("devtools/toolkit/deprecated-sync-thenables");
 this.defer = defer;
 this.resolve = resolve;
 this.reject = reject;
 this.all = all;
 
 var gRegisteredModules = Object.create(null);
 
@@ -171,17 +171,17 @@ var DebuggerServer = {
       return;
     }
 
     this.initTransport();
 
     this._initialized = true;
   },
 
-  protocol: require("devtools/server/protocol"),
+  get protocol() require("devtools/server/protocol"),
 
   /**
    * Initialize the debugger server's transport variables.  This can be
    * in place of init() for cases where the jsdebugger isn't needed.
    */
   initTransport: function DS_initTransport() {
     if (this._transportInitialized) {
       return;
--- a/toolkit/devtools/worker-loader.js
+++ b/toolkit/devtools/worker-loader.js
@@ -302,21 +302,42 @@ if (typeof Components === "object") {
     sandbox
   );
   const Debugger = sandbox.Debugger;
 
   const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
   const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
                        getService(Ci.nsIJSInspector);
 
-  this.worker = new WorkerDebuggerLoader({
+  let worker = this.worker = new WorkerDebuggerLoader({
     createSandbox: createSandbox,
     globals: {
       "isWorker": true,
       "reportError": Cu.reportError,
+      "loader": {
+        lazyGetter: function (aObject, aName, aLambda) {
+          Object.defineProperty(aObject, aName, {
+            get: function () {
+              delete aObject[aName];
+              return aObject[aName] = aLambda.apply(aObject);
+            },
+            configurable: true,
+            enumerable: true
+          });
+        },
+        lazyImporter: function () { throw new Error("Can't import JSM from worker debugger server") },
+        lazyServiceGetter: function () { throw new Error("Can't import XPCOM from worker debugger server") },
+        lazyRequireGetter: function (obj, property, module, destructure) {
+          Object.defineProperty(obj, property, {
+            get: () => destructure
+              ? worker.require(module)[property]
+              : worker.require(module || property)
+          });
+        }
+      }
     },
     loadInSandbox: loadInSandbox,
     modules: {
       "Services": {},
       "chrome": chrome,
       "promise": Promise,
       "Debugger": Debugger,
       "xpcInspector": xpcInspector,