From 61ec0bd3c32b965eaec5f8c58c0a627971a77679 Mon Sep 17 00:00:00 2001 From: Sean Feng Date: Thu, 27 Aug 2020 14:50:43 +0000 Subject: [PATCH] Bug 1518999 - Implement PerformancePaintTiming for FirstContentfulPaint r=smaug,mstange Spec: https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming We only support FirstContentfulPaint at the moment. Differential Revision: https://phabricator.services.mozilla.com/D66463 --- dom/performance/Performance.h | 3 + dom/performance/PerformanceMainThread.cpp | 30 +++++++- dom/performance/PerformanceMainThread.h | 2 + dom/performance/PerformanceObserver.cpp | 7 +- dom/performance/PerformancePaintTiming.cpp | 47 +++++++++++++ dom/performance/PerformancePaintTiming.h | 48 +++++++++++++ dom/performance/PerformanceWorker.h | 4 ++ dom/performance/moz.build | 2 + dom/performance/tests/logo.png | Bin 0 -> 21901 bytes dom/performance/tests/mochitest.ini | 8 +++ .../test_performance_paint_observer.html | 40 +++++++++++ ...est_performance_paint_observer_helper.html | 35 ++++++++++ .../tests/test_performance_paint_timing.html | 38 ++++++++++ .../test_performance_paint_timing_helper.html | 65 ++++++++++++++++++ .../mochitest/general/test_interfaces.js | 2 + dom/webidl/PerformancePaintTiming.webidl | 16 +++++ dom/webidl/moz.build | 1 + layout/base/nsPresContext.cpp | 21 ++++-- layout/base/nsPresContext.h | 6 +- layout/generic/nsTextFrame.cpp | 3 + layout/generic/nsVideoFrame.cpp | 7 ++ layout/painting/nsDisplayList.cpp | 19 +++-- layout/painting/nsDisplayList.h | 8 +++ 23 files changed, 393 insertions(+), 19 deletions(-) create mode 100644 dom/performance/PerformancePaintTiming.cpp create mode 100644 dom/performance/PerformancePaintTiming.h create mode 100644 dom/performance/tests/logo.png create mode 100644 dom/performance/tests/test_performance_paint_observer.html create mode 100644 dom/performance/tests/test_performance_paint_observer_helper.html create mode 100644 dom/performance/tests/test_performance_paint_timing.html create mode 100644 dom/performance/tests/test_performance_paint_timing_helper.html create mode 100644 dom/webidl/PerformancePaintTiming.webidl diff --git a/dom/performance/Performance.h b/dom/performance/Performance.h index 0aed510727ef..4ccb571d9e12 100644 --- a/dom/performance/Performance.h +++ b/dom/performance/Performance.h @@ -22,6 +22,7 @@ namespace dom { class PerformanceEntry; class PerformanceNavigation; +class PerformancePaintTiming; class PerformanceObserver; class PerformanceService; class PerformanceStorage; @@ -85,6 +86,8 @@ class Performance : public DOMEventTargetHelper { virtual PerformanceNavigation* Navigation() = 0; + virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) = 0; + IMPL_EVENT_HANDLER(resourcetimingbufferfull) virtual void GetMozMemory(JSContext* aCx, diff --git a/dom/performance/PerformanceMainThread.cpp b/dom/performance/PerformanceMainThread.cpp index ef673b2a6a95..08e48d17e369 100644 --- a/dom/performance/PerformanceMainThread.cpp +++ b/dom/performance/PerformanceMainThread.cpp @@ -6,6 +6,7 @@ #include "PerformanceMainThread.h" #include "PerformanceNavigation.h" +#include "PerformancePaintTiming.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_privacy.h" @@ -43,14 +44,14 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMainThread) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMainThread, Performance) - NS_IMPL_CYCLE_COLLECTION_UNLINK(mTiming, mNavigation, mDocEntry) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTiming, mNavigation, mDocEntry, mFCPTiming) tmp->mMozMemory = nullptr; mozilla::DropJSObjects(this); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMainThread, Performance) - NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTiming, mNavigation, mDocEntry) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTiming, mNavigation, mDocEntry, mFCPTiming) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMainThread, @@ -161,6 +162,14 @@ void PerformanceMainThread::AddRawEntry(UniquePtr aData, InsertResourceEntry(entry); } +void PerformanceMainThread::SetFCPTimingEntry(PerformancePaintTiming* aEntry) { + MOZ_ASSERT(aEntry); + if (!mFCPTiming) { + mFCPTiming = aEntry; + QueueEntry(aEntry); + } +} + // To be removed once bug 1124165 lands bool PerformanceMainThread::IsPerformanceTimingAttribute( const nsAString& aName) { @@ -376,6 +385,9 @@ void PerformanceMainThread::GetEntries( aRetval.AppendElement(mDocEntry); } + if (mFCPTiming) { + aRetval.AppendElement(mFCPTiming); + } aRetval.Sort(PerformanceEntryComparator()); } @@ -396,6 +408,13 @@ void PerformanceMainThread::GetEntriesByType( return; } + if (aEntryType.EqualsLiteral("paint")) { + if (mFCPTiming) { + aRetval.AppendElement(mFCPTiming); + return; + } + } + Performance::GetEntriesByType(aEntryType, aRetval); } @@ -410,6 +429,13 @@ void PerformanceMainThread::GetEntriesByName( Performance::GetEntriesByName(aName, aEntryType, aRetval); + if (mFCPTiming && mFCPTiming->GetName().Equals(aName) && + (!aEntryType.WasPassed() || + mFCPTiming->GetEntryType().Equals(aEntryType.Value()))) { + aRetval.AppendElement(mFCPTiming); + return; + } + // The navigation entry is the first one. If it exists and the name matches, // let put it in front. if (mDocEntry && mDocEntry->GetName().Equals(aName)) { diff --git a/dom/performance/PerformanceMainThread.h b/dom/performance/PerformanceMainThread.h index 4199d2c64681..708597c393b8 100644 --- a/dom/performance/PerformanceMainThread.h +++ b/dom/performance/PerformanceMainThread.h @@ -38,6 +38,7 @@ class PerformanceMainThread final : public Performance, void AddRawEntry(UniquePtr, const nsAString& aInitiatorType, const nsAString& aEntryName); + virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override; TimeStamp CreationTimeStamp() const override; @@ -90,6 +91,7 @@ class PerformanceMainThread final : public Performance, nsCOMPtr mChannel; RefPtr mTiming; RefPtr mNavigation; + RefPtr mFCPTiming; JS::Heap mMozMemory; const bool mCrossOriginIsolated; diff --git a/dom/performance/PerformanceObserver.cpp b/dom/performance/PerformanceObserver.cpp index 6c521d039466..3d8de3b82ab0 100644 --- a/dom/performance/PerformanceObserver.cpp +++ b/dom/performance/PerformanceObserver.cpp @@ -132,11 +132,8 @@ void PerformanceObserver::QueueEntry(PerformanceEntry* aEntry) { * Keep this list in alphabetical order. * https://w3c.github.io/performance-timeline/#supportedentrytypes-attribute */ -static const char16_t* const sValidTypeNames[4] = { - u"mark", - u"measure", - u"navigation", - u"resource", +static const char16_t* const sValidTypeNames[5] = { + u"mark", u"measure", u"navigation", u"paint", u"resource", }; void PerformanceObserver::ReportUnsupportedTypesErrorToConsole( diff --git a/dom/performance/PerformancePaintTiming.cpp b/dom/performance/PerformancePaintTiming.cpp new file mode 100644 index 000000000000..c2062f060c1c --- /dev/null +++ b/dom/performance/PerformancePaintTiming.cpp @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#include "PerformancePaintTiming.h" +#include "MainThreadUtils.h" +#include "mozilla/dom/PerformanceMeasureBinding.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PerformancePaintTiming, PerformanceEntry, + mPerformance) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformancePaintTiming) +NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry) + +NS_IMPL_ADDREF_INHERITED(PerformancePaintTiming, PerformanceEntry) +NS_IMPL_RELEASE_INHERITED(PerformancePaintTiming, PerformanceEntry) + +PerformancePaintTiming::PerformancePaintTiming(Performance* aPerformance, + const nsAString& aName, + const TimeStamp& aStartTime) + : PerformanceEntry(aPerformance->GetParentObject(), aName, u"paint"_ns), + mPerformance(aPerformance), + mStartTime(aStartTime) {} + +PerformancePaintTiming::~PerformancePaintTiming() = default; + +JSObject* PerformancePaintTiming::WrapObject( + JSContext* aCx, JS::Handle aGivenProto) { + return PerformancePaintTiming_Binding::Wrap(aCx, this, aGivenProto); +} + +DOMHighResTimeStamp PerformancePaintTiming::StartTime() const { + DOMHighResTimeStamp rawValue = + mPerformance->GetDOMTiming()->TimeStampToDOMHighRes(mStartTime); + return nsRFPService::ReduceTimePrecisionAsMSecs( + rawValue, mPerformance->GetRandomTimelineSeed(), + mPerformance->IsSystemPrincipal(), mPerformance->CrossOriginIsolated()); +} + +size_t PerformancePaintTiming::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} diff --git a/dom/performance/PerformancePaintTiming.h b/dom/performance/PerformancePaintTiming.h new file mode 100644 index 000000000000..544af14cd37a --- /dev/null +++ b/dom/performance/PerformancePaintTiming.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_PerformancePaintTiming_h___ +#define mozilla_dom_PerformancePaintTiming_h___ + +#include "mozilla/dom/PerformanceEntry.h" +#include "mozilla/dom/PerformancePaintTimingBinding.h" + +namespace mozilla { +namespace dom { + +// https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming +// Unlike timeToContentfulPaint, this timing is generated during +// displaylist building, when a frame is contentful, we collect +// the timestamp. Whereas, timeToContentfulPaint uses a compositor-side +// timestamp. +class PerformancePaintTiming final : public PerformanceEntry { + public: + PerformancePaintTiming(Performance* aPerformance, const nsAString& aName, + const TimeStamp& aStartTime); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PerformancePaintTiming, + PerformanceEntry) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + DOMHighResTimeStamp StartTime() const override; + + size_t SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const override; + + private: + ~PerformancePaintTiming(); + RefPtr mPerformance; + const TimeStamp mStartTime; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_PerformanacePaintTiming_h___ */ diff --git a/dom/performance/PerformanceWorker.h b/dom/performance/PerformanceWorker.h index dc2ac18469a2..99f7fdaa25e4 100644 --- a/dom/performance/PerformanceWorker.h +++ b/dom/performance/PerformanceWorker.h @@ -33,6 +33,10 @@ class PerformanceWorker final : public Performance { return nullptr; } + virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override { + MOZ_CRASH("This should not be called on workers."); + } + TimeStamp CreationTimeStamp() const override; DOMHighResTimeStamp CreationTime() const override; diff --git a/dom/performance/moz.build b/dom/performance/moz.build index df7f7dc72fc2..f5019df3f9b2 100644 --- a/dom/performance/moz.build +++ b/dom/performance/moz.build @@ -17,6 +17,7 @@ EXPORTS.mozilla.dom += [ 'PerformanceNavigationTiming.h', 'PerformanceObserver.h', 'PerformanceObserverEntryList.h', + 'PerformancePaintTiming.h', 'PerformanceResourceTiming.h', 'PerformanceServerTiming.h', 'PerformanceService.h', @@ -35,6 +36,7 @@ UNIFIED_SOURCES += [ 'PerformanceNavigationTiming.cpp', 'PerformanceObserver.cpp', 'PerformanceObserverEntryList.cpp', + 'PerformancePaintTiming.cpp', 'PerformanceResourceTiming.cpp', 'PerformanceServerTiming.cpp', 'PerformanceService.cpp', diff --git a/dom/performance/tests/logo.png b/dom/performance/tests/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a05926bcd7148b648cf3ba3e84c70c6e6a51d5da GIT binary patch literal 21901 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 literal 0 HcmV?d00001 diff --git a/dom/performance/tests/mochitest.ini b/dom/performance/tests/mochitest.ini index 72c6a3268bcc..a43158f77159 100644 --- a/dom/performance/tests/mochitest.ini +++ b/dom/performance/tests/mochitest.ini @@ -14,6 +14,14 @@ support-files = [test_performance_observer.html] [test_performance_user_timing.html] [test_performance_navigation_timing.html] +[test_performance_paint_timing.html] +support-files = + test_performance_paint_timing_helper.html + logo.png +[test_performance_paint_observer.html] +support-files = + test_performance_paint_observer_helper.html + logo.png [test_worker_user_timing.html] [test_worker_observer.html] [test_sharedWorker_performance_user_timing.html] diff --git a/dom/performance/tests/test_performance_paint_observer.html b/dom/performance/tests/test_performance_paint_observer.html new file mode 100644 index 000000000000..2ded1db7975c --- /dev/null +++ b/dom/performance/tests/test_performance_paint_observer.html @@ -0,0 +1,40 @@ + + + + + + Test for Bug 1518999 (Observer API) + + + + + + Mozilla + Bug 1518999 - Paint Timing API For Observers +

