Bug 1070089 - change timeline markers to have a consistent epoch (carrying forward r=smaug)
authorTom Tromey <tom@tromey.com>
Fri, 17 Oct 2014 16:02:42 -0700
changeset 211213 9acb83f0af3b3cec51075bd5521f9cf76fd5ba99
parent 211212 369cc491a650d3417f2c45a9f077e958a5a60a41
child 211214 e91747e0ee2fb768201ee4d2964c45225fdfb3dc
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerssmaug
bugs1070089
milestone36.0a1
Bug 1070089 - change timeline markers to have a consistent epoch (carrying forward r=smaug)
browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
browser/devtools/timeline/timeline.js
browser/devtools/timeline/widgets/overview.js
browser/devtools/timeline/widgets/waterfall.js
docshell/base/nsDocShell.cpp
docshell/base/nsDocShell.h
docshell/base/nsIDocShell.idl
dom/webidl/ProfileTimelineMarker.webidl
toolkit/devtools/server/actors/timeline.js
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
@@ -24,17 +24,17 @@ let test = Task.async(function*() {
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has ended.");
 
   let markers = TimelineController.getMarkers();
   let selection = TimelineView.overview.getSelection();
 
   is((selection.start) | 0,
-     (markers[0].start * TimelineView.overview.dataScaleX) | 0,
+     ((markers[0].start - markers.startTime) * TimelineView.overview.dataScaleX) | 0,
     "The initial selection start is correct.");
 
   is((selection.end - selection.start) | 0,
      (selectionRatio * TimelineView.overview.width) | 0,
     "The initial selection end is correct.");
 
   yield teardown(panel);
   finish();
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -102,23 +102,29 @@ let TimelineController = {
       yield this._stopRecording();
     }
   }),
 
   /**
    * Starts the recording, updating the UI as needed.
    */
   _startRecording: function*() {
+    TimelineView.handleRecordingStarted();
+    let startTime = yield gFront.start();
+    // Times must come from the actor in order to be self-consistent.
+    // However, we also want to update the view with the elapsed time
+    // even when the actor is not generating data.  To do this we get
+    // the local time and use it to compute a reasonable elapsed time.
+    // See _onRecordingTick.
+    this._localStartTime = performance.now();
+
     this._markers = [];
-    this._markers.startTime = performance.now();
-    this._markers.endTime = performance.now();
+    this._markers.startTime = startTime;
+    this._markers.endTime = startTime;
     this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
-
-    TimelineView.handleRecordingStarted();
-    yield gFront.start();
   },
 
   /**
    * Stops the recording, updating the UI as needed.
    */
   _stopRecording: function*() {
     clearInterval(this._updateId);
 
@@ -137,27 +143,36 @@ let TimelineController = {
   },
 
   /**
    * Callback handling the "markers" event on the timeline front.
    *
    * @param array markers
    *        A list of new markers collected since the last time this
    *        function was invoked.
+   * @param number endTime
+   *        A time after the last marker in markers was collected.
    */
-  _onMarkers: function(markers) {
+  _onMarkers: function(markers, endTime) {
     Array.prototype.push.apply(this._markers, markers);
+    this._markers.endTime = endTime;
   },
 
   /**
    * Callback invoked at a fixed interval while recording.
    * Updates the markers store with the current time and the timeline overview.
    */
   _onRecordingTick: function() {
-    this._markers.endTime = performance.now();
+    // Compute an approximate ending time for the view.  This is
+    // needed to ensure that the view updates even when new data is
+    // not being generated.
+    let fakeTime = this._markers.startTime + (performance.now() - this._localStartTime);
+    if (fakeTime > this._markers.endTime) {
+      this._markers.endTime = fakeTime;
+    }
     TimelineView.handleMarkersUpdate(this._markers);
   }
 };
 
 /**
  * Functions handling the timeline frontend view.
  */
 let TimelineView = {
@@ -209,22 +224,22 @@ let TimelineView = {
   handleRecordingEnded: function() {
     $("#record-button").removeAttribute("checked");
     $("#timeline-pane").selectedPanel = $("#timeline-waterfall");
 
     this.overview.selectionEnabled = true;
 
     let markers = TimelineController.getMarkers();
     if (markers.length) {
-      let start = markers[0].start * this.overview.dataScaleX;
+      let start = (markers[0].start - markers.startTime) * this.overview.dataScaleX;
       let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
       this.overview.setSelection({ start, end });
     } else {
       let duration = markers.endTime - markers.startTime;
-      this.waterfall.setData(markers, 0, duration);
+      this.waterfall.setData(markers, markers.startTime, markers.endTime);
     }
 
     window.emit(EVENTS.RECORDING_ENDED);
   },
 
   /**
    * Signals that a new set of markers was made available by the controller,
    * or that the overview graph needs to be updated.
@@ -246,18 +261,18 @@ let TimelineView = {
       this.waterfall.clearView();
       return;
     }
     let selection = this.overview.getSelection();
     let start = selection.start / this.overview.dataScaleX;
     let end = selection.end / this.overview.dataScaleX;
 
     let markers = TimelineController.getMarkers();
-    let timeStart = Math.min(start, end);
-    let timeEnd = Math.max(start, end);
+    let timeStart = markers.startTime + Math.min(start, end);
+    let timeEnd = markers.startTime + Math.max(start, end);
     this.waterfall.setData(markers, timeStart, timeEnd);
   },
 
   /**
    * Callback handling the "refresh" event on the timeline overview.
    */
   _onRefresh: function() {
     this.waterfall.recalculateBounds();
--- a/browser/devtools/timeline/widgets/overview.js
+++ b/browser/devtools/timeline/widgets/overview.js
@@ -165,16 +165,19 @@ Overview.prototype = Heritage.extend(Abs
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[0], style.stroke);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[1], style.fill);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[2], style.fill);
       gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[3], style.stroke);
       ctx.fillStyle = gradient;
       ctx.beginPath();
 
       for (let { start, end } of batch) {
+        start -= this._data.startTime;
+        end -= this._data.startTime;
+
         let left = start * dataScale;
         let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
         let width = Math.max(duration * dataScale, this._pixelRatio);
         ctx.rect(left, top, width, height);
       }
 
       ctx.fill();
 
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -71,26 +71,27 @@ function Waterfall(parent) {
 
 Waterfall.prototype = {
   /**
    * Populates this view with the provided data source.
    *
    * @param array markers
    *        A list of markers received from the controller.
    * @param number timeStart
-   *        The delta time (in milliseconds) to start drawing from.
+   *        The time (in milliseconds) to start drawing from.
    * @param number timeEnd
-   *        The delta time (in milliseconds) to end drawing at.
+   *        The time (in milliseconds) to end drawing at.
    */
   setData: function(markers, timeStart, timeEnd) {
     this.clearView();
 
     let dataScale = this._waterfallWidth / (timeEnd - timeStart);
     this._drawWaterfallBackground(dataScale);
-    this._buildHeader(this._headerContents, timeStart, dataScale);
+    // Label the header as if the first possible marker was at T=0.
+    this._buildHeader(this._headerContents, timeStart - markers.startTime, dataScale);
     this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
   },
 
   /**
    * Depopulates this view.
    */
   clearView: function() {
     while (this._headerContents.hasChildNodes()) {
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -2829,34 +2829,34 @@ NS_IMETHODIMP
 nsDocShell::SetRecordProfileTimelineMarkers(bool aValue)
 {
 #ifdef MOZ_ENABLE_PROFILER_SPS
   bool currentValue;
   GetRecordProfileTimelineMarkers(&currentValue);
   if (currentValue != aValue) {
     if (aValue) {
       ++gProfileTimelineRecordingsCount;
-      mProfileTimelineStartTime = TimeStamp::Now();
+      mProfileTimelineRecording = true;
     } else {
       --gProfileTimelineRecordingsCount;
-      mProfileTimelineStartTime = TimeStamp();
+      mProfileTimelineRecording = false;
       ClearProfileTimelineMarkers();
     }
   }
 
   return NS_OK;
 #else
   return NS_ERROR_FAILURE;
 #endif
 }
 
 NS_IMETHODIMP
 nsDocShell::GetRecordProfileTimelineMarkers(bool* aValue)
 {
-  *aValue = !mProfileTimelineStartTime.IsNull();
+  *aValue = mProfileTimelineRecording;
   return NS_OK;
 }
 
 nsresult
 nsDocShell::PopProfileTimelineMarkers(JSContext* aCx,
                           JS::MutableHandle<JS::Value> aProfileTimelineMarkers)
 {
 #ifdef MOZ_ENABLE_PROFILER_SPS
@@ -2934,45 +2934,49 @@ nsDocShell::PopProfileTimelineMarkers(JS
   mProfileTimelineMarkers.SwapElements(keptMarkers);
 
   return NS_OK;
 #else
   return NS_ERROR_FAILURE;
 #endif
 }
 
-float
-nsDocShell::GetProfileTimelineDelta()
-{
-  return (TimeStamp::Now() - mProfileTimelineStartTime).ToMilliseconds();
+nsresult
+nsDocShell::Now(DOMHighResTimeStamp* aWhen)
+{
+  bool ignore;
+  *aWhen = (TimeStamp::Now() - TimeStamp::ProcessCreation(ignore)).ToMilliseconds();
+  return NS_OK;
 }
 
 void
 nsDocShell::AddProfileTimelineMarker(const char* aName,
                                      TracingMetadata aMetaData)
 {
 #ifdef MOZ_ENABLE_PROFILER_SPS
-  if (!mProfileTimelineStartTime.IsNull()) {
-    float delta = GetProfileTimelineDelta();
+  if (mProfileTimelineRecording) {
+    DOMHighResTimeStamp delta;
+    Now(&delta);
     ProfilerMarkerTracing* payload = new ProfilerMarkerTracing("Timeline",
                                                                aMetaData);
     mProfileTimelineMarkers.AppendElement(
       new InternalProfileTimelineMarker(aName, payload, delta));
   }
 #endif
 }
 
 void
 nsDocShell::AddProfileTimelineMarker(const char* aName,
                                      ProfilerBacktrace* aCause,
                                      TracingMetadata aMetaData)
 {
 #ifdef MOZ_ENABLE_PROFILER_SPS
-  if (!mProfileTimelineStartTime.IsNull()) {
-    float delta = GetProfileTimelineDelta();
+  if (mProfileTimelineRecording) {
+    DOMHighResTimeStamp delta;
+    Now(&delta);
     ProfilerMarkerTracing* payload = new ProfilerMarkerTracing("Timeline",
                                                                aMetaData,
                                                                aCause);
     mProfileTimelineMarkers.AppendElement(
       new InternalProfileTimelineMarker(aName, payload, delta));
   }
 #endif
 }
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -946,46 +946,42 @@ private:
     nsCOMPtr<nsIPrincipal> mParentCharsetPrincipal;
     nsTObserverArray<nsWeakPtr> mPrivacyObservers;
     nsTObserverArray<nsWeakPtr> mReflowObservers;
     nsTObserverArray<nsWeakPtr> mScrollObservers;
     nsCString         mOriginalUriString;
     nsWeakPtr mOpener;
     nsWeakPtr mOpenedRemote;
 
-    // Storing profile timeline markers and if/when recording started
-    mozilla::TimeStamp mProfileTimelineStartTime;
+    // True if recording profiles.
+    bool mProfileTimelineRecording;
 
 #ifdef MOZ_ENABLE_PROFILER_SPS
     struct InternalProfileTimelineMarker
     {
       InternalProfileTimelineMarker(const char* aName,
                                     ProfilerMarkerTracing* aPayload,
-                                    float aTime)
+                                    DOMHighResTimeStamp aTime)
         : mName(aName)
         , mPayload(aPayload)
         , mTime(aTime)
       {}
 
       ~InternalProfileTimelineMarker()
       {
         delete mPayload;
       }
 
       const char* mName;
       ProfilerMarkerTracing* mPayload;
-      float mTime;
+      DOMHighResTimeStamp mTime;
     };
     nsTArray<InternalProfileTimelineMarker*> mProfileTimelineMarkers;
 #endif
 
-    // Get the elapsed time (in millis) since the profile timeline recording
-    // started
-    float GetProfileTimelineDelta();
-
     // Get rid of all the timeline markers accumulated so far
     void ClearProfileTimelineMarkers();
 
     // Separate function to do the actual name (i.e. not _top, _self etc.)
     // searching for FindItemWithName.
     nsresult DoFindItemWithName(const char16_t* aName,
                                 nsISupports* aRequestor,
                                 nsIDocShellTreeItem* aOriginalRequestor,
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -49,17 +49,17 @@ interface nsIWebBrowserPrint;
 interface nsIVariant;
 interface nsIPrivacyTransitionObserver;
 interface nsIReflowObserver;
 interface nsIScrollObserver;
 interface nsITabParent;
  
 typedef unsigned long nsLoadFlags;
 
-[scriptable, builtinclass, uuid(23157a63-26fd-44a0-a0f9-fdc64dcc004c)]
+[scriptable, builtinclass, uuid(da8f78f1-8f20-4d6d-be56-fe53e177b630)]
 interface nsIDocShell : nsIDocShellTreeItem
 {
   /**
    * Loads a given URI.  This will give priority to loading the requested URI
    * in the object implementing	this interface.  If it can't be loaded here
    * however, the URL dispatcher will go through its normal process of content
    * loading.
    *
@@ -668,16 +668,24 @@ interface nsIDocShell : nsIDocShellTreeI
     out nsIPrincipal parentCharsetPrincipal);
 
   /**
    * Whether the docShell records profile timeline markers at the moment
    */
   [infallible] attribute boolean recordProfileTimelineMarkers;
 
   /**
+   * Return a DOMHighResTimeStamp representing the number of
+   * milliseconds from an arbitrary point in time.  The reference
+   * point is shared by all DocShells and is also used by timestamps
+   * on markers.
+   */
+  DOMHighResTimeStamp now();
+
+  /**
    * Returns and flushes the profile timeline markers gathered by the docShell
    */
   [implicit_jscontext]
   jsval popProfileTimelineMarkers();
 
   /**
    * Add an observer to the list of parties to be notified when this docshell's
    * private browsing status is changed. |obs| must support weak references.
--- a/dom/webidl/ProfileTimelineMarker.webidl
+++ b/dom/webidl/ProfileTimelineMarker.webidl
@@ -1,11 +1,11 @@
 /* -*- 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 ProfileTimelineMarker {
   DOMString name = "";
-  DOMTimeStamp start = 0;
-  DOMTimeStamp end = 0;
+  DOMHighResTimeStamp start = 0;
+  DOMHighResTimeStamp end = 0;
 };
--- a/toolkit/devtools/server/actors/timeline.js
+++ b/toolkit/devtools/server/actors/timeline.js
@@ -46,17 +46,18 @@ let TimelineActor = exports.TimelineActo
      * at most, when profile markers are found. A marker has the following
      * properties:
      * - start {Number} ms
      * - end {Number} ms
      * - name {String}
      */
     "markers" : {
       type: "markers",
-      markers: Arg(0, "array:json")
+      markers: Arg(0, "array:json"),
+      endTime: Arg(1, "number")
     },
 
     /**
      * "memory" events emitted in tandem with "markers", if this was enabled
      * when the recording started.
      */
     "memory" : {
       type: "memory",
@@ -75,16 +76,17 @@ let TimelineActor = exports.TimelineActo
     }
   },
 
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
 
     this._isRecording = false;
+    this._startTime = 0;
 
     // Make sure to get markers from new windows as they become available
     this._onWindowReady = this._onWindowReady.bind(this);
     events.on(this.tabActor, "window-ready", this._onWindowReady);
   },
 
   /**
    * The timeline actor is the first (and last) in its hierarchy to use protocol.js
@@ -137,17 +139,18 @@ let TimelineActor = exports.TimelineActo
       return;
     }
 
     let markers = [];
     for (let docShell of this.docShells) {
       markers = [...markers, ...docShell.popProfileTimelineMarkers()];
     }
     if (markers.length > 0) {
-      events.emit(this, "markers", markers);
+      let endTime = this.docShells[0].now();
+      events.emit(this, "markers", markers, endTime);
     }
     if (this._memoryActor) {
       events.emit(this, "memory", Date.now(), this._memoryActor.measure());
     }
     if (this._framerateActor) {
       events.emit(this, "ticks", Date.now(), this._framerateActor.getPendingTicks());
     }
 
@@ -171,35 +174,40 @@ let TimelineActor = exports.TimelineActo
   /**
    * Start recording profile markers.
    */
   start: method(function({ withMemory, withTicks }) {
     if (this._isRecording) {
       return;
     }
     this._isRecording = true;
+    this._startTime = this.docShells[0].now();
 
     for (let docShell of this.docShells) {
       docShell.recordProfileTimelineMarkers = true;
     }
 
     if (withMemory) {
       this._memoryActor = new MemoryActor(this.conn, this.tabActor);
       events.emit(this, "memory", Date.now(), this._memoryActor.measure());
     }
     if (withTicks) {
       this._framerateActor = new FramerateActor(this.conn, this.tabActor);
       this._framerateActor.startRecording();
     }
 
     this._pullTimelineData();
+    return this._startTime;
   }, {
     request: {
       withMemory: Option(0, "boolean"),
       withTicks: Option(0, "boolean")
+    },
+    response: {
+      value: RetVal("number")
     }
   }),
 
   /**
    * Stop recording profile markers.
    */
   stop: method(function() {
     if (!this._isRecording) {
@@ -223,18 +231,16 @@ let TimelineActor = exports.TimelineActo
   }, {}),
 
   /**
    * When a new window becomes available in the tabActor, start recording its
    * markers if we were recording.
    */
   _onWindowReady: function({window}) {
     if (this._isRecording) {
-      // XXX As long as bug 1070089 isn't fixed, each docShell has its own start
-      // recording time, so markers aren't going to be properly ordered.
       let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIWebNavigation)
                            .QueryInterface(Ci.nsIDocShell);
       docShell.recordProfileTimelineMarkers = true;
     }
   }
 });