Bug 1298381: Implement TimeToFirstContentfulPaint behind a pref r=mattwoodrow,smaug
authorRandell Jesup <rjesup@jesup.org>
Thu, 18 Oct 2018 16:16:45 -0400
changeset 502970 a10cbfd5f4110f2e5f96095408aebd1f8acd1b87
parent 502969 73de6b13685597c94484baa9177c65d4316d05c7
child 502971 dca9c72df68bb59692118595dea65b78cde5b371
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmattwoodrow, smaug
bugs1298381
milestone65.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 1298381: Implement TimeToFirstContentfulPaint behind a pref r=mattwoodrow,smaug
dom/base/nsDOMNavigationTiming.cpp
dom/base/nsDOMNavigationTiming.h
dom/html/HTMLCanvasElement.cpp
dom/html/HTMLCanvasElement.h
dom/performance/PerformanceTiming.h
dom/webidl/PerformanceTiming.webidl
layout/base/nsPresContext.cpp
layout/base/nsPresContext.h
layout/generic/nsHTMLCanvasFrame.cpp
layout/painting/nsDisplayItemTypes.h
layout/painting/nsDisplayItemTypesList.h
layout/painting/nsDisplayList.cpp
layout/painting/nsDisplayList.h
modules/libpref/init/all.js
--- a/dom/base/nsDOMNavigationTiming.cpp
+++ b/dom/base/nsDOMNavigationTiming.cpp
@@ -47,16 +47,18 @@ nsDOMNavigationTiming::Clear()
   mUnloadEnd = TimeStamp();
   mLoadEventStart = TimeStamp();
   mLoadEventEnd = TimeStamp();
   mDOMLoading = TimeStamp();
   mDOMInteractive = TimeStamp();
   mDOMContentLoadedEventStart = TimeStamp();
   mDOMContentLoadedEventEnd = TimeStamp();
   mDOMComplete = TimeStamp();
+  mContentfulPaint = TimeStamp();
+  mNonBlankPaint = TimeStamp();
 
   mDocShellHasBeenActiveSinceNavigationStart = false;
 }
 
 DOMTimeMilliSec
 nsDOMNavigationTiming::TimeStampToDOM(TimeStamp aStamp) const
 {
   if (aStamp.IsNull()) {
@@ -303,50 +305,49 @@ MaxWithinWindowBeginningAtMin(const Time
 
 #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?");
+  MOZ_RELEASE_ASSERT(!mContentfulPaint.IsNull(), "TTI timeout with no contentful-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.
+  // To correctly implement TTI/TTFI as proposed, we'd need to 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;
+      mTTFI = mContentfulPaint;
     }
   }
   // 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;
 
@@ -394,25 +395,16 @@ 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,
@@ -421,16 +413,52 @@ nsDOMNavigationTiming::NotifyNonBlankPai
 
     Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_NON_BLANK_PAINT_MS,
                                    mNavigationStart,
                                    mNonBlankPaint);
   }
 }
 
 void
+nsDOMNavigationTiming::NotifyContentfulPaintForRootContentDocument()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!mNavigationStart.IsNull());
+
+  if (!mContentfulPaint.IsNull()) {
+    return;
+  }
+
+  mContentfulPaint = TimeStamp::Now();
+
+#ifdef MOZ_GECKO_PROFILER
+  if (profiler_is_active()) {
+    TimeDuration elapsed = mContentfulPaint - mNavigationStart;
+    nsAutoCString spec;
+    if (mLoadedURI) {
+      mLoadedURI->GetSpec(spec);
+    }
+    nsPrintfCString marker("Contentful 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");
+}
+
+void
 nsDOMNavigationTiming::NotifyDOMContentFlushedForRootContentDocument()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mNavigationStart.IsNull());
 
   if (!mDOMContentFlushed.IsNull()) {
     return;
   }
--- a/dom/base/nsDOMNavigationTiming.h
+++ b/dom/base/nsDOMNavigationTiming.h
@@ -92,16 +92,20 @@ public:
   DOMTimeMilliSec GetLoadEventEnd() const
   {
     return TimeStampToDOM(mLoadEventEnd);
   }
   DOMTimeMilliSec GetTimeToNonBlankPaint() const
   {
     return TimeStampToDOM(mNonBlankPaint);
   }