+ + + diff --git a/dom/performance/tests/test_performance_paint_observer_helper.html b/dom/performance/tests/test_performance_paint_observer_helper.html new file mode 100644 index 000000000000..ae27c9480dc9 --- /dev/null +++ b/dom/performance/tests/test_performance_paint_observer_helper.html @@ -0,0 +1,35 @@ + + + + + + + diff --git a/dom/performance/tests/test_performance_paint_timing.html b/dom/performance/tests/test_performance_paint_timing.html new file mode 100644 index 000000000000..f8784ecf260f --- /dev/null +++ b/dom/performance/tests/test_performance_paint_timing.html @@ -0,0 +1,38 @@ + + + + + + Test for Bug 1518999 + + + + + + Mozilla + Bug 1518999 - Paint Timing API +

+ + + diff --git a/dom/performance/tests/test_performance_paint_timing_helper.html b/dom/performance/tests/test_performance_paint_timing_helper.html new file mode 100644 index 000000000000..c05b38cac043 --- /dev/null +++ b/dom/performance/tests/test_performance_paint_timing_helper.html @@ -0,0 +1,65 @@ + + + + + + Test for Bug 1518999 + + + +
+
+
+ +
+ + + diff --git a/dom/tests/mochitest/general/test_interfaces.js b/dom/tests/mochitest/general/test_interfaces.js index afa361ebd87b..120d9656c79c 100644 --- a/dom/tests/mochitest/general/test_interfaces.js +++ b/dom/tests/mochitest/general/test_interfaces.js @@ -858,6 +858,8 @@ var interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceObserverEntryList", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformancePaintTiming", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceResourceTiming", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceServerTiming", insecureContext: false }, diff --git a/dom/webidl/PerformancePaintTiming.webidl b/dom/webidl/PerformancePaintTiming.webidl new file mode 100644 index 000000000000..6551bdfd8d8d --- /dev/null +++ b/dom/webidl/PerformancePaintTiming.webidl @@ -0,0 +1,16 @@ +/* -*- 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/. + * + * The origin of this IDL file is + * https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming + * + * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C + * liability, trademark and document use rules apply. + */ + +[Exposed=(Window)] +interface PerformancePaintTiming : PerformanceEntry +{ +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index f7d32cc20e38..366c000ab7c6 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -750,6 +750,7 @@ WEBIDL_FILES = [ 'PerformanceNavigationTiming.webidl', 'PerformanceObserver.webidl', 'PerformanceObserverEntryList.webidl', + 'PerformancePaintTiming.webidl', 'PerformanceResourceTiming.webidl', 'PerformanceServerTiming.webidl', 'PerformanceTiming.webidl', diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp index 110cdb039a47..496bf283b538 100644 --- a/layout/base/nsPresContext.cpp +++ b/layout/base/nsPresContext.cpp @@ -89,6 +89,7 @@ #include "mozilla/Telemetry.h" #include "mozilla/dom/Performance.h" #include "mozilla/dom/PerformanceTiming.h" +#include "mozilla/dom/PerformancePaintTiming.h" #include "mozilla/layers/APZThreadUtils.h" #include "MobileViewportManager.h" #include "mozilla/dom/ImageTracker.h" @@ -189,6 +190,7 @@ nsPresContext::nsPresContext(dom::Document* aDocument, nsPresContextType aType) mInflationDisabledForShrinkWrap(false), mInteractionTimeEnabled(true), mHasPendingInterrupt(false), + mHasEverBuiltInvisibleText(false), mPendingInterruptFromTest(false), mInterruptsEnabled(false), mSendAfterPaintToContent(false), @@ -2367,7 +2369,8 @@ void nsPresContext::NotifyNonBlankPaint() { } } -void nsPresContext::NotifyContentfulPaint() { +void nsPresContext::NotifyContentfulPaint( + const mozilla::TimeStamp& aTimeStamp) { if (!mHadContentfulPaint) { #if defined(MOZ_WIDGET_ANDROID) (new AsyncEventDispatcher(mDocument, u"MozFirstContentfulPaint"_ns, @@ -2375,10 +2378,18 @@ void nsPresContext::NotifyContentfulPaint() { ->PostDOMEvent(); #endif mHadContentfulPaint = true; - if (IsRootContentDocument()) { - if (nsRootPresContext* rootPresContext = GetRootPresContext()) { - mFirstContentfulPaintTransactionId = - Some(rootPresContext->mRefreshDriver->LastTransactionId().Next()); + if (nsRootPresContext* rootPresContext = GetRootPresContext()) { + mFirstContentfulPaintTransactionId = + Some(rootPresContext->mRefreshDriver->LastTransactionId().Next()); + nsPIDOMWindowInner* innerWindow = mDocument->GetInnerWindow(); + if (innerWindow) { + mozilla::dom::Performance* perf = innerWindow->GetPerformance(); + if (perf) { + RefPtr paintTiming = + new PerformancePaintTiming(perf, u"first-contentful-paint"_ns, + aTimeStamp); + perf->SetFCPTimingEntry(paintTiming); + } } } } diff --git a/layout/base/nsPresContext.h b/layout/base/nsPresContext.h index e6f2201fb158..9f8510680944 100644 --- a/layout/base/nsPresContext.h +++ b/layout/base/nsPresContext.h @@ -1031,10 +1031,13 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr { bool HadNonBlankPaint() const { return mHadNonBlankPaint; } bool HadContentfulPaint() const { return mHadContentfulPaint; } void NotifyNonBlankPaint(); - void NotifyContentfulPaint(); + void NotifyContentfulPaint(const mozilla::TimeStamp& aTimeStamp); void NotifyPaintStatusReset(); void NotifyDOMContentFlushed(); + bool HasEverBuiltInvisibleText() const { return mHasEverBuiltInvisibleText; } + void SetBuiltInvisibleText() { mHasEverBuiltInvisibleText = true; } + bool UsesExChUnits() const { return mUsesExChUnits; } void SetUsesExChUnits(bool aValue) { mUsesExChUnits = aValue; } @@ -1277,6 +1280,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr { protected: unsigned mInteractionTimeEnabled : 1; unsigned mHasPendingInterrupt : 1; + unsigned mHasEverBuiltInvisibleText : 1; unsigned mPendingInterruptFromTest : 1; unsigned mInterruptsEnabled : 1; unsigned mSendAfterPaintToContent : 1; diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp index 55a8b3c6a770..8292232feeba 100644 --- a/layout/generic/nsTextFrame.cpp +++ b/layout/generic/nsTextFrame.cpp @@ -4881,6 +4881,9 @@ void nsTextFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, TextDecorations textDecs; GetTextDecorations(PresContext(), eResolvedColors, textDecs); if (!textDecs.HasDecorationLines()) { + if (auto* currentPresContext = aBuilder->CurrentPresContext()) { + currentPresContext->SetBuiltInvisibleText(); + } return; } } diff --git a/layout/generic/nsVideoFrame.cpp b/layout/generic/nsVideoFrame.cpp index 18b65fbfa7b5..9517ba9a3a75 100644 --- a/layout/generic/nsVideoFrame.cpp +++ b/layout/generic/nsVideoFrame.cpp @@ -507,6 +507,13 @@ class nsDisplayVideo : public nsPaintedDisplayItem { return elem->IsPotentiallyPlaying() ? LayerState::LAYER_ACTIVE_FORCE : LayerState::LAYER_INACTIVE; } + + // Only report FirstContentfulPaint when the video is set + bool IsContentful() const override { + nsVideoFrame* f = static_cast(Frame()); + HTMLVideoElement* video = HTMLVideoElement::FromNode(f->GetContent()); + return video->VideoWidth() > 0; + } }; void nsVideoFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp index d28777481283..a92df04e8497 100644 --- a/layout/painting/nsDisplayList.cpp +++ b/layout/painting/nsDisplayList.cpp @@ -1065,7 +1065,8 @@ static bool DisplayListIsNonBlank(nsDisplayList* aList) { // 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) { +static bool DisplayListIsContentful(nsDisplayListBuilder* aBuilder, + nsDisplayList* aList) { for (nsDisplayItem* i : *aList) { DisplayItemType type = i->GetType(); nsDisplayList* children = i->GetChildren(); @@ -1077,10 +1078,14 @@ static bool DisplayListIsContentful(nsDisplayList* aList) { // actually tracking all modifications) default: if (i->IsContentful()) { - return true; + bool dummy; + nsRect bound = i->GetBounds(aBuilder, &dummy); + if (!bound.IsEmpty()) { + return true; + } } if (children) { - if (DisplayListIsContentful(children)) { + if (DisplayListIsContentful(aBuilder, children)) { return true; } } @@ -1105,9 +1110,11 @@ void nsDisplayListBuilder::LeavePresShell(const nsIFrame* aReferenceFrame, } } if (!pc->HadContentfulPaint()) { - if (!CurrentPresShellState()->mIsBackgroundOnly && - DisplayListIsContentful(aPaintedContents)) { - pc->NotifyContentfulPaint(); + if (!CurrentPresShellState()->mIsBackgroundOnly) { + if (pc->HasEverBuiltInvisibleText() || + DisplayListIsContentful(this, aPaintedContents)) { + pc->NotifyContentfulPaint(TimeStamp::Now()); + } } } } diff --git a/layout/painting/nsDisplayList.h b/layout/painting/nsDisplayList.h index 65d4391e5311..118aa335f47a 100644 --- a/layout/painting/nsDisplayList.h +++ b/layout/painting/nsDisplayList.h @@ -4678,6 +4678,14 @@ class nsDisplayBackgroundImage : public nsDisplayImageContainer { nsDisplayImageContainer::RemoveFrame(aFrame); } + // Match https://w3c.github.io/paint-timing/#contentful-image + bool IsContentful() const override { + const auto& styleImage = + mBackgroundStyle->StyleBackground()->mImage.mLayers[mLayer].mImage; + + return styleImage.IsSizeAvailable() && styleImage.IsUrl(); + } + protected: typedef class mozilla::layers::ImageContainer ImageContainer; typedef class mozilla::layers::ImageLayer ImageLayer;