Bug 1299118: non-spec TimeToFirstInteractive implementation behind a pref r=mstange,bholley
authorRandell Jesup <rjesup@jesup.org>
Thu, 11 Oct 2018 13:23:28 -0400
changeset 496549 fa3cf6dbf4667a469fb327627217a122c22390f5
parent 496548 9f83ebaabaa324623b5a925df2317c7530adf48b
child 496550 69dd27f16b71cdd416076de8b6ff37d55505139d
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange, bholley
bugs1299118
milestone64.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 1299118: non-spec TimeToFirstInteractive implementation behind a pref r=mstange,bholley
dom/base/nsDOMNavigationTiming.cpp
dom/base/nsDOMNavigationTiming.h
dom/performance/PerformanceTiming.h
dom/webidl/PerformanceTiming.webidl
modules/libpref/init/all.js
--- a/dom/base/nsDOMNavigationTiming.cpp
+++ b/dom/base/nsDOMNavigationTiming.cpp
@@ -14,16 +14,17 @@
 #include "nsIDocShellTreeItem.h"
 #include "nsIScriptSecurityManager.h"
 #include "prtime.h"
 #include "nsIURI.h"
 #include "nsPrintfCString.h"
 #include "mozilla/dom/PerformanceNavigation.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Telemetry.h"
+#include "ProfilerMarkerPayload.h"
 
 using namespace mozilla;
 
 nsDOMNavigationTiming::nsDOMNavigationTiming(nsDocShell* aDocShell)
 {
   Clear();
 
   mDocShell = aDocShell;
@@ -248,16 +249,123 @@ nsDOMNavigationTiming::NotifyDOMContentL
   PROFILER_TRACING("Navigation", "DOMContentLoaded", TRACING_INTERVAL_END);
 
   if (IsTopLevelContentDocumentInContentProcess()) {
     Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_DOM_CONTENT_LOADED_END_MS,
                                    mNavigationStart);
   }
 }
 
+// static
+void
+nsDOMNavigationTiming::TTITimeoutCallback(nsITimer* aTimer, void *aClosure)
+{
+  nsDOMNavigationTiming* self = static_cast<nsDOMNavigationTiming*>(aClosure);
+  self->TTITimeout(aTimer);
+}
+
+// Return the max of aT1 and aT2, or the lower of the two if there's more
+// than Nms (the window size) between them.  In other words, the window
+// starts at the lower of aT1 and aT2, and we only want to respect
+// timestamps within the window (and pick the max of those).
+//
+// This approach handles the edge case of a late wakeup: where there was
+// more than Nms after one (of aT1 or aT2) without the other, but the other
+// happened after Nms and before we woke up.  For example, if aT1 was 10
+// seconds after aT2, but we woke up late (after aT1) we don't want to
+// return aT1 if the window is 5 seconds.
+static const TimeStamp&
+MaxWithinWindowBeginningAtMin(const TimeStamp& aT1, const TimeStamp& aT2,
+                              const TimeDuration& aWindowSize)
+{
+  if (aT2.IsNull()) {
+    return aT1;
+  } else if (aT1.IsNull()) {
+    return aT2;
+  }
+  if (aT1 > aT2) {
+    if ((aT1 - aT2) > aWindowSize) {
+      return aT2;
+    }
+    return aT1;
+  }
+  if ((aT2 - aT1) > aWindowSize) {
+    return aT1;
+  }
+  return aT2;
+}
+
+#define TTI_WINDOW_SIZE_MS (5 * 1000)
+
+void
+nsDOMNavigationTiming::TTITimeout(nsITimer* aTimer)
+{
+  // Check TTI: see if it's been 5 seconds since the last Long Task
+  TimeStamp now = TimeStamp::Now();
+  MOZ_RELEASE_ASSERT(!mNonBlankPaint.IsNull(), "TTI timeout with no non-blank-paint?");
+
+  nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+  TimeStamp lastLongTaskEnded;
+  mainThread->GetLastLongNonIdleTaskEnd(&lastLongTaskEnded);
+  if (!lastLongTaskEnded.IsNull()) {
+    TimeDuration delta = now - lastLongTaskEnded;
+    if (delta.ToMilliseconds() < TTI_WINDOW_SIZE_MS) {
+      // Less than 5 seconds since the last long task.  Schedule another check
+      aTimer->InitWithNamedFuncCallback(TTITimeoutCallback, this, TTI_WINDOW_SIZE_MS,
+                                        nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+                                         "nsDOMNavigationTiming::TTITimeout");
+      return;
+    }
+  }
+  // To correctly implement TTI/TTFI as proposed, we'd need to use
+  // FirstContentfulPaint (FCP, which we have not yet implemented) instead
+  // of FirstNonBlankPaing (FNBP) to start at, and not fire it until there
+  // are no more than 2 network loads.  By the proposed definition, without
+  // that we're closer to TimeToFirstInteractive.
+
+  // XXX check number of network loads, and if > 2 mark to check if loads
+  // decreases to 2 (or record that point and let the normal timer here
+  // handle it)
+
+  // TTI has occurred!  TTI is either FCP (if there are no longtasks and no
+  // DCLEnd in the window that starts at FCP), or at the end of the last
+  // Long Task or DOMContentLoadedEnd (whichever is later).
+
+  if (mTTFI.IsNull()) {
+    mTTFI = MaxWithinWindowBeginningAtMin(lastLongTaskEnded, mDOMContentLoadedEventEnd,
+                                          TimeDuration::FromMilliseconds(TTI_WINDOW_SIZE_MS));
+    if (mTTFI.IsNull()) {
+      mTTFI = mNonBlankPaint;
+    }
+  }
+  // XXX Implement TTI via check number of network loads, and if > 2 mark
+  // to check if loads decreases to 2 (or record that point and let the
+  // normal timer here handle it)
+
+  mTTITimer = nullptr;
+
+#ifdef MOZ_GECKO_PROFILER
+  if (profiler_is_active()) {
+    TimeDuration elapsed = mTTFI - mNavigationStart;
+    TimeDuration elapsedLongTask = lastLongTaskEnded.IsNull() ? 0 : lastLongTaskEnded - mNavigationStart;
+    nsAutoCString spec;
+    if (mLoadedURI) {
+      mLoadedURI->GetSpec(spec);
+    }
+    nsPrintfCString marker("TTFI after %dms (LongTask after %dms) for URL %s",
+                           int(elapsed.ToMilliseconds()),
+                           int(elapsedLongTask.ToMilliseconds()),spec.get());
+
+    profiler_add_marker(
+      "TTI", MakeUnique<UserTimingMarkerPayload>(NS_ConvertASCIItoUTF16(marker), mTTFI));
+  }
+#endif
+  return;
+}
+
 void
 nsDOMNavigationTiming::NotifyNonBlankPaintForRootContentDocument()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mNavigationStart.IsNull());
 
   if (!mNonBlankPaint.IsNull()) {
     return;
@@ -274,16 +382,25 @@ nsDOMNavigationTiming::NotifyNonBlankPai
     }
     nsPrintfCString marker("Non-blank paint after %dms for URL %s, %s",
                            int(elapsed.ToMilliseconds()), spec.get(),
                            mDocShellHasBeenActiveSinceNavigationStart ? "foreground tab" : "this tab was inactive some of the time between navigation start and first non-blank paint");
     profiler_add_marker(marker.get());
   }
 #endif
 
