Bug 1070089 - change timeline markers to have a consistent epoch (carrying forward r=smaug)
--- 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(¤tValue);
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;
}
}
});