+  DOMTimeMilliSec GetTimeToContentfulPaint() const
+  {
+    return TimeStampToDOM(mContentfulPaint);
+  }
   DOMTimeMilliSec GetTimeToTTFI() const
   {
     return TimeStampToDOM(mTTFI);
   }
   DOMTimeMilliSec GetTimeToDOMContentFlushed() const
   {
     return TimeStampToDOM(mDOMContentFlushed);
   }
@@ -169,16 +173,17 @@ public:
   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 NotifyContentfulPaintForRootContentDocument();
   void NotifyDOMContentFlushedForRootContentDocument();
   void NotifyDocShellStateChanged(DocShellState aDocShellState);
 
   DOMTimeMilliSec TimeStampToDOM(mozilla::TimeStamp aStamp) const;
 
   inline DOMHighResTimeStamp TimeStampToDOMHighRes(mozilla::TimeStamp aStamp) const
   {
     if (aStamp.IsNull()) {
@@ -204,16 +209,17 @@ private:
   nsCOMPtr<nsIURI> mUnloadedURI;
   nsCOMPtr<nsIURI> mLoadedURI;
   nsCOMPtr<nsITimer> mTTITimer;
 
   Type mNavigationType;
   DOMHighResTimeStamp mNavigationStartHighRes;
   mozilla::TimeStamp mNavigationStart;
   mozilla::TimeStamp mNonBlankPaint;
+  mozilla::TimeStamp mContentfulPaint;
   mozilla::TimeStamp mDOMContentFlushed;
 
   mozilla::TimeStamp mBeforeUnloadStart;
   mozilla::TimeStamp mUnloadStart;
   mozilla::TimeStamp mUnloadEnd;
   mozilla::TimeStamp mLoadEventStart;
   mozilla::TimeStamp mLoadEventEnd;
 
--- a/dom/html/HTMLCanvasElement.cpp
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -391,16 +391,17 @@ HTMLCanvasElementObserver::HandleEvent(E
 
 NS_IMPL_ISUPPORTS(HTMLCanvasElementObserver, nsIObserver)
 
 // ---------------------------------------------------------------------------
 
 HTMLCanvasElement::HTMLCanvasElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
   : nsGenericHTMLElement(std::move(aNodeInfo)),
     mResetLayer(true) ,
+    mMaybeModified(false) ,
     mWriteOnly(false)
 {}
 
 HTMLCanvasElement::~HTMLCanvasElement()
 {
   if (mContextObserver) {
     mContextObserver->Destroy();
     mContextObserver = nullptr;
@@ -1005,30 +1006,32 @@ HTMLCanvasElement::MozGetAsFileImpl(cons
   return NS_OK;
 }
 
 nsresult
 HTMLCanvasElement::GetContext(const nsAString& aContextId,
                               nsISupports** aContext)
 {
   ErrorResult rv;
+  mMaybeModified = true; // For FirstContentfulPaint
   *aContext = GetContext(nullptr, aContextId, JS::NullHandleValue, rv).take();
   return rv.StealNSResult();
 }
 
 already_AddRefed<nsISupports>
 HTMLCanvasElement::GetContext(JSContext* aCx,
                               const nsAString& aContextId,
                               JS::Handle<JS::Value> aContextOptions,
                               ErrorResult& aRv)
 {
   if (mOffscreenCanvas) {
     return nullptr;
   }
 
+  mMaybeModified = true; // For FirstContentfulPaint
   return CanvasRenderingContextHelper::GetContext(aCx, aContextId,
     aContextOptions.isObject() ? aContextOptions : JS::NullHandleValue,
     aRv);
 }
 
 already_AddRefed<nsISupports>
 HTMLCanvasElement::MozGetIPCContext(const nsAString& aContextId,
                                     ErrorResult& aRv)
--- a/dom/html/HTMLCanvasElement.h
+++ b/dom/html/HTMLCanvasElement.h
@@ -344,16 +344,18 @@ public:
 
   void OnMemoryPressure();
 
   static void SetAttrFromAsyncCanvasRenderer(AsyncCanvasRenderer *aRenderer);
   static void InvalidateFromAsyncCanvasRenderer(AsyncCanvasRenderer *aRenderer);
 
   already_AddRefed<layers::SharedSurfaceTextureClient> GetVRFrame();
 
+  bool MaybeModified() const { return mMaybeModified; };
+
 protected:
   virtual ~HTMLCanvasElement();
 
   virtual JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   virtual nsIntSize GetWidthHeight() override;
 
   virtual already_AddRefed<nsICanvasRenderingContextInternal>
@@ -382,16 +384,17 @@ protected:
                                 bool aNotify) override;
   virtual nsresult OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
                                           const nsAttrValueOrString& aValue,
                                           bool aNotify) override;
 
   AsyncCanvasRenderer* GetAsyncCanvasRenderer();
 
   bool mResetLayer;
+  bool mMaybeModified; // we fetched the context, so we may have written to the canvas
   RefPtr<HTMLCanvasElement> mOriginalCanvas;
   RefPtr<PrintCallback> mPrintCallback;
   RefPtr<HTMLCanvasPrintState> mPrintState;
   nsTArray<WeakPtr<FrameCaptureListener>> mRequestedFrameListeners;
   RefPtr<RequestedFrameRefreshObserver> mRequestedFrameRefreshObserver;
   RefPtr<AsyncCanvasRenderer> mAsyncCanvasRenderer;
   RefPtr<OffscreenCanvas> mOffscreenCanvas;
   RefPtr<HTMLCanvasElementObserver> mContextObserver;
--- a/dom/performance/PerformanceTiming.h
+++ b/dom/performance/PerformanceTiming.h
@@ -435,16 +435,30 @@ public:
     if (mPerformance->IsSystemPrincipal()) {
       return GetDOMTiming()->GetTimeToNonBlankPaint();
     }
     return nsRFPService::ReduceTimePrecisionAsMSecs(
       GetDOMTiming()->GetTimeToNonBlankPaint(),
       mPerformance->GetRandomTimelineSeed());
   }
 
+  DOMTimeMilliSec TimeToContentfulPaint() const
+  {
+    if (!nsContentUtils::IsPerformanceTimingEnabled() ||
+        nsContentUtils::ShouldResistFingerprinting()) {
+      return 0;
+    }
+    if (mPerformance->IsSystemPrincipal()) {
+      return GetDOMTiming()->GetTimeToContentfulPaint();
+    }
+    return nsRFPService::ReduceTimePrecisionAsMSecs(
+      GetDOMTiming()->GetTimeToContentfulPaint(),
+      mPerformance->GetRandomTimelineSeed());
+  }
+
   DOMTimeMilliSec TimeToDOMContentFlushed() const
   {
     if (!nsContentUtils::IsPerformanceTimingEnabled() ||
         nsContentUtils::ShouldResistFingerprinting()) {
       return 0;
     }
     if (mPerformance->IsSystemPrincipal()) {
       return GetDOMTiming()->GetTimeToDOMContentFlushed();
--- a/dom/webidl/PerformanceTiming.webidl
+++ b/dom/webidl/PerformanceTiming.webidl
@@ -34,16 +34,20 @@ interface PerformanceTiming {
   readonly attribute unsigned long long loadEventEnd;
 
   // This is a Chrome proprietary extension and not part of the
   // performance/navigation timing specification.
   // Returns 0 if a non-blank paint has not happened.
   [Pref="dom.performance.time_to_non_blank_paint.enabled"]
   readonly attribute unsigned long long timeToNonBlankPaint;
 
+  // Returns 0 if a contentful paint has not happened.
+  [Pref="dom.performance.time_to_contentful_paint.enabled"]
+  readonly attribute unsigned long long timeToContentfulPaint;
+
   // 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.
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -246,17 +246,18 @@ nsPresContext::nsPresContext(nsIDocument
     mIsChrome(false),
     mIsChromeOriginImage(false),
     mPaintFlashing(false),
     mPaintFlashingInitialized(false),
     mHasWarnedAboutPositionedTableParts(false),
     mHasWarnedAboutTooLargeDashedOrDottedRadius(false),
     mQuirkSheetAdded(false),
     mNeedsPrefUpdate(false),
-    mHadNonBlankPaint(false)
+    mHadNonBlankPaint(false),
+    mHadContentfulPaint(false)
 #ifdef DEBUG
     , mInitialized(false)
 #endif
 {
   PodZero(&mBorderWidthTable);
 #ifdef DEBUG
   PodZero(&mLayoutPhaseCount);
 #endif
@@ -2888,16 +2889,32 @@ nsPresContext::NotifyNonBlankPaint()
       timing->NotifyNonBlankPaintForRootContentDocument();
     }
 
     mFirstNonBlankPaintTime = TimeStamp::Now();
   }
 }
 
 void
+nsPresContext::NotifyContentfulPaint()
+{
+  if (!mHadContentfulPaint) {
+    mHadContentfulPaint = true;
+    if (IsRootContentDocument()) {
+      RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
+      if (timing) {
+        timing->NotifyContentfulPaintForRootContentDocument();
+      }
+
+      mFirstContentfulPaintTime = TimeStamp::Now();
+    }
+  }
+}
+
+void
 nsPresContext::NotifyDOMContentFlushed()
 {
   NS_ENSURE_TRUE_VOID(mShell);
   if (IsRootContentDocument()) {
     RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
     if (timing) {
       timing->NotifyDOMContentFlushedForRootContentDocument();
     }
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -1115,18 +1115,21 @@ public:
     return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
   }
 
   bool IsRootContentDocument() const;
 
   bool HadNonBlankPaint() const {
     return mHadNonBlankPaint;
   }
-
+  bool HadContentfulPaint() const {
+    return mHadContentfulPaint;
+  }
   void NotifyNonBlankPaint();
+  void NotifyContentfulPaint();
   void NotifyDOMContentFlushed();
 
   bool UsesRootEMUnits() const {
     return mUsesRootEMUnits;
   }
 
   void SetUsesRootEMUnits(bool aValue) {
     mUsesRootEMUnits = aValue;
@@ -1393,16 +1396,17 @@ protected:
   uint64_t              mFramesConstructed;
   uint64_t              mFramesReflowed;
 
   mozilla::TimeStamp    mReflowStartTime;
 
   // Time of various first interaction types, used to report time from
   // first paint of the top level content pres shell to first interaction.
   mozilla::TimeStamp    mFirstNonBlankPaintTime;
+  mozilla::TimeStamp    mFirstContentfulPaintTime;
   mozilla::TimeStamp    mFirstClickTime;
   mozilla::TimeStamp    mFirstKeyTime;
   mozilla::TimeStamp    mFirstMouseMoveTime;
   mozilla::TimeStamp    mFirstScrollTime;
   bool                  mInteractionTimeEnabled;
 
   // last time we did a full style flush
   mozilla::TimeStamp    mLastStyleUpdateForAllAnimations;
@@ -1469,16 +1473,18 @@ protected:
   // Have we added quirk.css to the style set?
   unsigned              mQuirkSheetAdded : 1;
 
   // Is there a pref update to process once we have a container?
   unsigned              mNeedsPrefUpdate : 1;
 
   // Has NotifyNonBlankPaint been called on this PresContext?
   unsigned              mHadNonBlankPaint : 1;
+  // Has NotifyContentfulPaint been called on this PresContext?
+  unsigned              mHadContentfulPaint : 1;
 
 #ifdef DEBUG
   unsigned mInitialized : 1;
 #endif
 
   mozilla::Maybe<mozilla::MediaFeatureChange> mPendingMediaFeatureValuesChange;
 
 protected:
--- a/layout/generic/nsHTMLCanvasFrame.cpp
+++ b/layout/generic/nsHTMLCanvasFrame.cpp
@@ -216,16 +216,26 @@ public:
 
     // If compositing is cheap, just do that
     if (aManager->IsCompositingCheap() ||
         ActiveLayerTracker::IsContentActive(mFrame))
       return mozilla::LAYER_ACTIVE;
 
     return LAYER_INACTIVE;
   }
+
+  // FirstContentfulPaint is supposed to ignore "white" canvases.  We use MaybeModified (if
+  // GetContext() was called on the canvas) as a standin for "white"
+  virtual bool IsContentful() const override
+  {
+    nsHTMLCanvasFrame* f = static_cast<nsHTMLCanvasFrame*>(Frame());
+    HTMLCanvasElement* canvas =
+      HTMLCanvasElement::FromNode(f->GetContent());
+    return canvas->MaybeModified();
+  }
 };
 
 
 nsIFrame*
 NS_NewHTMLCanvasFrame(nsIPresShell* aPresShell, ComputedStyle* aStyle)
 {
   return new (aPresShell) nsHTMLCanvasFrame(aStyle);
 }
--- a/layout/painting/nsDisplayItemTypes.h
+++ b/layout/painting/nsDisplayItemTypes.h
@@ -29,17 +29,18 @@ enum class DisplayItemType
 enum
 {
   // Number of bits needed to represent all types
   TYPE_BITS = 8
 };
 
 enum DisplayItemFlags
 {
-  TYPE_RENDERS_NO_IMAGES = 1 << 0
+  TYPE_RENDERS_NO_IMAGES = 1 << 0,
+  TYPE_IS_CONTENTFUL = 1 << 1
 };
 
 inline const char*
 DisplayItemTypeName(DisplayItemType aType)
 {
   switch (aType) {
 #define DECLARE_DISPLAY_ITEM_TYPE(name, flags)                                 \
   case DisplayItemType::TYPE_##name:                                           \
--- a/layout/painting/nsDisplayItemTypesList.h
+++ b/layout/painting/nsDisplayItemTypesList.h
@@ -1,49 +1,49 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 // IWYU pragma: private, include "nsDisplayList.h"
 DECLARE_DISPLAY_ITEM_TYPE(ALT_FEEDBACK, 0)
-DECLARE_DISPLAY_ITEM_TYPE(BACKGROUND, 0)
+DECLARE_DISPLAY_ITEM_TYPE(BACKGROUND, TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(THEMED_BACKGROUND, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(BACKGROUND_COLOR, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(BLEND_CONTAINER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_BLEND_CONTAINER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(BLEND_MODE, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_BLEND_MODE, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(BORDER, 0)
 DECLARE_DISPLAY_ITEM_TYPE(BOX_SHADOW_OUTER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(BOX_SHADOW_INNER, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(BULLET, 0)
-DECLARE_DISPLAY_ITEM_TYPE(BUTTON_BORDER_BACKGROUND, 0)
-DECLARE_DISPLAY_ITEM_TYPE(BUTTON_BOX_SHADOW_OUTER, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(BUTTON_FOREGROUND, 0)
+DECLARE_DISPLAY_ITEM_TYPE(BULLET, TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(BUTTON_BORDER_BACKGROUND, TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(BUTTON_BOX_SHADOW_OUTER, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(BUTTON_FOREGROUND, TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS_BACKGROUND_COLOR, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(CANVAS_THEMED_BACKGROUND, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(CANVAS_BACKGROUND_IMAGE, 0)
+DECLARE_DISPLAY_ITEM_TYPE(CANVAS_THEMED_BACKGROUND, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(CANVAS_BACKGROUND_IMAGE, TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(CANVAS_FOCUS, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(CARET, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(CHECKED_CHECKBOX, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(CHECKED_RADIOBUTTON, TYPE_RENDERS_NO_IMAGES)
+DECLARE_DISPLAY_ITEM_TYPE(CARET, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(CHECKED_CHECKBOX, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(CHECKED_RADIOBUTTON, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(CLEAR_BACKGROUND, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(COLUMN_RULE, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(COMBOBOX_FOCUS, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(COMPOSITOR_HITTEST_INFO, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(EVENT_RECEIVER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(FIELDSET_BORDER_BACKGROUND, 0)
 DECLARE_DISPLAY_ITEM_TYPE(FIXED_POSITION, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(STICKY_POSITION, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(FRAMESET_BORDER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(FRAMESET_BLANK, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(HEADER_FOOTER, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(IMAGE, 0)
+DECLARE_DISPLAY_ITEM_TYPE(IMAGE, TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(LIST_FOCUS, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(OPACITY, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(OPTION_EVENT_GRABBER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(OUTLINE, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(OWN_LAYER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(PLUGIN, 0)
 DECLARE_DISPLAY_ITEM_TYPE(PLUGIN_READBACK, 0)
 DECLARE_DISPLAY_ITEM_TYPE(PRINT_PLUGIN, 0)
@@ -53,33 +53,33 @@ DECLARE_DISPLAY_ITEM_TYPE(RESOLUTION, TY
 DECLARE_DISPLAY_ITEM_TYPE(SCROLL_INFO_LAYER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(SELECTION_OVERLAY, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(SOLID_COLOR, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(SOLID_COLOR_REGION, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(SUBDOCUMENT, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(MASK, 0)
 DECLARE_DISPLAY_ITEM_TYPE(FILTER, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(SVG_OUTER_SVG, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(SVG_GEOMETRY, 0)
-DECLARE_DISPLAY_ITEM_TYPE(SVG_TEXT, 0)
-DECLARE_DISPLAY_ITEM_TYPE(SVG_CHAR_CLIP, TYPE_RENDERS_NO_IMAGES)
+DECLARE_DISPLAY_ITEM_TYPE(SVG_GEOMETRY, TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(SVG_TEXT, TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(SVG_CHAR_CLIP, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(SVG_WRAPPER, 0)
-DECLARE_DISPLAY_ITEM_TYPE(FOREIGN_OBJECT, 0)
+DECLARE_DISPLAY_ITEM_TYPE(FOREIGN_OBJECT, TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_CELL_BACKGROUND, 0)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_CELL_SELECTION, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_BORDER_COLLAPSE, 0)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_BACKGROUND_COLOR, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(TABLE_BACKGROUND_IMAGE, 0)
-DECLARE_DISPLAY_ITEM_TYPE(TABLE_THEMED_BACKGROUND_IMAGE, 0)
+DECLARE_DISPLAY_ITEM_TYPE(TABLE_BACKGROUND_IMAGE, TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(TABLE_THEMED_BACKGROUND_IMAGE, TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(TABLE_FIXED_POSITION, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(TEXT, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(TEXT_OVERFLOW, TYPE_RENDERS_NO_IMAGES)
+DECLARE_DISPLAY_ITEM_TYPE(TEXT, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
+DECLARE_DISPLAY_ITEM_TYPE(TEXT_OVERFLOW, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(TRANSFORM, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(PERSPECTIVE, TYPE_RENDERS_NO_IMAGES)
-DECLARE_DISPLAY_ITEM_TYPE(VIDEO, TYPE_RENDERS_NO_IMAGES)
+DECLARE_DISPLAY_ITEM_TYPE(VIDEO, TYPE_RENDERS_NO_IMAGES | TYPE_IS_CONTENTFUL)
 DECLARE_DISPLAY_ITEM_TYPE(WRAP_LIST, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(ZOOM, TYPE_RENDERS_NO_IMAGES)
 DECLARE_DISPLAY_ITEM_TYPE(GENERIC, TYPE_RENDERS_NO_IMAGES)
 
 #if defined(MOZ_REFLOW_PERF_DSP) && defined(MOZ_REFLOW_PERF)
 DECLARE_DISPLAY_ITEM_TYPE(REFLOW_COUNT, TYPE_RENDERS_NO_IMAGES)
 #endif
 
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -14,16 +14,17 @@
 
 #include <stdint.h>
 #include <algorithm>
 #include <limits>
 
 #include "gfxContext.h"
 #include "gfxUtils.h"
 #include "mozilla/dom/TabChild.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
 #include "mozilla/dom/KeyframeEffect.h"
 #include "mozilla/dom/Selection.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/layers/PLayerTransaction.h"
 #include "mozilla/ShapeUtils.h"
 #include "nsCSSRendering.h"
 #include "nsCSSRenderingGradients.h"
 #include "nsISelectionController.h"
@@ -1442,32 +1443,72 @@ DisplayListIsNonBlank(nsDisplayList* aLi
         return true;
       default:
         return true;
     }
   }
   return false;
 }
 
+// A contentful paint is a paint that does contains DOM content (text,
+// images, non-blank canvases, SVG): "First Contentful Paint entry
+// contains a DOMHighResTimeStamp reporting the time when the browser
+// first rendered any text, image (including background images),
+// non-white canvas or SVG. This excludes any content of iframes, but
+// includes text with pending webfonts. This is the first time users
+// could start consuming page content."
+static bool
+DisplayListIsContentful(nsDisplayList* aList)
+{
+  for (nsDisplayItem* i = aList->GetBottom(); i != nullptr; i = i->GetAbove()) {
+    DisplayItemType type = i->GetType();
+    nsDisplayList* children = i->GetChildren();
+
+    switch (type) {
+      case DisplayItemType::TYPE_SUBDOCUMENT: // iframes are ignored
+        break;
+      // CANVASes check if they may have been modified (as a stand-in
+      // actually tracking all modifications)
+      default:
+        if (i->IsContentful()) {
+          return true;
+        }
+        if (children) {
+          if (DisplayListIsContentful(children)) {
+            return true;
+          }
+        }
+        break;
+    }
+  }
+  return false;
+}
+
 void
 nsDisplayListBuilder::LeavePresShell(nsIFrame* aReferenceFrame,
                                      nsDisplayList* aPaintedContents)
 {
   NS_ASSERTION(CurrentPresShellState()->mPresShell ==
                  aReferenceFrame->PresShell(),
                "Presshell mismatch");
 
-  if (mIsPaintingToWindow) {
+  if (mIsPaintingToWindow && aPaintedContents) {
     nsPresContext* pc = aReferenceFrame->PresContext();
     if (!pc->HadNonBlankPaint()) {
       if (!CurrentPresShellState()->mIsBackgroundOnly &&
           DisplayListIsNonBlank(aPaintedContents)) {
         pc->NotifyNonBlankPaint();
       }
     }
+    if (!pc->HadContentfulPaint()) {
+      if (!CurrentPresShellState()->mIsBackgroundOnly &&
+          DisplayListIsContentful(aPaintedContents)) {
+        pc->NotifyContentfulPaint();
+      }
+    }
   }
 
   ResetMarkedFramesForDisplayList(aReferenceFrame);
   mPresShellStates.SetLength(mPresShellStates.Length() - 1);
 
   if (!mPresShellStates.IsEmpty()) {
     nsPresContext* pc = CurrentPresContext();
     nsCOMPtr<nsIDocShell> docShell = pc->GetDocShell();
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -2406,17 +2406,19 @@ public:
   virtual DisplayItemType GetType() const = 0;
   /**
    * Pairing this with the GetUnderlyingFrame() pointer gives a key that
    * uniquely identifies this display item in the display item tree.
    * XXX check nsOptionEventGrabberWrapper/nsXULEventRedirectorWrapper
    */
   virtual uint32_t GetPerFrameKey() const { return uint32_t(GetType()); }
 
-  uint8_t GetFlags() { return GetDisplayItemFlagsForType(GetType()); }
+  uint8_t GetFlags() const { return GetDisplayItemFlagsForType(GetType()); }
+
+  virtual bool IsContentful() const { return GetFlags() & TYPE_IS_CONTENTFUL; }
 
   /**
    * This is called after we've constructed a display list for event handling.
    * When this is called, we've already ensured that aRect intersects the
    * item's bounds and that clipping has been taking into account.
    *
    * @param aRect the point or rect being tested, relative to the reference
    * frame. If the width and height are both 1 app unit, it indicates we're
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -174,16 +174,19 @@ pref("dom.performance.enable_notify_perf
 pref("dom.performance.enable_scheduler_timing", true);
 
 // Enable Permission API's .revoke() method
 pref("dom.permissions.revoke.enable", false);
 
 // Enable exposing timeToNonBlankPaint
 pref("dom.performance.time_to_non_blank_paint.enabled", false);
 
+// Enable exposing timeToContentfulPaint
+pref("dom.performance.time_to_contentful_paint.enabled", false);
+
 // Enable exposing timeToDOMContentFlushed
 pref("dom.performance.time_to_dom_content_flushed.enabled", false);
 
 // Enable exposing timeToFirstInteractive
 pref("dom.performance.time_to_first_interactive.enabled", false);
 
 // Enable requestIdleCallback API
 pref("dom.requestIdleCallback.enabled", true);