Bug 995198 - Promise Debugging API. r=paolo
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Tue, 15 Apr 2014 12:51:19 -0400
changeset 197182 963eaafdb70f7f3fbaaf4d27734f1ec9709a4a22
parent 197181 ea688f9353895db67bcadc3b0da136c9e0176de8
child 197183 1dbade92ce0a37b9a5db2373b68dd81bf2cca210
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs995198
milestone31.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 995198 - Promise Debugging API. r=paolo
toolkit/modules/Promise-backend.js
toolkit/modules/tests/xpcshell/test_Promise.js
--- a/toolkit/modules/Promise-backend.js
+++ b/toolkit/modules/Promise-backend.js
@@ -73,18 +73,37 @@ const N_WITNESS = Name("witness");
 // In this snippet, the error is reported both by p1 and by p2.
 //
 
 XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
                                    "@mozilla.org/toolkit/finalizationwitness;1",
                                    "nsIFinalizationWitnessService");
 
 let PendingErrors = {
+  // An internal counter, used to generate unique id.
   _counter: 0,
+  // Functions registered to be notified when a pending error
+  // is reported as uncaught.
+  _observers: new Set(),
   _map: new Map(),
+
+  /**
+   * Initialize PendingErrors
+   */
+  init: function() {
+    Services.obs.addObserver(function observe(aSubject, aTopic, aValue) {
+      PendingErrors.report(aValue);
+    }, "promise-finalization-witness", false);
+  },
+
+  /**
+   * Register an error as tracked.
+   *
+   * @return The unique identifier of the error.
+   */
   register: function(error) {
     let id = "pending-error-" + (this._counter++);
     //
     // At this stage, ideally, we would like to store the error itself
     // and delay any treatment until we are certain that we will need
     // to report that error. However, in the (unlikely but possible)
     // case the error holds a reference to the promise itself, doing so
     // would prevent the promise from being garbabe-collected, which
@@ -161,56 +180,112 @@ let PendingErrors = {
         }
       }
     } catch (ex) {
       // Ignore value
     }
     this._map.set(id, value);
     return id;
   },
-  extract: function(id) {
+
+  /**
+   * Notify all observers that a pending error is now uncaught.
+   *
+   * @param id The identifier of the pending error, as returned by
+   * |register|.
+   */
+  report: function(id) {
     let value = this._map.get(id);
+    if (!value) {
+      return; // The error has already been reported
+    }
     this._map.delete(id);
-    return value;
+    for (let obs of this._observers.values()) {
+      obs(value);
+    }
   },
+
+  /**
+   * Mark all pending errors are uncaught, notify the observers.
+   */
+  flush: function() {
+    // Since we are going to modify the map while walking it,
+    // let's copying the keys first.
+    let keys = [key for (key of this._map.keys())];
+    for (let key of keys) {
+      this.report(key);
+    }
+  },
+
+  /**
+   * Stop tracking an error, as this error has been caught,
+   * eventually.
+   */
   unregister: function(id) {
     this._map.delete(id);
+  },
+
+  /**
+   * Add an observer notified when an error is reported as uncaught.
+   *
+   * @param {function} observer A function notified when an error is
+   * reported as uncaught. Its arguments are
+   *   {message, date, fileName, stack, lineNumber}
+   * All arguments are optional.
+   */
+  addObserver: function(observer) {
+    this._observers.add(observer);
+  },
+
+  /**
+   * Remove an observer added with addObserver
+   */
+  removeObserver: function(observer) {
+    this._observers.delete(observer);
+  },
+
+  /**
+   * Remove all the observers added with addObserver
+   */
+  removeAllObservers: function() {
+    this._observers.clear();
   }
 };
+PendingErrors.init();
 
-// Actually print the finalization warning.
-Services.obs.addObserver(function observe(aSubject, aTopic, aValue) {
-  let error = PendingErrors.extract(aValue);
-  let {message, date, fileName, stack, lineNumber} = error;
+// Default mechanism for displaying errors
+PendingErrors.addObserver(function(details) {
   let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
   if (!error || !Services.console) {
     // Too late during shutdown to use the nsIConsole
     dump("*************************\n");
     dump("A promise chain failed to handle a rejection\n\n");
-    dump("On: " + date + "\n");
-    dump("Full message: " + message + "\n");
+    dump("On: " + details.date + "\n");
+    dump("Full message: " + details.message + "\n");
     dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n");
-    dump("Full stack: " + (stack||"not available") + "\n");
+    dump("Full stack: " + (details.stack||"not available") + "\n");
     dump("*************************\n");
     return;
   }
-  if (stack) {
-    message += "\nFull Stack: " + stack;
+  let message = details.message;
+  if (details.stack) {
+    message += "\nFull Stack: " + details.stack;
   }
   error.init(
              /*message*/"A promise chain failed to handle a rejection.\n\n" +
-             "Date: " + date + "\nFull Message: " + message,
-             /*sourceName*/ fileName,
-             /*sourceLine*/ lineNumber?("" + lineNumber):0,
-             /*lineNumber*/ lineNumber || 0,
+             "Date: " + details.date + "\nFull Message: " + details.message,
+             /*sourceName*/ details.fileName,
+             /*sourceLine*/ details.lineNumber?("" + details.lineNumber):0,
+             /*lineNumber*/ details.lineNumber || 0,
              /*columnNumber*/ 0,
              /*flags*/ Ci.nsIScriptError.errorFlag,
              /*category*/ "chrome javascript");
   Services.console.logMessage(error);
-}, "promise-finalization-witness", false);
+});
+
 
 ///////// Additional warnings for developers
 //
 // The following error types are considered programmer errors, which should be
 // reported (possibly redundantly) so as to let programmers fix their code.
 const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -491,16 +566,56 @@ Promise.race = function (aValues)
 
   return new Promise((resolve, reject) => {
     for (let value of aValues) {
       Promise.resolve(value).then(resolve, reject);
     }
   });
 };
 
