Bug 1177226 - Support User Timing API events in the Developer HUD. r=ehsan, r=jryans
authorRuss Nicoletti <rnicoletti@mozilla.com>
Fri, 31 Jul 2015 15:40:08 -0700
changeset 287767 105d5ff7b72c8c4b8e47f13cb85434f1af418672
parent 287709 13296fe1f1d16a1d40ba1c1918c43fd66bf4acec
child 287768 664df831252b1badd3a040b8bfc512a60c84d1a9
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, jryans
bugs1177226
milestone42.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 1177226 - Support User Timing API events in the Developer HUD. r=ehsan, r=jryans
b2g/app/b2g.js
b2g/chrome/content/devtools/hud.js
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsPerformance.cpp
dom/base/nsPerformance.h
dom/webidl/PerformanceEntryEvent.webidl
dom/webidl/moz.build
modules/libpref/init/all.js
toolkit/devtools/server/actors/performance-entries.js
toolkit/devtools/server/main.js
toolkit/devtools/server/moz.build
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -1162,8 +1162,12 @@ pref("layers.compositor-lru-size", 10);
 // Enable Cardboard VR on mobile, assuming VR at all is enabled
 pref("dom.vr.cardboard.enabled", true);
 
 // In B2G by deafult any AudioChannelAgent is muted when created.
 pref("dom.audiochannel.mutedByDefault", true);
 
 // Default device name for Presentation API
 pref("dom.presentation.device.name", "Firefox OS");
+
+// Enable notification of performance timing
+pref("dom.performance.enable_notify_performance_timing", true);
+
--- a/b2g/chrome/content/devtools/hud.js
+++ b/b2g/chrome/content/devtools/hud.js
@@ -20,16 +20,20 @@ XPCOMUtils.defineLazyGetter(this, 'Debug
 XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() {
   return devtools.require('devtools/toolkit/webconsole/utils').Utils;
 });
 
 XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() {
   return devtools.require('devtools/server/actors/eventlooplag').EventLoopLagFront;
 });
 
+XPCOMUtils.defineLazyGetter(this, 'PerformanceEntriesFront', function() {
+  return devtools.require('devtools/server/actors/performance-entries').PerformanceEntriesFront;
+});
+
 XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() {
   return devtools.require('devtools/server/actors/memory').MemoryFront;
 });
 
 Cu.import('resource://gre/modules/Frames.jsm');
 
 let _telemetryDebug = true;
 
@@ -583,16 +587,86 @@ let eventLoopLagWatcher = {
     if (fronts.has(target)) {
       fronts.get(target).destroy();
       fronts.delete(target);
     }
   }
 };
 developerHUD.registerWatcher(eventLoopLagWatcher);
 
