Bug 1299118: non-spec TimeToFirstInteractive implementation behind a pref r=mstange,bholley
☠☠ backed out by 6a40850883ff ☠ ☠
authorRandell Jesup <rjesup@jesup.org>
Thu, 11 Oct 2018 13:23:28 -0400
changeset 499252 e5adc30bdf7f3d5c10b7160836c477c4abf0f17d
parent 499251 8f7bb583fbb516aac8016b285a622b3c69d619ad
child 499253 14451eb9a2b8ca1549e4fe1f6ae877fde2cf9f20
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [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()) {
+    mTTI = 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);