+Promise.Debugging = {
+  /**
+   * Add an observer notified when an error is reported as uncaught.
+   *
+   * @param {function} observer A function notified when an error is
+   * reported as uncaught. Its arguments are
+   *   {message, date, fileName, stack, lineNumber}
+   * All arguments are optional.
+   */
+  addUncaughtErrorObserver: function(observer) {
+    PendingErrors.addObserver(observer);
+  },
+
+  /**
+   * Remove an observer added with addUncaughtErrorObserver
+   *
+   * @param {function} An observer registered with
+   * addUncaughtErrorObserver.
+   */
+  removeUncaughtErrorObserver: function(observer) {
+    PendingErrors.removeObserver(observer);
+  },
+
+  /**
+   * Remove all the observers added with addUncaughtErrorObserver
+   */
+  clearUncaughtErrorObservers: function() {
+    PendingErrors.removeAllObservers();
+  },
+
+  /**
+   * Force all pending errors to be reported immediately as uncaught.
+   * Note that this may cause some false positives.
+   */
+  flushUncaughtErrors: function() {
+    PendingErrors.flush();
+  },
+};
+Object.freeze(Promise.Debugging);
+
 Object.freeze(Promise);
 
 ////////////////////////////////////////////////////////////////////////////////
 //// PromiseWalker
 
 /**
  * This singleton object invokes the handlers registered on resolved and
  * rejected promises, ensuring that processing is not recursive and is done in
--- a/toolkit/modules/tests/xpcshell/test_Promise.js
+++ b/toolkit/modules/tests/xpcshell/test_Promise.js
@@ -1,16 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 Components.utils.import("resource://gre/modules/Promise.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 
+// Deactivate the standard xpcshell observer, as it turns uncaught
+// rejections into failures, which we don't want here.
+Promise.Debugging.clearUncaughtErrorObservers();
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Test runner
 
 let run_promise_tests = function run_promise_tests(tests, cb) {
   let loop = function loop(index) {
     if (index >= tests.length) {
       if (cb) {
         cb.call();
@@ -954,38 +958,36 @@ tests.push(
 function wait_for_uncaught(aMustAppear, aTimeout = undefined) {
   let remaining = new Set();
   for (let k of aMustAppear) {
     remaining.add(k);
   }
   let deferred = Promise.defer();
   let print = do_print;
   let execute_soon = do_execute_soon;
-  let observer = function(aMessage) {
-    execute_soon(function() {
-      let message = aMessage.message;
-      print("Observing " + message);
-      for (let expected of remaining) {
-        if (message.indexOf(expected) != -1) {
-          print("I found " + expected);
-          remaining.delete(expected);
-        }
+  let observer = function({message, stack}) {
+    let data = message + stack;
+    print("Observing " + message + ", looking for " + aMustAppear.join(", "));
+    for (let expected of remaining) {
+      if (data.indexOf(expected) != -1) {
+        print("I found " + expected);
+        remaining.delete(expected);
       }
       if (remaining.size == 0 && observer) {
-        Services.console.unregisterListener(observer);
+        Promise.Debugging.removeUncaughtErrorObserver(observer);
         observer = null;
         deferred.resolve();
       }
-    });
+    }
   };
-  Services.console.registerListener(observer);
+  Promise.Debugging.addUncaughtErrorObserver(observer);
   if (aTimeout) {
     do_timeout(aTimeout, function timeout() {
       if (observer) {
-        Services.console.unregisterListener(observer);
+        Promise.Debugging.removeUncaughtErrorObserver(observer);
         observer = null;
       }
       deferred.reject(new Error("Timeout"));
     });
   }
   return deferred.promise;
 }
 
@@ -1050,16 +1052,17 @@ function wait_for_uncaught(aMustAppear, 
           //
           // Unfortunately, we might still have intermittent failures,
           // materialized as timeouts.
           //
           for (let i = 0; i < 100; ++i) {
             Promise.reject(error);
           }
         })();
+        do_print("Posted all rejections");
         Components.utils.forceGC();
         Components.utils.forceCC();
         Components.utils.forceShrinkingGC();
         return promise;
       }));
   }
 })();