+/*
+ * The performanceEntriesWatcher determines the delta between the epoch
+ * of an app's launch time and the app's performance entry marks.
+ * When it receives an "appLaunch" performance entry mark it records the
+ * name of the app being launched and the epoch of when the launch ocurred.
+ * When it receives subsequent performance entry events for the app being
+ * launched, it records the delta of the performance entry opoch compared
+ * to the app-launch epoch and emits an "app-start-time-<performance mark name>"
+ * event containing the delta.
+ */
+let performanceEntriesWatcher = {
+  _client: null,
+  _fronts: new Map(),
+  _appLaunchName: null,
+  _appLaunchStartTime: null,
+
+  init(client) {
+    this._client = client;
+  },
+
+  trackTarget(target) {
+    // The performanceEntries watcher doesn't register a metric because
+    // currently the metrics generated are not displayed in
+    // in the front-end.
+
+    let front = new PerformanceEntriesFront(this._client, target.actor);
+    this._fronts.set(target, front);
+
+    // User timings are always gathered; there is no setting to enable/
+    // disable.
+    front.start();
+
+    front.on('entry', detail => {
+      if (detail.type === 'mark') {
+        let name = detail.name;
+        let epoch = detail.epoch;
+        let CHARS_UNTIL_APP_NAME = 7; // '@app://'
+
+        // FIXME There is a potential race condition that can result
+        // in some performance entries being disregarded. See bug 1189942.
+        if (name.indexOf('appLaunch') != -1) {
+          let appStartPos = name.indexOf('@app') + CHARS_UNTIL_APP_NAME;
+          let length = (name.indexOf('.') - appStartPos);
+          this._appLaunchName = name.substr(appStartPos, length);
+          this._appLaunchStartTime = epoch;
+        } else {
+          let origin = detail.origin;
+          origin = origin.substr(0, origin.indexOf('.'));
+          if (this._appLaunchName === origin) {
+            let time = epoch - this._appLaunchStartTime;
+            let eventName = 'app-startup-time-' + name;
+
+            // Events based on performance marks are for telemetry only, they are
+            // not displayed in the HUD front end.
+            target._sendTelemetryEvent({name: eventName, value: time});
+          }
+        }
+      }
+    });
+  },
+
+  untrackTarget(target) {
+    let fronts = this._fronts;
+    if (fronts.has(target)) {
+      fronts.get(target).destroy();
+      fronts.delete(target);
+    }
+  }
+};
+developerHUD.registerWatcher(performanceEntriesWatcher);
 
 /**
  * The Memory Watcher uses devtools actors to track memory usage.
  */
 let memoryWatcher = {
 
   _client: null,
   _fronts: new Map(),
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -257,16 +257,17 @@ bool nsContentUtils::sTrustedFullScreenO
 bool nsContentUtils::sIsCutCopyAllowed = true;
 bool nsContentUtils::sIsPerformanceTimingEnabled = false;
 bool nsContentUtils::sIsResourceTimingEnabled = false;
 bool nsContentUtils::sIsUserTimingLoggingEnabled = false;
 bool nsContentUtils::sIsExperimentalAutocompleteEnabled = false;
 bool nsContentUtils::sEncodeDecodeURLHash = false;
 bool nsContentUtils::sGettersDecodeURLHash = false;
 bool nsContentUtils::sPrivacyResistFingerprinting = false;
+bool nsContentUtils::sSendPerformanceTimingNotifications = false;
 
 uint32_t nsContentUtils::sHandlingInputTimeout = 1000;
 
 nsHtml5StringParser* nsContentUtils::sHTMLFragmentParser = nullptr;
 nsIParser* nsContentUtils::sXMLFragmentParser = nullptr;
 nsIFragmentContentSink* nsContentUtils::sXMLFragmentSink = nullptr;
 bool nsContentUtils::sFragmentParsingActive = false;
 
@@ -548,16 +549,19 @@ nsContentUtils::Init()
 
   Preferences::AddBoolVarCache(&sPrivacyResistFingerprinting,
                                "privacy.resistFingerprinting", false);
 
   Preferences::AddUintVarCache(&sHandlingInputTimeout,
                                "dom.event.handling-user-input-time-limit",
                                1000);
 
+  Preferences::AddBoolVarCache(&sSendPerformanceTimingNotifications,
+                               "dom.performance.enable_notify_performance_timing", false);
+
 #if !(defined(DEBUG) || defined(MOZ_ENABLE_JS_DUMP))
   Preferences::AddBoolVarCache(&sDOMWindowDumpEnabled,
                                "browser.dom.window.dump.enabled");
 #endif
 
   Element::InitCCCallbacks();
 
   nsCOMPtr<nsIUUIDGenerator> uuidGenerator =
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1944,16 +1944,24 @@ public:
    * Returns true if the performance timing APIs are enabled.
    */
   static bool IsResourceTimingEnabled()
   {
     return sIsResourceTimingEnabled;
   }
 
   /*
+   * Returns true if notification should be sent for peformance timing events.
+   */
+  static bool SendPerformanceTimingNotifications()
+  {
+    return sSendPerformanceTimingNotifications;
+  }
+
+  /*
    * Returns true if URL setters should percent encode the Hash/Ref segment
    * and getters should return the percent decoded value of the segment
    */
   static bool EncodeDecodeURLHash()
   {
     return sEncodeDecodeURLHash;
   }
 
@@ -2539,16 +2547,17 @@ private:
   static uint32_t sHandlingInputTimeout;
   static bool sIsPerformanceTimingEnabled;
   static bool sIsResourceTimingEnabled;
   static bool sIsUserTimingLoggingEnabled;
   static bool sIsExperimentalAutocompleteEnabled;
   static bool sEncodeDecodeURLHash;
   static bool sGettersDecodeURLHash;
   static bool sPrivacyResistFingerprinting;
+  static bool sSendPerformanceTimingNotifications;
 
   static nsHtml5StringParser* sHTMLFragmentParser;
   static nsIParser* sXMLFragmentParser;
   static nsIFragmentContentSink* sXMLFragmentSink;
 
   /**
    * True if there's a fragment parser activation on the stack.
    */
--- a/dom/base/nsPerformance.cpp
+++ b/dom/base/nsPerformance.cpp
@@ -16,16 +16,17 @@
 #include "nsIURI.h"
 #include "nsThreadUtils.h"
 #include "PerformanceEntry.h"
 #include "PerformanceMark.h"
 #include "PerformanceMeasure.h"
 #include "PerformanceResourceTiming.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/PerformanceBinding.h"
+#include "mozilla/dom/PerformanceEntryEvent.h"
 #include "mozilla/dom/PerformanceTimingBinding.h"
 #include "mozilla/dom/PerformanceNavigationBinding.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/TimeStamp.h"
 #include "js/HeapAPI.h"
 #include "WorkerPrivate.h"
 #include "WorkerRunnable.h"
@@ -733,24 +734,34 @@ nsPerformance::IsEnabled(JSContext* aCx,
   return runnable->IsEnabled();
 }
 
 void
 nsPerformance::InsertUserEntry(PerformanceEntry* aEntry)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (nsContentUtils::IsUserTimingLoggingEnabled()) {
-    nsAutoCString uri;
+  nsAutoCString uri;
+  uint64_t markCreationEpoch = 0;
+  if (nsContentUtils::IsUserTimingLoggingEnabled() ||
+      nsContentUtils::SendPerformanceTimingNotifications()) {
     nsresult rv = GetOwner()->GetDocumentURI()->GetHost(uri);
     if(NS_FAILED(rv)) {
       // If we have no URI, just put in "none".
       uri.AssignLiteral("none");
     }
-    PerformanceBase::LogEntry(aEntry, uri);
+    markCreationEpoch = static_cast<uint64_t>(PR_Now() / PR_USEC_PER_MSEC);
+
+    if (nsContentUtils::IsUserTimingLoggingEnabled()) {
+      PerformanceBase::LogEntry(aEntry, uri);
+    }
+  }
+
+  if (nsContentUtils::SendPerformanceTimingNotifications()) {
+    TimingNotification(aEntry, uri, markCreationEpoch);
   }
 
   PerformanceBase::InsertUserEntry(aEntry);
 }
 
 DOMHighResTimeStamp
 nsPerformance::DeltaFromNavigationStart(DOMHighResTimeStamp aTime)
 {
@@ -982,16 +993,39 @@ PerformanceBase::LogEntry(PerformanceEnt
           NS_ConvertUTF16toUTF8(aEntry->GetEntryType()).get(),
           NS_ConvertUTF16toUTF8(aEntry->GetName()).get(),
           aEntry->StartTime(),
           aEntry->Duration(),
           static_cast<uint64_t>(PR_Now() / PR_USEC_PER_MSEC));
 }
 
 void
+PerformanceBase::TimingNotification(PerformanceEntry* aEntry, const nsACString& aOwner, uint64_t aEpoch)
+{
+  PerformanceEntryEventInit init;
+  init.mBubbles = false;
+  init.mCancelable = false;
+  init.mName = aEntry->GetName();
+  init.mEntryType = aEntry->GetEntryType();
+  init.mStartTime = aEntry->StartTime();
+  init.mDuration = aEntry->Duration();
+  init.mEpoch = aEpoch;
+  init.mOrigin = NS_ConvertUTF8toUTF16(aOwner.BeginReading());
+
+  nsRefPtr<PerformanceEntryEvent> perfEntryEvent =
+    PerformanceEntryEvent::Constructor(this, NS_LITERAL_STRING("performanceentry"), init);
+
+  nsCOMPtr<EventTarget> et = do_QueryInterface(GetOwner());
+  if (et) {
+    bool dummy = false;
+    et->DispatchEvent(perfEntryEvent, &dummy);
+  }
+}
+
+void
 PerformanceBase::InsertUserEntry(PerformanceEntry* aEntry)
 {
   mUserEntries.InsertElementSorted(aEntry,
                                    PerformanceEntryComparator());
 }
 
 void
 PerformanceBase::SetResourceTimingBufferSize(uint64_t aMaxSize)
--- a/dom/base/nsPerformance.h
+++ b/dom/base/nsPerformance.h
@@ -346,16 +346,17 @@ protected:
   GetPerformanceTimingFromString(const nsAString& aTimingName) = 0;
 
   bool IsResourceEntryLimitReached() const
   {
     return mResourceEntries.Length() >= mResourceTimingBufferSize;
   }
 
   void LogEntry(PerformanceEntry* aEntry, const nsACString& aOwner) const;
+  void TimingNotification(PerformanceEntry* aEntry, const nsACString& aOwner, uint64_t epoch);
 
 private:
   nsTArray<nsRefPtr<PerformanceEntry>> mUserEntries;
   nsTArray<nsRefPtr<PerformanceEntry>> mResourceEntries;
 
   uint64_t mResourceTimingBufferSize;
   static const uint64_t kDefaultResourceTimingBufferSize = 150;
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/PerformanceEntryEvent.webidl
@@ -0,0 +1,27 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+dictionary PerformanceEntryEventInit : EventInit
+{
+  DOMString name = "";
+  DOMString entryType = "";
+  DOMHighResTimeStamp startTime = 0;
+  DOMHighResTimeStamp duration = 0;
+  double epoch = 0;
+  DOMString origin = "";
+};
+
+[Constructor(DOMString type, optional PerformanceEntryEventInit eventInitDict),
+ ChromeOnly]
+interface PerformanceEntryEvent : Event
+{
+  readonly attribute DOMString name;
+  readonly attribute DOMString entryType;
+  readonly attribute DOMHighResTimeStamp startTime;
+  readonly attribute DOMHighResTimeStamp duration;
+  readonly attribute double epoch;
+  readonly attribute DOMString origin;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -774,16 +774,17 @@ GENERATED_EVENTS_WEBIDL_FILES = [
     'MozMmsEvent.webidl',
     'MozOtaStatusEvent.webidl',
     'MozSettingsEvent.webidl',
     'MozSettingsTransactionEvent.webidl',
     'MozSmsEvent.webidl',
     'MozStkCommandEvent.webidl',
     'MozVoicemailEvent.webidl',
     'PageTransitionEvent.webidl',
+    'PerformanceEntryEvent.webidl',
     'PluginCrashedEvent.webidl',
     'PopStateEvent.webidl',
     'PopupBlockedEvent.webidl',
     'ProgressEvent.webidl',
     'RecordErrorEvent.webidl',
     'ScrollViewChangeEvent.webidl',
     'SelectionStateChangedEvent.webidl',
     'StyleRuleChangeEvent.webidl',
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -155,16 +155,19 @@ pref("dom.enable_performance", true);
 pref("dom.enable_resource_timing", true);
 
 // Enable high-resolution timing markers for users
 pref("dom.enable_user_timing", true);
 
 // Enable printing performance marks/measures to log
 pref("dom.performance.enable_user_timing_logging", false);
 
+// Enable notification of performance timing
+pref("dom.performance.enable_notify_performance_timing", false);
+
 // Whether the Gamepad API is enabled
 pref("dom.gamepad.enabled", true);
 #ifdef RELEASE_BUILD
 pref("dom.gamepad.non_standard_events.enabled", false);
 #else
 pref("dom.gamepad.non_standard_events.enabled", true);
 #endif
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/performance-entries.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The performanceEntries actor emits events corresponding to performance
+ * entries. It receives `performanceentry` events containing the performance
+ * entry details and emits an event containing the name, type, origin, and
+ * epoch of the performance entry.
+ */
+
+const {
+  method, Arg, Option, RetVal, Front, FrontClass, Actor, ActorClass
+} = require("devtools/server/protocol");
+const events = require("sdk/event/core");
+
+let PerformanceEntriesActor = exports.PerformanceEntriesActor = ActorClass({
+
+  typeName: "performanceEntries",
+
+  listenerAdded: false,
+
+  events: {
+    "entry" : {
+      type: "entry",
+      detail: Arg(0, "json") // object containing performance entry name, type,
+                             // origin, and epoch.
+    }
+  },
+
+  initialize: function(conn, tabActor) {
+    Actor.prototype.initialize.call(this, conn);
+    this.window = tabActor.window;
+  },
+
+  /**
+   * Start tracking the user timings.
+   */
+  start: method(function() {
+    if (!this.listenerAdded) {
+      this.onPerformanceEntry = this.onPerformanceEntry.bind(this);
+      this.window.addEventListener("performanceentry", this.onPerformanceEntry, true);
+      this.listenerAdded = true;
+    }
+  }),
+
+  /**
+   * Stop tracking the user timings.
+   */
+  stop: method(function() {
+    if (this.listenerAdded) {
+      this.window.removeEventListener("performanceentry", this.onPerformanceEntry, true);
+      this.listenerAdded = false;
+    }
+  }),
+
+  disconnect: function() {
+    this.destroy();
+  },
+
+  destroy: function() {
+    this.stop();
+    Actor.prototype.destroy.call(this);
+  },
+
+  onPerformanceEntry: function (e) {
+    let emitDetail = {
+      type: e.entryType,
+      name: e.name,
+      origin: e.origin,
+      epoch: e.epoch
+    };
+    events.emit(this, 'entry', emitDetail);
+  }
+});
+
+exports.PerformanceEntriesFront = FrontClass(PerformanceEntriesActor, {
+  initialize: function(client, form) {
+    Front.prototype.initialize.call(this, client);
+    this.actorID = form.performanceEntriesActor;
+    this.manage(this);
+  },
+});
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -540,16 +540,21 @@ var DebuggerServer = {
       constructor: "AnimationsActor",
       type: { tab: true }
     });
     this.registerModule("devtools/server/actors/promises", {
       prefix: "promises",
       constructor: "PromisesActor",
       type: { global: true, tab: true }
     });
+    this.registerModule("devtools/server/actors/performance-entries", {
+      prefix: "performanceEntries",
+      constructor: "PerformanceEntriesActor",
+      type: { tab: true }
+    });
   },
 
   /**
    * Passes a set of options to the BrowserAddonActors for the given ID.
    *
    * @param aId string
    *        The ID of the add-on to pass the options to
    * @param aOptions object
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -70,16 +70,17 @@ EXTRA_JS_MODULES.devtools.server.actors 
     'actors/framerate.js',
     'actors/gcli.js',
     'actors/highlighter.js',
     'actors/inspector.js',
     'actors/layout.js',
     'actors/memory.js',
     'actors/monitor.js',
     'actors/object.js',
+    'actors/performance-entries.js',
     'actors/preference.js',
     'actors/pretty-print-worker.js',
     'actors/profiler.js',
     'actors/promises.js',
     'actors/root.js',
     'actors/script.js',
     'actors/settings.js',
     'actors/storage.js',