+  if (!mTTITimer) {
+    mTTITimer = NS_NewTimer();
+  }
+
+  // TTI is first checked 5 seconds after the FCP (non-blank-paint is very close to FCP).
+  mTTITimer->InitWithNamedFuncCallback(TTITimeoutCallback, this, TTI_WINDOW_SIZE_MS,
+                                       nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+                                       "nsDOMNavigationTiming::TTITimeout");
+
   if (mDocShellHasBeenActiveSinceNavigationStart) {
     if (net::nsHttp::IsBeforeLastActiveTabLoadOptimization(mNavigationStart)) {
       Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_NON_BLANK_PAINT_NETOPT_MS,
                                      mNavigationStart,
                                      mNonBlankPaint);
     } else {
       Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_NON_BLANK_PAINT_NO_NETOPT_MS,
                                      mNavigationStart,
--- a/dom/base/nsDOMNavigationTiming.h
+++ b/dom/base/nsDOMNavigationTiming.h
@@ -7,16 +7,17 @@
 #ifndef nsDOMNavigationTiming_h___
 #define nsDOMNavigationTiming_h___
 
 #include "nsCOMPtr.h"
 #include "nsCOMArray.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/RelativeTimeline.h"
 #include "mozilla/TimeStamp.h"
+#include "nsITimer.h"
 
 class nsDocShell;
 class nsIURI;
 
 typedef unsigned long long DOMTimeMilliSec;
 typedef double DOMHighResTimeStamp;
 
 class nsDOMNavigationTiming final
@@ -91,16 +92,20 @@ public:
   DOMTimeMilliSec GetLoadEventEnd() const
   {
     return TimeStampToDOM(mLoadEventEnd);
   }
   DOMTimeMilliSec GetTimeToNonBlankPaint() const
   {
     return TimeStampToDOM(mNonBlankPaint);
   }
+  DOMTimeMilliSec GetTimeToTTFI() const
+  {
+    return TimeStampToDOM(mTTFI);
+  }
   DOMTimeMilliSec GetTimeToDOMContentFlushed() const
   {
     return TimeStampToDOM(mDOMContentFlushed);
   }
 
   DOMHighResTimeStamp GetUnloadEventStartHighRes()
   {
     mozilla::TimeStamp stamp = GetUnloadEventStartTimeStamp();
@@ -159,16 +164,20 @@ public:
   // Document changes state to 'loading' before connecting to timing
   void SetDOMLoadingTimeStamp(nsIURI* aURI, mozilla::TimeStamp aValue);
   void NotifyDOMLoading(nsIURI* aURI);
   void NotifyDOMInteractive(nsIURI* aURI);
   void NotifyDOMComplete(nsIURI* aURI);
   void NotifyDOMContentLoadedStart(nsIURI* aURI);
   void NotifyDOMContentLoadedEnd(nsIURI* aURI);
 
+  static void TTITimeoutCallback(nsITimer* aTimer, void *aClosure);
+  void TTITimeout(nsITimer* aTimer);
+
+  void NotifyLongTask(mozilla::TimeStamp aWhen);
   void NotifyNonBlankPaintForRootContentDocument();
   void NotifyDOMContentFlushedForRootContentDocument();
   void NotifyDocShellStateChanged(DocShellState aDocShellState);
 
   DOMTimeMilliSec TimeStampToDOM(mozilla::TimeStamp aStamp) const;
 
   inline DOMHighResTimeStamp TimeStampToDOMHighRes(mozilla::TimeStamp aStamp) const
   {
@@ -189,16 +198,17 @@ private:
   mozilla::TimeStamp GetUnloadEventEndTimeStamp() const;
 
   bool IsTopLevelContentDocumentInContentProcess() const;
 
   mozilla::WeakPtr<nsDocShell> mDocShell;
 
   nsCOMPtr<nsIURI> mUnloadedURI;
   nsCOMPtr<nsIURI> mLoadedURI;
+  nsCOMPtr<nsITimer> mTTITimer;
 
   Type mNavigationType;
   DOMHighResTimeStamp mNavigationStartHighRes;
   mozilla::TimeStamp mNavigationStart;
   mozilla::TimeStamp mNonBlankPaint;
   mozilla::TimeStamp mDOMContentFlushed;
 
   mozilla::TimeStamp mBeforeUnloadStart;
@@ -208,12 +218,14 @@ private:
   mozilla::TimeStamp mLoadEventEnd;
 
   mozilla::TimeStamp mDOMLoading;
   mozilla::TimeStamp mDOMInteractive;
   mozilla::TimeStamp mDOMContentLoadedEventStart;
   mozilla::TimeStamp mDOMContentLoadedEventEnd;
   mozilla::TimeStamp mDOMComplete;
 
+  mozilla::TimeStamp mTTFI;
+
   bool mDocShellHasBeenActiveSinceNavigationStart : 1;
 };
 
 #endif /* nsDOMNavigationTiming_h___ */
--- a/dom/performance/PerformanceTiming.h
+++ b/dom/performance/PerformanceTiming.h
@@ -449,16 +449,30 @@ public:
     if (mPerformance->IsSystemPrincipal()) {
       return GetDOMTiming()->GetTimeToDOMContentFlushed();
     }
     return nsRFPService::ReduceTimePrecisionAsMSecs(
       GetDOMTiming()->GetTimeToDOMContentFlushed(),
       mPerformance->GetRandomTimelineSeed());
   }
 
+  DOMTimeMilliSec TimeToFirstInteractive() const
+  {
+    if (!nsContentUtils::IsPerformanceTimingEnabled() ||
+        nsContentUtils::ShouldResistFingerprinting()) {
+      return 0;
+    }
+    if (mPerformance->IsSystemPrincipal()) {
+      return GetDOMTiming()->GetTimeToTTFI();
+    }
+    return nsRFPService::ReduceTimePrecisionAsMSecs(
+      GetDOMTiming()->GetTimeToTTFI(),
+      mPerformance->GetRandomTimelineSeed());
+  }
+
   PerformanceTimingData* Data() const
   {
     return mTimingData.get();
   }
 
 private:
   ~PerformanceTiming();
 
--- a/dom/webidl/PerformanceTiming.webidl
+++ b/dom/webidl/PerformanceTiming.webidl
@@ -40,10 +40,16 @@ interface PerformanceTiming {
   readonly attribute unsigned long long timeToNonBlankPaint;
 
   // This is a Mozilla proprietary extension and not part of the
   // performance/navigation timing specification. It marks the
   // completion of the first presentation flush after DOMContentLoaded.
   [Pref="dom.performance.time_to_dom_content_flushed.enabled"]
   readonly attribute unsigned long long timeToDOMContentFlushed;
 
+  // This is a Chrome proprietary extension and not part of the
+  // performance/navigation timing specification.
+  // Returns 0 if a time-to-interactive measurement has not happened.
+  [Pref="dom.performance.time_to_first_interactive.enabled"]
+  readonly attribute unsigned long long timeToFirstInteractive;
+
   [Default] object toJSON();
 };
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -177,16 +177,19 @@ pref("dom.performance.enable_scheduler_t
 pref("dom.permissions.revoke.enable", false);
 
 // Enable exposing timeToNonBlankPaint
 pref("dom.performance.time_to_non_blank_paint.enabled", false);
 
 // Enable exposing timeToDOMContentFlushed
 pref("dom.performance.time_to_dom_content_flushed.enabled", false);
 
+// Enable exposing timeToInteractive
+pref("dom.performance.time_to_interactive.enabled", false);
+
 // Enable requestIdleCallback API
 pref("dom.requestIdleCallback.enabled", true);
 
 // Enable Pointer Lock API
 // This is added for accessibility purpose. When user has no way to exit
 // pointer lock (e.g. no keyboard available), they can use this pref to
 // disable the Pointer Lock API altogether.
 pref("dom.pointer-lock.enabled", true);