diff --git a/dom/base/DOMIntersectionObserver.cpp b/dom/base/DOMIntersectionObserver.cpp index 76411b4b6f78..911884878411 100644 --- a/dom/base/DOMIntersectionObserver.cpp +++ b/dom/base/DOMIntersectionObserver.cpp @@ -647,7 +647,7 @@ IntersectionInput DOMIntersectionObserver::ComputeInput( // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo // (steps 2.1 - 2.5) IntersectionOutput DOMIntersectionObserver::Intersect( - const IntersectionInput& aInput, Element& aTarget, + const IntersectionInput& aInput, const Element& aTarget, IsContentVisibilityObserver aIsContentVisibilityObserver) { const bool isSimilarOrigin = SimilarOrigin(aTarget, aInput.mRootNode) == BrowsingContextOrigin::Similar; diff --git a/dom/base/DOMIntersectionObserver.h b/dom/base/DOMIntersectionObserver.h index 5b7273907eca..bdb371766386 100644 --- a/dom/base/DOMIntersectionObserver.h +++ b/dom/base/DOMIntersectionObserver.h @@ -154,7 +154,7 @@ class DOMIntersectionObserver final : public nsISupports, enum class IsContentVisibilityObserver : bool { No, Yes }; static IntersectionOutput Intersect( - const IntersectionInput&, Element&, + const IntersectionInput&, const Element&, IsContentVisibilityObserver = IsContentVisibilityObserver::No); // Intersects with a given rect, already relative to the root frame. static IntersectionOutput Intersect(const IntersectionInput&, const nsRect&); @@ -168,6 +168,9 @@ class DOMIntersectionObserver final : public nsISupports, static already_AddRefed CreateContentVisibilityObserver(Document&); + static Maybe EdgeInclusiveIntersection(const nsRect& aRect, + const nsRect& aOtherRect); + protected: void Connect(); void QueueIntersectionObserverEntry(Element* aTarget, diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 20073e552558..f02dc61def63 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -2539,6 +2539,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAll) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocGroup) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameRequestManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentIdentifiersForLCP) // Traverse all our nsCOMArrays. NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPreloadingImages) @@ -2664,6 +2665,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document) NS_IMPL_CYCLE_COLLECTION_UNLINK(mAll) NS_IMPL_CYCLE_COLLECTION_UNLINK(mReferrerInfo) NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadReferrerInfo) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentIdentifiersForLCP); if (tmp->mDocGroup && tmp->mDocGroup->GetBrowsingContextGroup()) { tmp->mDocGroup->GetBrowsingContextGroup()->RemoveDocument(tmp, diff --git a/dom/base/Document.h b/dom/base/Document.h index 41feb0a06af5..8626699e8264 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -53,6 +53,7 @@ #include "mozilla/dom/RadioGroupContainer.h" #include "mozilla/dom/TreeOrderedArray.h" #include "mozilla/dom/ViewportMetaData.h" +#include "mozilla/dom/LargestContentfulPaint.h" #include "mozilla/glean/GleanMetrics.h" #include "nsAtom.h" #include "nsCOMArray.h" @@ -2428,6 +2429,10 @@ class Document : public nsINode, LinkedList& MediaQueryLists() { return mDOMMediaQueryLists; } + nsTHashtable& ContentIdentifiersForLCP() { + return mContentIdentifiersForLCP; + } + /** * Get the compatibility mode for this document */ @@ -5043,6 +5048,11 @@ class Document : public nsINode, // Our live MediaQueryLists LinkedList mDOMMediaQueryLists; + // A hashset to keep track of which {element, imgRequestProxy} + // combination has been processed to avoid considering the same + // element twice for LargestContentfulPaint. + nsTHashtable mContentIdentifiersForLCP; + // Array of observers nsTObserverArray mObservers; diff --git a/dom/base/Element.h b/dom/base/Element.h index b71c363f224f..a252040aeba6 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -181,8 +181,13 @@ enum : uint32_t { // element or has a HTML datalist element ancestor. ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR = ELEMENT_FLAG_BIT(4), + // If this flag is set on an element, that means this element + // has been considered by our LargestContentfulPaint algorithm and + // it's not going to be considered again. + ELEMENT_PROCESSED_BY_LCP_FOR_TEXT = ELEMENT_FLAG_BIT(5), + // Remaining bits are for subclasses - ELEMENT_TYPE_SPECIFIC_BITS_OFFSET = NODE_TYPE_SPECIFIC_BITS_OFFSET + 5 + ELEMENT_TYPE_SPECIFIC_BITS_OFFSET = NODE_TYPE_SPECIFIC_BITS_OFFSET + 6 }; #undef ELEMENT_FLAG_BIT diff --git a/dom/base/nsImageLoadingContent.cpp b/dom/base/nsImageLoadingContent.cpp index 74a5b589f00f..c1320a3472cb 100644 --- a/dom/base/nsImageLoadingContent.cpp +++ b/dom/base/nsImageLoadingContent.cpp @@ -55,6 +55,7 @@ #include "mozilla/dom/ScriptSettings.h" #include "mozilla/intl/LocaleService.h" #include "mozilla/intl/Locale.h" +#include "mozilla/dom/LargestContentfulPaint.h" #include "mozilla/net/UrlClassifierFeatureFactory.h" #include "mozilla/widget/TextRecognition.h" @@ -254,9 +255,11 @@ void nsImageLoadingContent::OnLoadComplete(imgIRequest* aRequest, FireEvent(u"error"_ns); } - SVGObserverUtils::InvalidateDirectRenderingObservers( - AsContent()->AsElement()); + Element* element = AsContent()->AsElement(); + SVGObserverUtils::InvalidateDirectRenderingObservers(element); MaybeResolveDecodePromises(); + LargestContentfulPaint::MaybeProcessImageForElementTiming(mCurrentRequest, + element); } void nsImageLoadingContent::OnUnlockedDraw() { diff --git a/dom/events/EventDispatcher.cpp b/dom/events/EventDispatcher.cpp index a70f2ccb0db5..03dc3e9dc140 100644 --- a/dom/events/EventDispatcher.cpp +++ b/dom/events/EventDispatcher.cpp @@ -40,6 +40,7 @@ #include "mozilla/dom/NotifyPaintEvent.h" #include "mozilla/dom/PageTransitionEvent.h" #include "mozilla/dom/PerformanceEventTiming.h" +#include "mozilla/dom/PerformanceMainThread.h" #include "mozilla/dom/PointerEvent.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/dom/ScrollAreaEvent.h" @@ -838,6 +839,14 @@ nsresult EventDispatcher::Dispatch(EventTarget* aTarget, if (aPresContext && !aPresContext->IsPrintingOrPrintPreview()) { eventTimingEntry = PerformanceEventTiming::TryGenerateEventTiming(target, aEvent); + + if (aEvent->IsTrusted() && aEvent->mMessage == eScroll) { + if (auto* perf = aPresContext->GetPerformanceMainThread()) { + if (!perf->HasDispatchedScrollEvent()) { + perf->SetHasDispatchedScrollEvent(); + } + } + } } bool retargeted = false; diff --git a/dom/performance/LargestContentfulPaint.cpp b/dom/performance/LargestContentfulPaint.cpp index 29984ade73d1..1607d1f12701 100644 --- a/dom/performance/LargestContentfulPaint.cpp +++ b/dom/performance/LargestContentfulPaint.cpp @@ -5,15 +5,30 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/Element.h" #include "nsContentUtils.h" +#include "nsLayoutUtils.h" #include "nsRFPService.h" #include "Performance.h" +#include "imgRequest.h" #include "PerformanceMainThread.h" #include "LargestContentfulPaint.h" +#include "mozilla/dom/DOMIntersectionObserver.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" + +#include "mozilla/PresShell.h" +#include "mozilla/Logging.h" +#include "mozilla/nsVideoFrame.h" + namespace mozilla::dom { +static LazyLogModule gLCPLogging("LargestContentfulPaint"); + +#define LOG(...) MOZ_LOG(gLCPLogging, LogLevel::Debug, (__VA_ARGS__)) + NS_IMPL_CYCLE_COLLECTION_INHERITED(LargestContentfulPaint, PerformanceEntry, - mPerformance, mURI, mElement) + mPerformance, mURI, mElement, + mLCPImageEntryKey) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LargestContentfulPaint) NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry) @@ -21,19 +36,34 @@ NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry) NS_IMPL_ADDREF_INHERITED(LargestContentfulPaint, PerformanceEntry) NS_IMPL_RELEASE_INHERITED(LargestContentfulPaint, PerformanceEntry) -LargestContentfulPaint::LargestContentfulPaint(Performance* aPerformance, - DOMHighResTimeStamp aRenderTime, - DOMHighResTimeStamp aLoadTime, - unsigned long aSize, - nsIURI* aURI, Element* aElement) +static double GetAreaInDoublePixelsFromAppUnits(const nsSize& aSize) { + return NSAppUnitsToDoublePixels(aSize.Width(), AppUnitsPerCSSPixel()) * + NSAppUnitsToDoublePixels(aSize.Height(), AppUnitsPerCSSPixel()); +} + +static double GetAreaInDoublePixelsFromAppUnits(const nsRect& aRect) { + return NSAppUnitsToDoublePixels(aRect.Width(), AppUnitsPerCSSPixel()) * + NSAppUnitsToDoublePixels(aRect.Height(), AppUnitsPerCSSPixel()); +} + +ImagePendingRendering::ImagePendingRendering( + const LCPImageEntryKey& aLCPImageEntryKey, DOMHighResTimeStamp aLoadTime) + : mLCPImageEntryKey(aLCPImageEntryKey), mLoadTime(aLoadTime) {} + +LargestContentfulPaint::LargestContentfulPaint( + PerformanceMainThread* aPerformance, const DOMHighResTimeStamp aRenderTime, + const DOMHighResTimeStamp aLoadTime, const unsigned long aSize, + nsIURI* aURI, Element* aElement, + const Maybe& aLCPImageEntryKey) : PerformanceEntry(aPerformance->GetParentObject(), u""_ns, kLargestContentfulPaintName), - mPerformance(static_cast(aPerformance)), + mPerformance(aPerformance), mRenderTime(aRenderTime), mLoadTime(aLoadTime), mSize(aSize), mURI(aURI), - mElement(aElement) { + mElement(aElement), + mLCPImageEntryKey(aLCPImageEntryKey) { MOZ_ASSERT(mPerformance); MOZ_ASSERT(mElement); // The element could be a pseudo-element @@ -58,6 +88,187 @@ Element* LargestContentfulPaint::GetElement() const { : nullptr; } +void LargestContentfulPaint::BufferEntryIfNeeded() { + mPerformance->BufferLargestContentfulPaintEntryIfNeeded(this); +} + +/* static*/ +bool LCPHelpers::IsQualifiedImageRequest(imgRequest* aRequest, + Element* aContainingElement) { + MOZ_ASSERT(aContainingElement); + if (!aRequest) { + return false; + } + + if (aRequest->IsChrome()) { + return false; + } + + if (!aContainingElement->ChromeOnlyAccess()) { + return true; + } + + // Exception: this is a poster image of video element + if (nsIContent* parent = aContainingElement->GetParent()) { + nsVideoFrame* videoFrame = do_QueryFrame(parent->GetPrimaryFrame()); + if (videoFrame && videoFrame->GetPosterImage() == aContainingElement) { + return true; + } + } + + // Exception: CSS generated images + if (aContainingElement->IsInNativeAnonymousSubtree()) { + if (nsINode* rootParentOrHost = + aContainingElement + ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) { + if (!rootParentOrHost->ChromeOnlyAccess()) { + return true; + } + } + } + return false; +} +void LargestContentfulPaint::MaybeProcessImageForElementTiming( + imgRequestProxy* aRequest, Element* aElement) { + if (!StaticPrefs::dom_enable_largest_contentful_paint()) { + return; + } + + MOZ_ASSERT(aRequest); + imgRequest* request = aRequest->GetOwner(); + if (!LCPHelpers::IsQualifiedImageRequest(request, aElement)) { + return; + } + + Document* document = aElement->GetComposedDoc(); + if (!document) { + return; + } + + nsPresContext* pc = + aElement->GetPresContext(Element::PresContextFor::eForComposedDoc); + if (!pc) { + return; + } + + PerformanceMainThread* performance = pc->GetPerformanceMainThread(); + if (!performance) { + return; + } + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) { + nsCOMPtr uri; + aRequest->GetURI(getter_AddRefs(uri)); + LOG("MaybeProcessImageForElementTiming, Element=%p, URI=%s, " + "performance=%p ", + aElement, uri ? uri->GetSpecOrDefault().get() : "", performance); + } + + const LCPImageEntryKey entryKey = LCPImageEntryKey(aElement, aRequest); + if (!document->ContentIdentifiersForLCP().EnsureInserted(entryKey)) { + LOG(" The content identifier existed for element=%p and request=%p, " + "return.", + aElement, aRequest); + return; + } + +#ifdef DEBUG + uint32_t status = imgIRequest::STATUS_NONE; + aRequest->GetImageStatus(&status); + MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE); +#endif + + // Here we are exposing the load time of the image which could be + // a privacy concern. The spec talks about it at + // https://wicg.github.io/element-timing/#sec-security + // TLDR: The similar metric can be obtained by ResourceTiming + // API and onload handlers already, so this is not exposing anything + // new. + DOMHighResTimeStamp nowTime = + performance->TimeStampToDOMHighResForRendering(TimeStamp::Now()); + + if (!request->IsData() && !request->ShouldReportRenderTimeForLCP()) { + // https://wicg.github.io/element-timing/#report-image-element-timing + LOG(" Added a pending image rendering (TAO FAILED)"); + // For TAO failed requests, the renderTime is exposed as 0 for + // security reasons. + LCPHelpers::CreateLCPEntryForImage(performance, aElement, aRequest, nowTime, + 0 /* aRenderTime */, entryKey); + return; + } + + // Otherwise, add the triple (element, imageRequest, now) to root’s images + // pending rendering. + // + // At this point, the loadTime of the image is known, but + // the renderTime is unknown, so it's added to ImagesPendingRendering + // as a placeholder, and the corresponding LCP entry will be created + // when the renderTime is known. + LOG(" Added a pending image rendering (TAO PASSED)"); + performance->AddImagesPendingRendering( + ImagePendingRendering{entryKey, nowTime}); +} + +bool LCPHelpers::CanFinalizeLCPEntry(const nsIFrame* aFrame) { + if (!StaticPrefs::dom_enable_largest_contentful_paint()) { + return false; + } + + if (!aFrame) { + return false; + } + + nsPresContext* presContext = aFrame->PresContext(); + return !presContext->HasStoppedGeneratingLCP() && + presContext->GetPerformanceMainThread(); +} + +void LCPHelpers::FinalizeLCPEntryForImage( + Element* aContainingBlock, imgRequestProxy* aImgRequestProxy, + const nsRect& aTargetRectRelativeToSelf) { + LOG("FinalizeLCPEntryForImage element=%p", aContainingBlock); + if (!aImgRequestProxy) { + return; + } + + if (!IsQualifiedImageRequest(aImgRequestProxy->GetOwner(), + aContainingBlock)) { + return; + } + + nsIFrame* frame = aContainingBlock->GetPrimaryFrame(); + + if (!CanFinalizeLCPEntry(frame)) { + return; + } + + PerformanceMainThread* performance = + frame->PresContext()->GetPerformanceMainThread(); + MOZ_ASSERT(performance); + + RefPtr entry = + performance->GetImageLCPEntry(aContainingBlock, aImgRequestProxy); + if (!entry) { + LOG(" No Image Entry"); + return; + } + entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, performance, + true); + // If area is less than or equal to document’s largest contentful paint size, + // return. + if (!performance->UpdateLargestContentfulPaintSize(entry->Size())) { + LOG( + + " This paint(%lu) is not greater than the largest paint (%lf)that " + "we've " + "reported so far, return", + entry->Size(), performance->GetLargestContentfulPaintSize()); + return; + } + + entry->QueueEntry(); +} + DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const { return nsRFPService::ReduceTimePrecisionAsMSecs( mRenderTime, mPerformance->GetRandomTimelineSeed(), @@ -77,9 +288,228 @@ DOMHighResTimeStamp LargestContentfulPaint::StartTime() const { mPerformance->GetRTPCallerType()); } +/* static */ +Element* LargestContentfulPaint::GetContainingBlockForTextFrame( + const nsTextFrame* aTextFrame) { + nsIFrame* containingFrame = aTextFrame->GetContainingBlock(); + MOZ_ASSERT(containingFrame); + return Element::FromNodeOrNull(containingFrame->GetContent()); +} + +void LargestContentfulPaint::QueueEntry() { + LOG("QueueEntry entry=%p", this); + mPerformance->QueueLargestContentfulPaintEntry(this); +} + void LargestContentfulPaint::GetUrl(nsAString& aUrl) { if (mURI) { CopyUTF8toUTF16(mURI->GetSpecOrDefault(), aUrl); } } + +void LargestContentfulPaint::UpdateSize( + const Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf, + const PerformanceMainThread* aPerformance, bool aIsImage) { + nsIFrame* frame = aContainingBlock->GetPrimaryFrame(); + MOZ_ASSERT(frame); + + nsIFrame* rootFrame = frame->PresShell()->GetRootFrame(); + if (!rootFrame) { + return; + } + + if (frame->Style()->IsInOpacityZeroSubtree()) { + LOG(" Opacity:0 return"); + return; + } + + // The following size computation is based on a pending pull request + // https://github.com/w3c/largest-contentful-paint/pull/99 + + // Let visibleDimensions be concreteDimensions, adjusted for positioning + // by object-position or background-position and element’s content box. + const nsRect& visibleDimensions = aTargetRectRelativeToSelf; + + // Let clientContentRect be the smallest DOMRectReadOnly containing + // visibleDimensions with element’s transforms applied. + nsRect clientContentRect = nsLayoutUtils::TransformFrameRectToAncestor( + frame, visibleDimensions, rootFrame); + + // Let intersectionRect be the value returned by the intersection rect + // algorithm using element as the target and viewport as the root. + // (From https://wicg.github.io/element-timing/#sec-report-image-element) + IntersectionInput input = DOMIntersectionObserver::ComputeInput( + *frame->PresContext()->Document(), rootFrame->GetContent(), nullptr); + const IntersectionOutput output = + DOMIntersectionObserver::Intersect(input, *aContainingBlock); + + Maybe intersectionRect = output.mIntersectionRect; + + if (intersectionRect.isNothing()) { + LOG(" The intersectionRect is nothing for Element=%p. return.", + aContainingBlock); + return; + } + + // Let intersectingClientContentRect be the intersection of clientContentRect + // with intersectionRect. + Maybe intersectionWithContentRect = + clientContentRect.EdgeInclusiveIntersection(intersectionRect.value()); + + if (intersectionWithContentRect.isNothing()) { + LOG(" The intersectionWithContentRect is nothing for Element=%p. return.", + aContainingBlock); + return; + } + + nsRect renderedRect = intersectionWithContentRect.value(); + + double area = GetAreaInDoublePixelsFromAppUnits(renderedRect); + + double viewport = GetAreaInDoublePixelsFromAppUnits(input.mRootRect); + + LOG(" Viewport = %f, RenderRect = %f.", viewport, area); + // We don't want to report things that take the entire viewport. + if (area >= viewport) { + LOG(" The renderedRect is at least same as the area of the " + "viewport for Element=%p, return.", + aContainingBlock); + return; + } + + Maybe intrinsicSize = frame->GetIntrinsicSize().ToSize(); + const bool hasIntrinsicSize = intrinsicSize && !intrinsicSize->IsEmpty(); + + if (aIsImage && hasIntrinsicSize) { + // Let (naturalWidth, naturalHeight) be imageRequest’s natural dimension. + // Let naturalArea be naturalWidth * naturalHeight. + double naturalArea = + GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value()); + + LOG(" naturalArea = %f", naturalArea); + + // Let boundingClientArea be clientContentRect’s width * clientContentRect’s + // height. + double boundingClientArea = + NSAppUnitsToDoublePixels(clientContentRect.Width(), + AppUnitsPerCSSPixel()) * + NSAppUnitsToDoublePixels(clientContentRect.Height(), + AppUnitsPerCSSPixel()); + LOG(" boundingClientArea = %f", boundingClientArea); + + // Let scaleFactor be boundingClientArea / naturalArea. + double scaleFactor = boundingClientArea / naturalArea; + LOG(" scaleFactor = %f", scaleFactor); + + // If scaleFactor is greater than 1, then divide area by scaleFactor. + if (scaleFactor > 1) { + LOG(" area before sacled doown %f", area); + area = area / scaleFactor; + } + } + + MOZ_ASSERT(!mSize); + mSize = area; +} + +void LCPTextFrameHelper::MaybeUnionTextFrame( + nsTextFrame* aTextFrame, const nsRect& aRelativeToSelfRect) { + if (!StaticPrefs::dom_enable_largest_contentful_paint() || + aTextFrame->PresContext()->HasStoppedGeneratingLCP()) { + return; + } + + Element* containingBlock = + LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame); + if (!containingBlock || + // If element is contained in doc’s set of elements with rendered text, + // continue + containingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT) || + containingBlock->ChromeOnlyAccess()) { + return; + } + + MOZ_ASSERT(containingBlock->GetPrimaryFrame()); + + PerformanceMainThread* perf = + aTextFrame->PresContext()->GetPerformanceMainThread(); + if (!perf) { + return; + } + + auto& unionRect = perf->GetTextFrameUnions().LookupOrInsert(containingBlock); + unionRect = unionRect.Union(aRelativeToSelfRect); +} + +void LCPHelpers::CreateLCPEntryForImage( + PerformanceMainThread* aPerformance, Element* aElement, + imgRequestProxy* aRequestProxy, const DOMHighResTimeStamp aLoadTime, + const DOMHighResTimeStamp aRenderTime, + const LCPImageEntryKey& aImageEntryKey) { + MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint()); + MOZ_ASSERT(aRequestProxy); + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) { + nsCOMPtr uri; + aRequestProxy->GetURI(getter_AddRefs(uri)); + LOG("CreateLCPEntryForImage " + "Element=%p, aRequestProxy=%p, URI=%s loadTime=%f, " + "aRenderTime=%f\n", + aElement, aRequestProxy, uri->GetSpecOrDefault().get(), aLoadTime, + aRenderTime); + } + MOZ_ASSERT(aPerformance); + MOZ_ASSERT_IF(aRenderTime < aLoadTime, aRenderTime == 0); + if (aPerformance->HasDispatchedInputEvent() || + aPerformance->HasDispatchedScrollEvent()) { + return; + } + + // Let url be the empty string. + // If imageRequest is not null, set url to be imageRequest’s request URL. + nsCOMPtr requestURI; + aRequestProxy->GetURI(getter_AddRefs(requestURI)); + + // At this point, we have all the information about the entry + // except the size. + RefPtr entry = + new LargestContentfulPaint(aPerformance, aRenderTime, aLoadTime, 0, + requestURI, aElement, Some(aImageEntryKey)); + + LOG(" Upsert a LargestContentfulPaint entry=%p to LCPEntryMap.", + entry.get()); + aPerformance->StoreImageLCPEntry(aElement, aRequestProxy, entry); +} + +void LCPHelpers::FinalizeLCPEntryForText( + PerformanceMainThread* aPerformance, const DOMHighResTimeStamp aRenderTime, + Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf, + const nsPresContext* aPresContext) { + MOZ_ASSERT(aPerformance); + LOG("FinalizeLCPEntryForText element=%p", aContainingBlock); + + if (!aContainingBlock->GetPrimaryFrame()) { + return; + } + MOZ_ASSERT(CanFinalizeLCPEntry(aContainingBlock->GetPrimaryFrame())); + MOZ_ASSERT(!aContainingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT)); + MOZ_ASSERT(!aContainingBlock->ChromeOnlyAccess()); + + aContainingBlock->SetFlags(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT); + + RefPtr entry = new LargestContentfulPaint( + aPerformance, aRenderTime, 0, 0, nullptr, aContainingBlock, Nothing()); + + entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, aPerformance, + false); + // If area is less than or equal to document’s largest contentful paint size, + // return. + if (!aPerformance->UpdateLargestContentfulPaintSize(entry->Size())) { + LOG(" This paint(%lu) is not greater than the largest paint (%lf)that " + "we've " + "reported so far, return", + entry->Size(), aPerformance->GetLargestContentfulPaintSize()); + return; + } + entry->QueueEntry(); +} } // namespace mozilla::dom diff --git a/dom/performance/LargestContentfulPaint.h b/dom/performance/LargestContentfulPaint.h index e32eb37970a6..386b4d5ffee7 100644 --- a/dom/performance/LargestContentfulPaint.h +++ b/dom/performance/LargestContentfulPaint.h @@ -10,7 +10,8 @@ #include "nsCycleCollectionParticipant.h" #include "mozilla/dom/PerformanceEntry.h" #include "mozilla/dom/PerformanceLargestContentfulPaintBinding.h" -#include "nsIWeakReferenceUtils.h" + +#include "imgRequestProxy.h" class nsTextFrame; namespace mozilla::dom { @@ -21,6 +22,143 @@ static constexpr nsLiteralString kLargestContentfulPaintName = class Performance; class PerformanceMainThread; +struct LCPImageEntryKey { + LCPImageEntryKey(Element* aElement, imgRequestProxy* aImgRequestProxy) + : mElement(aElement), mImageRequestProxy(aImgRequestProxy) { + MOZ_ASSERT(aElement); + MOZ_ASSERT(aImgRequestProxy); + } + + LCPImageEntryKey(const LCPImageEntryKey& aLCPImageEntryKey) { + mElement = aLCPImageEntryKey.mElement; + mImageRequestProxy = aLCPImageEntryKey.mImageRequestProxy; + } + + bool operator==(const LCPImageEntryKey& aOther) const { + return mElement == aOther.mElement && + mImageRequestProxy == aOther.mImageRequestProxy; + } + + bool Equals(const Element* aElement, + const imgRequestProxy* aImgRequestProxy) const { + return mElement == mElement && mImageRequestProxy == mImageRequestProxy; + } + + RefPtr mElement; + RefPtr mImageRequestProxy; + + ~LCPImageEntryKey() = default; +}; + +inline void ImplCycleCollectionUnlink(LCPImageEntryKey& aField) { + aField.mElement = nullptr; + aField.mImageRequestProxy = nullptr; +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, LCPImageEntryKey& aField, + const char* aName, uint32_t aFlags = 0) { + ImplCycleCollectionTraverse(aCallback, aField.mElement, + "LCPImageEntryKey.mElement", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mImageRequestProxy, + "LCPImageEntryKey.mImageRequestProxy", aFlags); +} + +struct LCPTextFrameHelper final { + static void MaybeUnionTextFrame(nsTextFrame* aTextFrame, + const nsRect& aRelativeToSelfRect); + static void HasContainingElementBeenProcessed(); +}; + +class ImagePendingRendering final { + public: + ImagePendingRendering(const LCPImageEntryKey& aLCPImageEntryKey, + DOMHighResTimeStamp aLoadTime); + + Element* GetElement() const { return mLCPImageEntryKey.mElement; } + + imgRequestProxy* GetImgRequestProxy() const { + return mLCPImageEntryKey.mImageRequestProxy; + } + + LCPImageEntryKey mLCPImageEntryKey; + DOMHighResTimeStamp mLoadTime; +}; + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ImagePendingRendering& aField, const char* aName, uint32_t aFlags = 0) { + ImplCycleCollectionTraverse(aCallback, aField.mLCPImageEntryKey, + "ImagePendingRendering.mImageEntryKey", + aCallback.Flags()); +} + +class LCPEntryHashEntry : public PLDHashEntryHdr { + public: + typedef const LCPImageEntryKey& KeyType; + typedef const LCPImageEntryKey* KeyTypePointer; + + explicit LCPEntryHashEntry(KeyTypePointer aKey) : mKey(*aKey) {} + LCPEntryHashEntry(LCPEntryHashEntry&&) = default; + + ~LCPEntryHashEntry() = default; + + bool KeyEquals(KeyTypePointer aKey) const { return mKey == *aKey; } + + KeyType GetKey() const { return mKey; } + + static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; } + + static PLDHashNumber HashKey(KeyTypePointer aKey) { + if (!aKey) { + return 0; + } + + return mozilla::HashGeneric( + reinterpret_cast(aKey->mElement.get()), + reinterpret_cast(aKey->mImageRequestProxy.get())); + } + enum { ALLOW_MEMMOVE = true }; + + LCPImageEntryKey mKey; +}; + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, LCPEntryHashEntry& aField, + const char* aName, uint32_t aFlags = 0) { + ImplCycleCollectionTraverse(aCallback, aField.mKey, "LCPEntryHashEntry.mKey", + aFlags); +} + +class LCPHelpers final { + public: + // Creates the LCP Entry for images with all information except the size of + // the element. The size of the image is unknown at the moment. The entry is + // not going to be queued in this function. + static void CreateLCPEntryForImage( + PerformanceMainThread* aPerformance, Element* aElement, + imgRequestProxy* aRequestProxy, const DOMHighResTimeStamp aLoadTime, + const DOMHighResTimeStamp aRenderTime, + const LCPImageEntryKey& aContentIdentifier); + + // Called when the size of the image is known. + static void FinalizeLCPEntryForImage(Element* aContainingBlock, + imgRequestProxy* aImgRequestProxy, + const nsRect& aTargetRectRelativeToSelf); + + static void FinalizeLCPEntryForText(PerformanceMainThread* aPerformance, + const DOMHighResTimeStamp aRenderTime, + Element* aContainingBlock, + const nsRect& aTargetRectRelativeToSelf, + const nsPresContext* aPresContext); + + static bool IsQualifiedImageRequest(imgRequest* aRequest, + Element* aContainingElement); + + private: + static bool CanFinalizeLCPEntry(const nsIFrame* aFrame); +}; + // https://w3c.github.io/largest-contentful-paint/ class LargestContentfulPaint final : public PerformanceEntry { public: @@ -29,10 +167,12 @@ class LargestContentfulPaint final : public PerformanceEntry { NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LargestContentfulPaint, PerformanceEntry) - LargestContentfulPaint(Performance* aPerformance, - DOMHighResTimeStamp aRenderTime, - DOMHighResTimeStamp aLoadTime, unsigned long aSize, - nsIURI* aURI, Element* aElement); + LargestContentfulPaint( + PerformanceMainThread* aPerformance, + const DOMHighResTimeStamp aRenderTime, + const DOMHighResTimeStamp aLoadTime, const unsigned long aSize, + nsIURI* aURI, Element* aElement, + const Maybe& aLCPImageEntryKey); JSObject* WrapObject(JSContext* aCx, JS::Handle aGivenProto) override; @@ -51,6 +191,23 @@ class LargestContentfulPaint final : public PerformanceEntry { Element* GetElement() const; + static Element* GetContainingBlockForTextFrame(const nsTextFrame* aTextFrame); + + void UpdateSize(const Element* aContainingBlock, + const nsRect& aTargetRectRelativeToSelf, + const PerformanceMainThread* aPerformance, bool aIsImage); + + void BufferEntryIfNeeded() override; + + static void MaybeProcessImageForElementTiming(imgRequestProxy* aRequest, + Element* aElement); + + void QueueEntry(); + + const Maybe& GetLCPImageEntryKey() const { + return mLCPImageEntryKey; + } + private: ~LargestContentfulPaint() = default; @@ -63,6 +220,8 @@ class LargestContentfulPaint final : public PerformanceEntry { RefPtr mElement; RefPtr mId; + + Maybe mLCPImageEntryKey; }; } // namespace mozilla::dom #endif diff --git a/dom/performance/PerformanceMainThread.cpp b/dom/performance/PerformanceMainThread.cpp index 1665849db2bc..d5bb0c85fd53 100644 --- a/dom/performance/PerformanceMainThread.cpp +++ b/dom/performance/PerformanceMainThread.cpp @@ -12,6 +12,7 @@ #include "js/PropertyAndElement.h" // JS_DefineProperty #include "mozilla/HoldDropJSObjects.h" #include "PerformanceEventTiming.h" +#include "LargestContentfulPaint.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/EventCounts.h" @@ -25,9 +26,13 @@ #include "nsIChannel.h" #include "nsIHttpChannel.h" #include "nsIDocShell.h" +#include "nsTextFrame.h" +#include "nsContainerFrame.h" namespace mozilla::dom { +extern mozilla::LazyLogModule gLCPLogging; + namespace { void GetURLSpecFromChannel(nsITimedChannel* aChannel, nsAString& aSpec) { @@ -59,19 +64,24 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMainThread) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMainThread, Performance) - NS_IMPL_CYCLE_COLLECTION_UNLINK(mTiming, mNavigation, mDocEntry, mFCPTiming, - mEventTimingEntries, mFirstInputEvent, - mPendingPointerDown, - mPendingEventTimingEntries, mEventCounts) + NS_IMPL_CYCLE_COLLECTION_UNLINK( + mTiming, mNavigation, mDocEntry, mFCPTiming, mEventTimingEntries, + mLargestContentfulPaintEntries, mFirstInputEvent, mPendingPointerDown, + mPendingEventTimingEntries, mEventCounts) + tmp->mImageLCPEntryMap.Clear(); + tmp->mTextFrameUnions.Clear(); + tmp->mImagesPendingRendering.Clear(); mozilla::DropJSObjects(tmp); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMainThread, Performance) - NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTiming, mNavigation, mDocEntry, mFCPTiming, - mEventTimingEntries, mFirstInputEvent, - mPendingPointerDown, - mPendingEventTimingEntries, mEventCounts) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mTiming, mNavigation, mDocEntry, mFCPTiming, mEventTimingEntries, + mLargestContentfulPaintEntries, mFirstInputEvent, mPendingPointerDown, + mPendingEventTimingEntries, mEventCounts, mImagesPendingRendering, + mImageLCPEntryMap, mTextFrameUnions) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMainThread, @@ -241,6 +251,15 @@ void PerformanceMainThread::BufferEventTimingEntryIfNeeded( } } +void PerformanceMainThread::BufferLargestContentfulPaintEntryIfNeeded( + LargestContentfulPaint* aEntry) { + MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint()); + if (mLargestContentfulPaintEntries.Length() < + kMaxLargestContentfulPaintBufferSize) { + mLargestContentfulPaintEntries.AppendElement(aEntry); + } +} + void PerformanceMainThread::DispatchPendingEventTimingEntries() { DOMHighResTimeStamp renderingTime = NowUnclamped(); @@ -267,7 +286,7 @@ void PerformanceMainThread::DispatchPendingEventTimingEntries() { MOZ_ASSERT(!mFirstInputEvent); mFirstInputEvent = mPendingPointerDown.forget(); QueueEntry(mFirstInputEvent); - mHasDispatchedInputEvent = true; + SetHasDispatchedInputEvent(); } break; } @@ -277,7 +296,7 @@ void PerformanceMainThread::DispatchPendingEventTimingEntries() { mFirstInputEvent = entry->Clone(); mFirstInputEvent->SetEntryType(u"first-input"_ns); QueueEntry(mFirstInputEvent); - mHasDispatchedInputEvent = true; + SetHasDispatchedInputEvent(); break; } default: @@ -449,6 +468,12 @@ void PerformanceMainThread::QueueNavigationTimingEntry() { QueueEntry(mDocEntry); } +void PerformanceMainThread::QueueLargestContentfulPaintEntry( + LargestContentfulPaint* aEntry) { + MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint()); + QueueEntry(aEntry); +} + EventCounts* PerformanceMainThread::EventCounts() { MOZ_ASSERT(StaticPrefs::dom_enable_event_timing()); return mEventCounts; @@ -501,6 +526,14 @@ void PerformanceMainThread::GetEntriesByTypeForObserver( aRetval.AppendElements(mEventTimingEntries); return; } + + if (StaticPrefs::dom_enable_largest_contentful_paint()) { + if (aEntryType.EqualsLiteral("largest-contentful-paint")) { + aRetval.AppendElements(mLargestContentfulPaintEntries); + return; + } + } + return GetEntriesByType(aEntryType, aRetval); } @@ -562,4 +595,129 @@ size_t PerformanceMainThread::SizeOfEventEntries( } return eventEntries; } + +void PerformanceMainThread::ProcessElementTiming() { + if (!StaticPrefs::dom_enable_largest_contentful_paint()) { + return; + } + const bool shouldLCPDataEmpty = + HasDispatchedInputEvent() || HasDispatchedScrollEvent(); + MOZ_ASSERT_IF(shouldLCPDataEmpty, + mTextFrameUnions.IsEmpty() && mImageLCPEntryMap.IsEmpty()); + + if (shouldLCPDataEmpty) { + return; + } + + nsPresContext* presContext = GetPresShell()->GetPresContext(); + MOZ_ASSERT(presContext); + + // After https://github.com/w3c/largest-contentful-paint/issues/104 is + // resolved, LargestContentfulPaint and FirstContentfulPaint should + // be using the same timestamp, which should be the same timestamp + // as to what https://w3c.github.io/paint-timing/#mark-paint-timing step 2 + // defines. + // TODO(sefeng): Check the timestamp after this issue is resolved. + DOMHighResTimeStamp rawNowTime = + TimeStampToDOMHighResForRendering(presContext->GetMarkPaintTimingStart()); + + MOZ_ASSERT(GetOwnerGlobal()); + Document* document = GetOwnerGlobal()->GetAsInnerWindow()->GetExtantDoc(); + if (!document || + !nsContentUtils::GetInProcessSubtreeRootDocument(document)->IsActive()) { + return; + } + + nsTArray imagesPendingRendering = + std::move(mImagesPendingRendering); + for (const auto& imagePendingRendering : imagesPendingRendering) { + RefPtr element = imagePendingRendering.GetElement(); + if (!element) { + continue; + } + + MOZ_ASSERT(imagePendingRendering.mLoadTime < rawNowTime); + if (imgRequestProxy* requestProxy = + imagePendingRendering.GetImgRequestProxy()) { + LCPHelpers::CreateLCPEntryForImage( + this, element, requestProxy, imagePendingRendering.mLoadTime, + rawNowTime, imagePendingRendering.mLCPImageEntryKey); + } + } + + MOZ_ASSERT(mImagesPendingRendering.IsEmpty()); +} + +void PerformanceMainThread::FinalizeLCPEntriesForText() { + nsPresContext* presContext = GetPresShell()->GetPresContext(); + MOZ_ASSERT(presContext); + + DOMHighResTimeStamp renderTime = + TimeStampToDOMHighResForRendering(presContext->GetMarkPaintTimingStart()); + + bool canFinalize = StaticPrefs::dom_enable_largest_contentful_paint() && + !presContext->HasStoppedGeneratingLCP(); + if (canFinalize) { + for (const auto& textFrameUnion : GetTextFrameUnions()) { + LCPHelpers::FinalizeLCPEntryForText( + this, renderTime, textFrameUnion.GetKey(), textFrameUnion.GetData(), + presContext); + } + } + + ClearTextFrameUnions(); +} + +void PerformanceMainThread::StoreImageLCPEntry( + Element* aElement, imgRequestProxy* aImgRequestProxy, + LargestContentfulPaint* aEntry) { + mImageLCPEntryMap.InsertOrUpdate({aElement, aImgRequestProxy}, aEntry); +} + +already_AddRefed +PerformanceMainThread::GetImageLCPEntry(Element* aElement, + imgRequestProxy* aImgRequestProxy) { + Maybe> entry = + mImageLCPEntryMap.Extract({aElement, aImgRequestProxy}); + if (entry.isNothing()) { + return nullptr; + } + + Document* doc = aElement->GetComposedDoc(); + MOZ_ASSERT(doc, "Element should be connected when it's painted"); + + const Maybe& contentIdentifier = + entry.value()->GetLCPImageEntryKey(); + if (contentIdentifier.isSome()) { + doc->ContentIdentifiersForLCP().EnsureRemoved(contentIdentifier.value()); + } + + return entry.value().forget(); +} + +bool PerformanceMainThread::UpdateLargestContentfulPaintSize(double aSize) { + if (aSize > mLargestContentfulPaintSize) { + mLargestContentfulPaintSize = aSize; + return true; + } + return false; +} + +void PerformanceMainThread::SetHasDispatchedScrollEvent() { + mHasDispatchedScrollEvent = true; + ClearGeneratedTempDataForLCP(); +} + +void PerformanceMainThread::SetHasDispatchedInputEvent() { + mHasDispatchedInputEvent = true; + mImageLCPEntryMap.Clear(); + ClearGeneratedTempDataForLCP(); +} + +void PerformanceMainThread::ClearTextFrameUnions() { mTextFrameUnions.Clear(); } + +void PerformanceMainThread::ClearGeneratedTempDataForLCP() { + ClearTextFrameUnions(); + mImageLCPEntryMap.Clear(); +} } // namespace mozilla::dom diff --git a/dom/performance/PerformanceMainThread.h b/dom/performance/PerformanceMainThread.h index 8a38a5faecc3..36d421da3023 100644 --- a/dom/performance/PerformanceMainThread.h +++ b/dom/performance/PerformanceMainThread.h @@ -9,12 +9,19 @@ #include "Performance.h" #include "PerformanceStorage.h" +#include "LargestContentfulPaint.h" +#include "nsTextFrame.h" namespace mozilla::dom { class PerformanceNavigationTiming; class PerformanceEventTiming; +using ImageLCPEntryMap = + nsTHashMap>; + +using TextFrameUnions = nsTHashMap, nsRect>; + class PerformanceMainThread final : public Performance, public PerformanceStorage { public: @@ -45,11 +52,14 @@ class PerformanceMainThread final : public Performance, const nsAString& aInitiatorType, const nsAString& aEntryName); virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override; + bool HadFCPTimingEntry() const { return mFCPTiming; } void InsertEventTimingEntry(PerformanceEventTiming*) override; void BufferEventTimingEntryIfNeeded(PerformanceEventTiming*) override; void DispatchPendingEventTimingEntries() override; + void BufferLargestContentfulPaintEntryIfNeeded(LargestContentfulPaint*); + TimeStamp CreationTimeStamp() const override; DOMHighResTimeStamp CreationTime() const override; @@ -87,6 +97,7 @@ class PerformanceMainThread final : public Performance, void UpdateNavigationTimingEntry() override; void QueueNavigationTimingEntry() override; + void QueueLargestContentfulPaintEntry(LargestContentfulPaint* aEntry); size_t SizeOfEventEntries(mozilla::MallocSizeOf aMallocSizeOf) const override; @@ -94,10 +105,40 @@ class PerformanceMainThread final : public Performance, static constexpr uint32_t kDefaultEventTimingDurationThreshold = 104; static constexpr double kDefaultEventTimingMinDuration = 16.0; + static constexpr uint32_t kMaxLargestContentfulPaintBufferSize = 150; + class EventCounts* EventCounts() override; bool IsGlobalObjectWindow() const override { return true; }; + bool HasDispatchedInputEvent() const { return mHasDispatchedInputEvent; } + + void SetHasDispatchedScrollEvent(); + bool HasDispatchedScrollEvent() const { return mHasDispatchedScrollEvent; } + + void ProcessElementTiming(); + + void AddImagesPendingRendering(ImagePendingRendering aImagePendingRendering) { + mImagesPendingRendering.AppendElement(aImagePendingRendering); + } + + void StoreImageLCPEntry(Element* aElement, imgRequestProxy* aImgRequestProxy, + LargestContentfulPaint* aEntry); + + already_AddRefed GetImageLCPEntry( + Element* aElement, imgRequestProxy* aImgRequestProxy); + + bool UpdateLargestContentfulPaintSize(double aSize); + double GetLargestContentfulPaintSize() const { + return mLargestContentfulPaintSize; + } + + nsTHashMap, nsRect>& GetTextFrameUnions() { + return mTextFrameUnions; + } + + void FinalizeLCPEntriesForText(); + protected: ~PerformanceMainThread(); @@ -119,23 +160,76 @@ class PerformanceMainThread final : public Performance, JS::Heap mMozMemory; nsTArray> mEventTimingEntries; + nsTArray> mLargestContentfulPaintEntries; AutoCleanLinkedList> mPendingEventTimingEntries; bool mHasDispatchedInputEvent = false; + bool mHasDispatchedScrollEvent = false; RefPtr mFirstInputEvent; RefPtr mPendingPointerDown; private: + void ClearTextFrameUnions(); + void ClearGeneratedTempDataForLCP(); + + void SetHasDispatchedInputEvent(); + bool mHasQueuedRefreshdriverObserver = false; RefPtr mEventCounts; void IncEventCount(const nsAtom* aType); PresShell* GetPresShell(); + + nsTArray mImagesPendingRendering; + + // The key is the pair of the element initiates the image loading + // and the imgRequestProxy of the image, and the value is + // the LCP entry for this image. When the image is + // completely loaded, we add it to mImageLCPEntryMap. + // Later, when the image is painted, we get the LCP entry from it + // to update the size and queue the entry if needed. + // + // When the initiating element is disconnected from the document, + // we keep the orphan entry because if the same memory address is + // reused by a different LCP candidate, it'll update + // mImageLCPEntryMap precedes before it tries to get the LCP entry. + ImageLCPEntryMap mImageLCPEntryMap; + + // Keeps track of the rendered size of the largest contentful paint that + // we have processed so far. + double mLargestContentfulPaintSize = 0.0; + + // When a text frame is painted, its area (relative to the + // containing block) is unioned with other text frames that + // belong to the same containing block. + // mTextFrameUnions's key is the containing block, and + // the value is the unioned area. + TextFrameUnions mTextFrameUnions; }; +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, ImageLCPEntryMap& aField, + const char* aName, uint32_t aFlags = 0) { + for (auto& entry : aField) { + ImplCycleCollectionTraverse(aCallback, entry.mKey, "ImageLCPEntryMap.mKey", aCallback.Flags()); + RefPtr* lcpEntry = entry.GetModifiableData(); + ImplCycleCollectionTraverse(aCallback, *lcpEntry, "ImageLCPEntryMap.mData", aCallback.Flags()); + } +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, TextFrameUnions& aField, + const char* aName, uint32_t aFlags = 0) { + for (auto& entry : aField) { + ImplCycleCollectionTraverse( + aCallback, entry, "TextFrameUnions's key (nsRefPtrHashKey)", + aFlags); + } +} + } // namespace mozilla::dom #endif // mozilla_dom_PerformanceMainThread_h diff --git a/dom/performance/PerformanceObserver.cpp b/dom/performance/PerformanceObserver.cpp index e132b70860c2..d5d1725c5773 100644 --- a/dom/performance/PerformanceObserver.cpp +++ b/dom/performance/PerformanceObserver.cpp @@ -17,6 +17,7 @@ #include "nsQueryObject.h" #include "nsString.h" #include "PerformanceEntry.h" +#include "LargestContentfulPaint.h" #include "PerformanceObserverEntryList.h" using namespace mozilla; @@ -215,6 +216,12 @@ void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions, } } } + if (StaticPrefs::dom_enable_largest_contentful_paint()) { + if (entryTypes.Contains(kLargestContentfulPaintName) && + !validEntryTypes.Contains(kLargestContentfulPaintName)) { + validEntryTypes.AppendElement(kLargestContentfulPaintName); + } + } for (const nsLiteralString& name : kValidTypeNames) { if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) { validEntryTypes.AppendElement(name); @@ -275,6 +282,12 @@ void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions, } } + if (StaticPrefs::dom_enable_largest_contentful_paint()) { + if (type == kLargestContentfulPaintName) { + typeValid = true; + } + } + if (!typeValid) { ReportUnsupportedTypesErrorToConsole( NS_IsMainThread(), UnsupportedEntryTypesIgnoredMsgId, type); @@ -328,6 +341,10 @@ void PerformanceObserver::GetSupportedEntryTypes( validTypes.AppendElement(name); } } + + if (StaticPrefs::dom_enable_largest_contentful_paint()) { + validTypes.AppendElement(u"largest-contentful-paint"_ns); + } for (const nsLiteralString& name : kValidTypeNames) { validTypes.AppendElement(name); } diff --git a/dom/tests/mochitest/general/test_interfaces.js b/dom/tests/mochitest/general/test_interfaces.js index 40d418d4b1b6..3edaee8d5595 100644 --- a/dom/tests/mochitest/general/test_interfaces.js +++ b/dom/tests/mochitest/general/test_interfaces.js @@ -776,6 +776,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "KeyframeEffect", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "LargestContentfulPaint", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "Location", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! "Lock", diff --git a/image/imgRequest.cpp b/image/imgRequest.cpp index 27fd7e29b0d8..47bdd3480cf0 100644 --- a/image/imgRequest.cpp +++ b/image/imgRequest.cpp @@ -63,6 +63,7 @@ imgRequest::imgRequest(imgLoader* aLoader, const ImageCacheKey& aCacheKey) mImageAvailable(false), mIsDeniedCrossSiteCORSRequest(false), mIsCrossSiteNoCORSRequest(false), + mShouldReportRenderTimeForLCP(false), mMutex("imgRequest"), mProgressTracker(new ProgressTracker()), mIsMultiPartChannel(false), @@ -641,6 +642,7 @@ imgRequest::OnStartRequest(nsIRequest* aRequest) { mIsCrossSiteNoCORSRequest = loadInfo->GetTainting() == LoadTainting::Opaque; } + UpdateShouldReportRenderTimeForLCP(); // Figure out if we're multipart. nsCOMPtr multiPartChannel = do_QueryInterface(aRequest); { @@ -1232,3 +1234,13 @@ imgRequest::OnRedirectVerifyCallback(nsresult result) { mRedirectCallback = nullptr; return NS_OK; } + +void imgRequest::UpdateShouldReportRenderTimeForLCP() { + if (mTimedChannel) { + bool allRedirectPassTAO = false; + mTimedChannel->GetAllRedirectsPassTimingAllowCheck(&allRedirectPassTAO); + mShouldReportRenderTimeForLCP = + mTimedChannel->TimingAllowCheck(mTriggeringPrincipal) && + allRedirectPassTAO; + } +} diff --git a/image/imgRequest.h b/image/imgRequest.h index 7a751e650524..b948869ae81d 100644 --- a/image/imgRequest.h +++ b/image/imgRequest.h @@ -202,6 +202,10 @@ class imgRequest final : public nsIThreadRetargetableStreamListener, bool IsCrossSiteNoCORSRequest() const { return mIsCrossSiteNoCORSRequest; } + bool ShouldReportRenderTimeForLCP() const { + return mShouldReportRenderTimeForLCP; + } + private: friend class FinishPreparingForNewPartRunnable; @@ -209,6 +213,8 @@ class imgRequest final : public nsIThreadRetargetableStreamListener, void FinishPreparingForNewPart(const NewPartResult& aResult); + void UpdateShouldReportRenderTimeForLCP(); + void Cancel(nsresult aStatus); // Update the cache entry size based on the image container. @@ -276,6 +282,8 @@ class imgRequest final : public nsIThreadRetargetableStreamListener, bool mIsDeniedCrossSiteCORSRequest; bool mIsCrossSiteNoCORSRequest; + bool mShouldReportRenderTimeForLCP; + mutable mozilla::Mutex mMutex; // Member variables protected by mMutex. Note that *all* flags in our bitfield diff --git a/image/imgRequestProxy.h b/image/imgRequestProxy.h index 72e5637f7b20..b745ac21b56d 100644 --- a/image/imgRequestProxy.h +++ b/image/imgRequestProxy.h @@ -225,6 +225,10 @@ class imgRequestProxy : public mozilla::PreloaderBase, bool mHadListener : 1; }; +inline nsISupports* ToSupports(imgRequestProxy* p) { + return NS_ISUPPORTS_CAST(imgIRequest*, p); +} + NS_DEFINE_STATIC_IID_ACCESSOR(imgRequestProxy, NS_IMGREQUESTPROXY_CID) // Used for static image proxies for which no requests are available, so diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp index b02283dea6f9..f481c6462faf 100644 --- a/layout/base/PresShell.cpp +++ b/layout/base/PresShell.cpp @@ -11,6 +11,8 @@ #include "Units.h" #include "mozilla/dom/FontFaceSet.h" #include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/LargestContentfulPaint.h" +#include "mozilla/dom/PerformanceMainThread.h" #include "mozilla/ArrayUtils.h" #include "mozilla/Attributes.h" #include "mozilla/AutoRestore.h" @@ -11839,6 +11841,13 @@ void PresShell::EndPaint() { } return CallState::Continue; }); + + if (nsPresContext* presContext = GetPresContext()) { + if (PerformanceMainThread* perf = + presContext->GetPerformanceMainThread()) { + perf->FinalizeLCPEntriesForText(); + } + } } } diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h index cc733aa7ea94..20ebcbc7a3eb 100644 --- a/layout/base/PresShell.h +++ b/layout/base/PresShell.h @@ -85,6 +85,7 @@ struct RangePaintInfo; class ReflowCountMgr; #endif class WeakFrame; +class nsTextFrame; class ZoomConstraintsClient; struct nsCallbackEventRequest; @@ -113,6 +114,7 @@ class Element; class Event; class HTMLSlotElement; class Selection; +class PerformanceMainThread; } // namespace dom namespace gfx { diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp index 7b5da89a0b06..4b0d5d808731 100644 --- a/layout/base/nsPresContext.cpp +++ b/layout/base/nsPresContext.cpp @@ -89,6 +89,7 @@ #include "mozilla/Telemetry.h" #include "mozilla/TimelineManager.h" #include "mozilla/dom/Performance.h" +#include "mozilla/dom/PerformanceMainThread.h" #include "mozilla/dom/PerformanceTiming.h" #include "mozilla/dom/PerformancePaintTiming.h" #include "mozilla/layers/APZThreadUtils.h" @@ -2781,8 +2782,16 @@ void nsPresContext::NotifyNonBlankPaint() { } } +bool nsPresContext::HasStoppedGeneratingLCP() const { + if (auto* perf = GetPerformanceMainThread()) { + return perf->HasDispatchedInputEvent() || perf->HasDispatchedScrollEvent(); + } + + return true; +} + void nsPresContext::NotifyContentfulPaint() { - if (mHadFirstContentfulPaint) { + if (mHadFirstContentfulPaint && HasStoppedGeneratingLCP()) { return; } nsRootPresContext* rootPresContext = GetRootPresContext(); @@ -2804,19 +2813,24 @@ void nsPresContext::NotifyContentfulPaint() { } return; } - mHadFirstContentfulPaint = true; - mFirstContentfulPaintTransactionId = - Some(rootPresContext->mRefreshDriver->LastTransactionId().Next()); - if (nsPIDOMWindowInner* innerWindow = mDocument->GetInnerWindow()) { - if (Performance* perf = innerWindow->GetPerformance()) { + + if (!mHadFirstContentfulPaint) { + mHadFirstContentfulPaint = true; + mFirstContentfulPaintTransactionId = + Some(rootPresContext->mRefreshDriver->LastTransactionId().Next()); + } + + if (auto* perf = GetPerformanceMainThread()) { + mMarkPaintTimingStart = TimeStamp::Now(); + MOZ_ASSERT(rootPresContext->RefreshDriver()->IsInRefresh(), + "We should only notify contentful paint during refresh " + "driver ticks"); + if (!perf->HadFCPTimingEntry()) { TimeStamp nowTime = rootPresContext->RefreshDriver()->MostRecentRefresh( /* aEnsureTimerStarted */ false); MOZ_ASSERT(!nowTime.IsNull(), "Most recent refresh timestamp should exist since we are in " "a refresh driver tick"); - MOZ_ASSERT(rootPresContext->RefreshDriver()->IsInRefresh(), - "We should only notify contentful paint during refresh " - "driver ticks"); RefPtr paintTiming = new PerformancePaintTiming( perf, u"first-contentful-paint"_ns, nowTime); perf->SetFCPTimingEntry(paintTiming); @@ -2833,12 +2847,15 @@ void nsPresContext::NotifyContentfulPaint() { nsContentUtils::TruncatedURLForDisplay(docURI).get()); PROFILER_MARKER_TEXT( "FirstContentfulPaint", DOM, - MarkerOptions(MarkerTiming::Interval(navigationStart, nowTime), - MarkerInnerWindowId(innerWindow->WindowID())), + MarkerOptions( + MarkerTiming::Interval(navigationStart, nowTime), + MarkerInnerWindowId(mDocument->GetInnerWindow()->WindowID())), marker); } } } + + perf->ProcessElementTiming(); } } @@ -3050,6 +3067,16 @@ void nsPresContext::SetSafeAreaInsets(const ScreenIntMargin& aSafeAreaInsets) { RestyleHint::RecascadeSubtree()); } +PerformanceMainThread* nsPresContext::GetPerformanceMainThread() const { + if (nsPIDOMWindowInner* innerWindow = mDocument->GetInnerWindow()) { + if (auto* perf = static_cast( + innerWindow->GetPerformance())) { + return perf; + } + } + return nullptr; +} + #ifdef DEBUG void nsPresContext::ValidatePresShellAndDocumentReleation() const { diff --git a/layout/base/nsPresContext.h b/layout/base/nsPresContext.h index 3d723066a99c..ed99f9a72026 100644 --- a/layout/base/nsPresContext.h +++ b/layout/base/nsPresContext.h @@ -92,6 +92,7 @@ class LayerManager; namespace dom { class Document; class Element; +class PerformanceMainThread; enum class PrefersColorSchemeOverride : uint8_t; } // namespace dom namespace gfx { @@ -214,6 +215,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr { void DocumentCharSetChanged(NotNull aCharSet); + mozilla::dom::PerformanceMainThread* GetPerformanceMainThread() const; /** * Returns the parent prescontext for this one. Returns null if this is a * root. @@ -1040,6 +1042,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr { bool HadNonBlankPaint() const { return mHadNonBlankPaint; } bool HadFirstContentfulPaint() const { return mHadFirstContentfulPaint; } + bool HasStoppedGeneratingLCP() const; void NotifyNonBlankPaint(); void NotifyContentfulPaint(); void NotifyPaintStatusReset(); @@ -1122,6 +1125,10 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr { } } + mozilla::TimeStamp GetMarkPaintTimingStart() const { + return mMarkPaintTimingStart; + } + protected: // May be called multiple times (unlink, destructor) void Destroy(); @@ -1245,6 +1252,9 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr { mozilla::TimeStamp mReflowStartTime; + // Defined in https://w3c.github.io/paint-timing/#mark-paint-timing step 2. + mozilla::TimeStamp mMarkPaintTimingStart; + Maybe mFirstContentfulPaintTransactionId; mozilla::UniquePtr diff --git a/layout/base/nsRefreshDriver.cpp b/layout/base/nsRefreshDriver.cpp index 2c4689b22aae..5f881fde6052 100644 --- a/layout/base/nsRefreshDriver.cpp +++ b/layout/base/nsRefreshDriver.cpp @@ -45,6 +45,7 @@ #include "nsITimer.h" #include "nsLayoutUtils.h" #include "nsPresContext.h" +#include "imgRequest.h" #include "nsComponentManagerUtils.h" #include "mozilla/Logging.h" #include "mozilla/dom/Document.h" @@ -52,6 +53,7 @@ #include "nsIXULRuntime.h" #include "jsapi.h" #include "nsContentUtils.h" +#include "nsTextFrame.h" #include "mozilla/PendingAnimationTracker.h" #include "mozilla/PendingFullscreenEvent.h" #include "mozilla/dom/PerformanceMainThread.h" @@ -71,6 +73,7 @@ #include "mozilla/dom/Selection.h" #include "mozilla/dom/VsyncMainChild.h" #include "mozilla/dom/WindowBinding.h" +#include "mozilla/dom/LargestContentfulPaint.h" #include "mozilla/layers/WebRenderLayerManager.h" #include "mozilla/RestyleManager.h" #include "mozilla/TaskController.h" diff --git a/layout/generic/moz.build b/layout/generic/moz.build index cfb84e4864f2..e76de4becc93 100644 --- a/layout/generic/moz.build +++ b/layout/generic/moz.build @@ -143,6 +143,7 @@ EXPORTS.mozilla += [ "CSSAlignUtils.h", "CSSOrderAwareFrameIterator.h", "LayoutMessageUtils.h", + "nsVideoFrame.h", "PrintedSheetFrame.h", "ReflowInput.h", "ReflowOutput.h", diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h index 80c3504e1af3..bbf3db4f70a3 100644 --- a/layout/generic/nsIFrame.h +++ b/layout/generic/nsIFrame.h @@ -5582,6 +5582,8 @@ class MOZ_HEAP_CLASS WeakFrame { nsIFrame* operator->() { return mFrame; } operator nsIFrame*() { return mFrame; } + bool operator==(nsIFrame* const aOther) const { return mFrame == aOther; } + void Clear(mozilla::PresShell* aPresShell); bool IsAlive() const { return !!mFrame; } diff --git a/layout/generic/nsImageFrame.cpp b/layout/generic/nsImageFrame.cpp index 4b3d9184e54f..f158f118f8dd 100644 --- a/layout/generic/nsImageFrame.cpp +++ b/layout/generic/nsImageFrame.cpp @@ -27,6 +27,7 @@ #include "mozilla/dom/HTMLImageElement.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/dom/ResponsiveImageSelector.h" +#include "mozilla/dom/LargestContentfulPaint.h" #include "mozilla/image/WebRenderImageProvider.h" #include "mozilla/layers/RenderRootStateManager.h" #include "mozilla/layers/WebRenderLayerManager.h" @@ -1115,6 +1116,8 @@ void nsImageFrame::Notify(imgIRequest* aRequest, int32_t aType, } if (aType == imgINotificationObserver::LOAD_COMPLETE) { + LargestContentfulPaint::MaybeProcessImageForElementTiming( + static_cast(aRequest), GetContent()->AsElement()); uint32_t imgStatus; aRequest->GetImageStatus(&imgStatus); nsresult status = @@ -2365,6 +2368,13 @@ bool nsDisplayImage::CreateWebRenderCommands( mImage->GetImageProvider(aManager->LayerManager(), decodeSize, svgContext, region, flags, getter_AddRefs(provider)); + if (nsCOMPtr currentRequest = frame->GetCurrentRequest()) { + LCPHelpers::FinalizeLCPEntryForImage( + frame->GetContent()->AsElement(), + static_cast(currentRequest.get()), + GetDestRect() - ToReferenceFrame()); + } + // While we got a container, it may not contain a fully decoded surface. If // that is the case, and we have an image we were previously displaying which // has a fully decoded surface, then we should prefer the previous image. diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp index b0e4f61119ed..db1b2e70aaf3 100644 --- a/layout/generic/nsTextFrame.cpp +++ b/layout/generic/nsTextFrame.cpp @@ -27,6 +27,7 @@ #include "mozilla/IntegerRange.h" #include "mozilla/Unused.h" #include "mozilla/PodOperations.h" +#include "mozilla/dom/PerformanceMainThread.h" #include "nsCOMPtr.h" #include "nsBlockFrame.h" diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp index 5357333a1875..6c0f119cbe94 100644 --- a/layout/painting/nsDisplayList.cpp +++ b/layout/painting/nsDisplayList.cpp @@ -28,6 +28,7 @@ #include "mozilla/dom/ServiceWorkerRegistration.h" #include "mozilla/dom/SVGElement.h" #include "mozilla/dom/TouchEvent.h" +#include "mozilla/dom/PerformanceMainThread.h" #include "mozilla/gfx/2D.h" #include "mozilla/PresShell.h" #include "mozilla/ShapeUtils.h" @@ -1220,7 +1221,7 @@ void nsDisplayListBuilder::LeavePresShell(const nsIFrame* aReferenceFrame, } } nsRootPresContext* rootPresContext = pc->GetRootPresContext(); - if (!pc->HadFirstContentfulPaint() && rootPresContext) { + if (!pc->HasStoppedGeneratingLCP() && rootPresContext) { if (!CurrentPresShellState()->mIsBackgroundOnly) { if (pc->HasEverBuiltInvisibleText() || DisplayListIsContentful(this, aPaintedContents)) { @@ -3429,6 +3430,17 @@ bool nsDisplayBackgroundImage::CreateWebRenderCommands( if (result == ImgDrawResult::NOT_SUPPORTED) { return false; } + + if (nsIContent* content = StyleFrame()->GetContent()) { + if (imgRequestProxy* requestProxy = mBackgroundStyle->StyleBackground() + ->mImage.mLayers[mLayer] + .mImage.GetImageRequest()) { + // LCP don't consider gradient backgrounds. + LCPHelpers::FinalizeLCPEntryForImage(content->AsElement(), requestProxy, + mBounds - ToReferenceFrame()); + } + } + return true; } @@ -7483,6 +7495,10 @@ void nsDisplayText::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) { // the WebRender fallback painting path, and we don't want to issue // recorded commands that are dependent on the visible/building rect. RenderToContext(aCtx, aBuilder, GetPaintRect(aBuilder, aCtx)); + + auto* textFrame = static_cast(mFrame); + LCPTextFrameHelper::MaybeUnionTextFrame(textFrame, + mBounds - ToReferenceFrame()); } bool nsDisplayText::CreateWebRenderCommands( @@ -7568,6 +7584,8 @@ bool nsDisplayText::CreateWebRenderCommands( gfxContext* textDrawer = aBuilder.GetTextContext(aResources, aSc, aManager, this, bounds, deviceOffset); + LCPTextFrameHelper::MaybeUnionTextFrame(f, bounds - ToReferenceFrame()); + aBuilder.StartGroup(this); RenderToContext(textDrawer, aDisplayListBuilder, mVisibleRect, diff --git a/layout/style/ImageLoader.cpp b/layout/style/ImageLoader.cpp index c60f539751d0..c56ecdfd20c1 100644 --- a/layout/style/ImageLoader.cpp +++ b/layout/style/ImageLoader.cpp @@ -12,6 +12,7 @@ #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/LargestContentfulPaint.h" #include "mozilla/dom/ImageTracker.h" #include "nsContentUtils.h" #include "nsIReflowCallback.h" @@ -818,10 +819,16 @@ void ImageLoader::OnLoadComplete(imgIRequest* aRequest) { // may happen during other network events. UnblockOnloadIfNeeded(fwf); } - if (fwf.mFrame->StyleVisibility()->IsVisible()) { - fwf.mFrame->SchedulePaint(); + nsIFrame* frame = fwf.mFrame; + if (frame->StyleVisibility()->IsVisible()) { + frame->SchedulePaint(); + } + + if (StaticPrefs::dom_enable_largest_contentful_paint()) { + LargestContentfulPaint::MaybeProcessImageForElementTiming( + static_cast(aRequest), + frame->GetContent()->AsElement()); } } } - } // namespace mozilla::css diff --git a/layout/svg/SVGImageFrame.cpp b/layout/svg/SVGImageFrame.cpp index da04beee71bd..ae1cf1ded7d1 100644 --- a/layout/svg/SVGImageFrame.cpp +++ b/layout/svg/SVGImageFrame.cpp @@ -28,6 +28,7 @@ #include "mozilla/SVGUtils.h" #include "mozilla/dom/MutationEventBinding.h" #include "mozilla/dom/SVGImageElement.h" +#include "mozilla/dom/LargestContentfulPaint.h" #include "nsIReflowCallback.h" using namespace mozilla::dom; @@ -391,6 +392,12 @@ void SVGImageFrame::PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, LayoutDeviceSize devPxSize(width, height); nsRect destRect(nsPoint(), LayoutDevicePixel::ToAppUnits( devPxSize, appUnitsPerDevPx)); + nsCOMPtr currentRequest = GetCurrentRequest(); + if (currentRequest) { + LCPHelpers::FinalizeLCPEntryForImage( + GetContent()->AsElement(), + static_cast(currentRequest.get()), destRect); + } // Note: Can't use DrawSingleUnscaledImage for the TYPE_VECTOR case. // That method needs our image to have a fixed native width & height, @@ -467,14 +474,7 @@ bool SVGImageFrame::CreateWebRenderCommands( // try to setup the image if (!mImageContainer) { - nsCOMPtr currentRequest; - nsCOMPtr imageLoader = - do_QueryInterface(GetContent()); - if (imageLoader) { - imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, - getter_AddRefs(currentRequest)); - } - + nsCOMPtr currentRequest = GetCurrentRequest(); if (currentRequest) { currentRequest->GetImage(getter_AddRefs(mImageContainer)); } @@ -633,6 +633,14 @@ bool SVGImageFrame::CreateWebRenderCommands( mImageContainer, this, destRect, clipRect, aSc, flags, svgContext, region); + if (nsCOMPtr currentRequest = GetCurrentRequest()) { + LCPHelpers::FinalizeLCPEntryForImage( + GetContent()->AsElement(), + static_cast(currentRequest.get()), + LayoutDeviceRect::ToAppUnits(destRect, appUnitsPerDevPx) - + toReferenceFrame); + } + RefPtr provider; ImgDrawResult drawResult = mImageContainer->GetImageProvider( aManager->LayerManager(), decodeSize, svgContext, region, flags, @@ -783,6 +791,17 @@ bool SVGImageFrame::ReflowFinished() { void SVGImageFrame::ReflowCallbackCanceled() { mReflowCallbackPosted = false; } +already_AddRefed SVGImageFrame::GetCurrentRequest() const { + nsCOMPtr request; + nsCOMPtr imageLoader = + do_QueryInterface(GetContent()); + if (imageLoader) { + imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(request)); + } + return request.forget(); +} + bool SVGImageFrame::IgnoreHitTest() const { switch (Style()->PointerEvents()) { case StylePointerEvents::None: diff --git a/layout/svg/SVGImageFrame.h b/layout/svg/SVGImageFrame.h index 71e291e57953..7f8cbde781e0 100644 --- a/layout/svg/SVGImageFrame.h +++ b/layout/svg/SVGImageFrame.h @@ -118,6 +118,8 @@ class SVGImageFrame final : public nsIFrame, private: bool IgnoreHitTest() const; + already_AddRefed GetCurrentRequest() const; + gfx::Matrix GetRasterImageTransform(int32_t aNativeWidth, int32_t aNativeHeight); gfx::Matrix GetVectorImageTransform(); diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 006c52e493bf..63004725cb97 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -2392,7 +2392,7 @@ # Whether the LargestContentfulPaint API will be gathered and returned by performance observer* - name: dom.enable_largest_contentful_paint type: RelaxedAtomicBool - value: false + value: @IS_NIGHTLY_BUILD@ mirror: always # Whether performance.GetEntries* will contain an entry for the active document