1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/dom/Element.h"
7 #include "nsContentUtils.h"
8 #include "nsLayoutUtils.h"
9 #include "nsRFPService.h"
10 #include "Performance.h"
11 #include "imgRequest.h"
12 #include "PerformanceMainThread.h"
13 #include "LargestContentfulPaint.h"
15 #include "mozilla/dom/BrowsingContext.h"
16 #include "mozilla/dom/DOMIntersectionObserver.h"
17 #include "mozilla/dom/Document.h"
18 #include "mozilla/dom/Element.h"
20 #include "mozilla/PresShell.h"
21 #include "mozilla/Logging.h"
22 #include "mozilla/nsVideoFrame.h"
24 namespace mozilla::dom
{
26 static LazyLogModule
gLCPLogging("LargestContentfulPaint");
28 #define LOG(...) MOZ_LOG(gLCPLogging, LogLevel::Debug, (__VA_ARGS__))
30 NS_IMPL_CYCLE_COLLECTION_INHERITED(LargestContentfulPaint
, PerformanceEntry
,
31 mPerformance
, mURI
, mElement
)
33 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LargestContentfulPaint
)
34 NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry
)
36 NS_IMPL_ADDREF_INHERITED(LargestContentfulPaint
, PerformanceEntry
)
37 NS_IMPL_RELEASE_INHERITED(LargestContentfulPaint
, PerformanceEntry
)
39 static double GetAreaInDoublePixelsFromAppUnits(const nsSize
& aSize
) {
40 return NSAppUnitsToDoublePixels(aSize
.Width(), AppUnitsPerCSSPixel()) *
41 NSAppUnitsToDoublePixels(aSize
.Height(), AppUnitsPerCSSPixel());
44 static double GetAreaInDoublePixelsFromAppUnits(const nsRect
& aRect
) {
45 return NSAppUnitsToDoublePixels(aRect
.Width(), AppUnitsPerCSSPixel()) *
46 NSAppUnitsToDoublePixels(aRect
.Height(), AppUnitsPerCSSPixel());
49 static DOMHighResTimeStamp
GetReducedTimePrecisionDOMHighRes(
50 Performance
* aPerformance
, const TimeStamp
& aRawTimeStamp
) {
51 MOZ_ASSERT(aPerformance
);
52 DOMHighResTimeStamp rawValue
=
53 aPerformance
->GetDOMTiming()->TimeStampToDOMHighRes(aRawTimeStamp
);
54 return nsRFPService::ReduceTimePrecisionAsMSecs(
55 rawValue
, aPerformance
->GetRandomTimelineSeed(),
56 aPerformance
->GetRTPCallerType());
59 LargestContentfulPaint::LargestContentfulPaint(
60 PerformanceMainThread
* aPerformance
, const TimeStamp
& aRenderTime
,
61 const Maybe
<TimeStamp
>& aLoadTime
, const unsigned long aSize
, nsIURI
* aURI
,
62 Element
* aElement
, bool aShouldExposeRenderTime
)
63 : PerformanceEntry(aPerformance
->GetParentObject(), u
""_ns
,
64 kLargestContentfulPaintName
),
65 mPerformance(aPerformance
),
66 mRenderTime(aRenderTime
),
68 mShouldExposeRenderTime(aShouldExposeRenderTime
),
71 MOZ_ASSERT(mPerformance
);
73 // The element could be a pseudo-element
74 if (aElement
->ChromeOnlyAccess()) {
75 mElement
= do_GetWeakReference(Element::FromNodeOrNull(
76 aElement
->FindFirstNonChromeOnlyAccessContent()));
78 mElement
= do_GetWeakReference(aElement
);
81 if (const Element
* element
= GetElement()) {
82 mId
= element
->GetID();
86 JSObject
* LargestContentfulPaint::WrapObject(
87 JSContext
* aCx
, JS::Handle
<JSObject
*> aGivenProto
) {
88 return LargestContentfulPaint_Binding::Wrap(aCx
, this, aGivenProto
);
91 Element
* LargestContentfulPaint::GetElement() const {
92 nsCOMPtr
<Element
> element
= do_QueryReferent(mElement
);
93 return element
? nsContentUtils::GetAnElementForTiming(
94 element
, element
->GetComposedDoc(), nullptr)
98 void LargestContentfulPaint::BufferEntryIfNeeded() {
99 mPerformance
->BufferLargestContentfulPaintEntryIfNeeded(this);
103 bool LCPHelpers::IsQualifiedImageRequest(imgRequest
* aRequest
,
104 Element
* aContainingElement
) {
105 MOZ_ASSERT(aContainingElement
);
110 if (aRequest
->IsChrome()) {
114 if (!aContainingElement
->ChromeOnlyAccess()) {
118 // Exception: this is a poster image of video element
119 if (nsIContent
* parent
= aContainingElement
->GetParent()) {
120 nsVideoFrame
* videoFrame
= do_QueryFrame(parent
->GetPrimaryFrame());
121 if (videoFrame
&& videoFrame
->GetPosterImage() == aContainingElement
) {
126 // Exception: CSS generated images
127 if (aContainingElement
->IsInNativeAnonymousSubtree()) {
128 if (nsINode
* rootParentOrHost
=
130 ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
131 if (!rootParentOrHost
->ChromeOnlyAccess()) {
138 void LargestContentfulPaint::MaybeProcessImageForElementTiming(
139 imgRequestProxy
* aRequest
, Element
* aElement
) {
140 if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
144 MOZ_ASSERT(aRequest
);
145 imgRequest
* request
= aRequest
->GetOwner();
146 if (!LCPHelpers::IsQualifiedImageRequest(request
, aElement
)) {
150 Document
* document
= aElement
->GetComposedDoc();
156 aElement
->GetPresContext(Element::PresContextFor::eForComposedDoc
);
161 PerformanceMainThread
* performance
= pc
->GetPerformanceMainThread();
166 if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging
, LogLevel::Debug
))) {
167 nsCOMPtr
<nsIURI
> uri
;
168 aRequest
->GetURI(getter_AddRefs(uri
));
169 LOG("MaybeProcessImageForElementTiming, Element=%p, URI=%s, "
171 aElement
, uri
? uri
->GetSpecOrDefault().get() : "", performance
);
174 aElement
->SetFlags(ELEMENT_IN_CONTENT_IDENTIFIER_FOR_LCP
);
176 nsTArray
<WeakPtr
<PreloaderBase
>>& imageRequestProxiesForElement
=
177 document
->ContentIdentifiersForLCP().LookupOrInsert(aElement
);
179 if (imageRequestProxiesForElement
.Contains(aRequest
)) {
180 LOG(" The content identifier existed for element=%p and request=%p, "
186 imageRequestProxiesForElement
.AppendElement(aRequest
);
189 uint32_t status
= imgIRequest::STATUS_NONE
;
190 aRequest
->GetImageStatus(&status
);
191 MOZ_ASSERT(status
& imgIRequest::STATUS_LOAD_COMPLETE
);
194 // At this point, the loadTime of the image is known, but
195 // the renderTime is unknown, so it's added to ImagesPendingRendering
196 // as a placeholder, and the corresponding LCP entry will be created
197 // when the renderTime is known.
198 // Here we are exposing the load time of the image which could be
199 // a privacy concern. The spec talks about it at
200 // https://wicg.github.io/element-timing/#sec-security
201 // TLDR: The similar metric can be obtained by ResourceTiming
202 // API and onload handlers already, so this is not exposing anything
204 LOG(" Added a pending image rendering");
205 performance
->AddImagesPendingRendering(
206 ImagePendingRendering
{aElement
, aRequest
, TimeStamp::Now()});
209 bool LCPHelpers::CanFinalizeLCPEntry(const nsIFrame
* aFrame
) {
210 if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
218 nsPresContext
* presContext
= aFrame
->PresContext();
219 return !presContext
->HasStoppedGeneratingLCP() &&
220 presContext
->GetPerformanceMainThread();
223 void LCPHelpers::FinalizeLCPEntryForImage(
224 Element
* aContainingBlock
, imgRequestProxy
* aImgRequestProxy
,
225 const nsRect
& aTargetRectRelativeToSelf
) {
226 LOG("FinalizeLCPEntryForImage element=%p image=%p", aContainingBlock
,
228 if (!aImgRequestProxy
) {
232 if (!IsQualifiedImageRequest(aImgRequestProxy
->GetOwner(),
237 nsIFrame
* frame
= aContainingBlock
->GetPrimaryFrame();
239 if (!CanFinalizeLCPEntry(frame
)) {
243 PerformanceMainThread
* performance
=
244 frame
->PresContext()->GetPerformanceMainThread();
245 MOZ_ASSERT(performance
);
247 if (performance
->HasDispatchedInputEvent() ||
248 performance
->HasDispatchedScrollEvent()) {
252 if (!performance
->IsPendingLCPCandidate(aContainingBlock
, aImgRequestProxy
)) {
256 imgRequestProxy::LCPTimings
& lcpTimings
= aImgRequestProxy
->GetLCPTimings();
257 if (!lcpTimings
.AreSet()) {
261 imgRequest
* request
= aImgRequestProxy
->GetOwner();
264 nsCOMPtr
<nsIURI
> requestURI
;
265 aImgRequestProxy
->GetURI(getter_AddRefs(requestURI
));
267 const bool taoPassed
=
268 request
->ShouldReportRenderTimeForLCP() || request
->IsData();
270 RefPtr
<LargestContentfulPaint
> entry
= new LargestContentfulPaint(
271 performance
, lcpTimings
.mRenderTime
.ref(), lcpTimings
.mLoadTime
, 0,
272 requestURI
, aContainingBlock
, taoPassed
);
274 entry
->UpdateSize(aContainingBlock
, aTargetRectRelativeToSelf
, performance
,
277 // Resets the LCPTiming so that unless this (element, image) pair goes
278 // through PerformanceMainThread::ProcessElementTiming again, they
279 // won't generate new LCP entries.
282 // If area is less than or equal to document’s largest contentful paint size,
284 if (!performance
->UpdateLargestContentfulPaintSize(entry
->Size())) {
287 " This paint(%lu) is not greater than the largest paint (%lf)that "
289 "reported so far, return",
290 entry
->Size(), performance
->GetLargestContentfulPaintSize());
297 DOMHighResTimeStamp
LargestContentfulPaint::RenderTime() const {
298 if (!mShouldExposeRenderTime
) {
301 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mRenderTime
);
304 DOMHighResTimeStamp
LargestContentfulPaint::LoadTime() const {
305 if (mLoadTime
.isNothing()) {
309 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mLoadTime
.ref());
312 DOMHighResTimeStamp
LargestContentfulPaint::StartTime() const {
313 if (mShouldExposeRenderTime
) {
314 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mRenderTime
);
317 if (mLoadTime
.isNothing()) {
321 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mLoadTime
.ref());
325 Element
* LargestContentfulPaint::GetContainingBlockForTextFrame(
326 const nsTextFrame
* aTextFrame
) {
327 nsIFrame
* containingFrame
= aTextFrame
->GetContainingBlock();
328 MOZ_ASSERT(containingFrame
);
329 return Element::FromNodeOrNull(containingFrame
->GetContent());
332 void LargestContentfulPaint::QueueEntry() {
333 LOG("QueueEntry entry=%p", this);
334 mPerformance
->QueueLargestContentfulPaintEntry(this);
336 ReportLCPToNavigationTimings();
339 void LargestContentfulPaint::GetUrl(nsAString
& aUrl
) {
341 CopyUTF8toUTF16(mURI
->GetSpecOrDefault(), aUrl
);
345 void LargestContentfulPaint::UpdateSize(
346 const Element
* aContainingBlock
, const nsRect
& aTargetRectRelativeToSelf
,
347 const PerformanceMainThread
* aPerformance
, bool aIsImage
) {
348 nsIFrame
* frame
= aContainingBlock
->GetPrimaryFrame();
351 nsIFrame
* rootFrame
= frame
->PresShell()->GetRootFrame();
356 if (frame
->Style()->IsInOpacityZeroSubtree()) {
357 LOG(" Opacity:0 return");
361 // The following size computation is based on a pending pull request
362 // https://github.com/w3c/largest-contentful-paint/pull/99
364 // Let visibleDimensions be concreteDimensions, adjusted for positioning
365 // by object-position or background-position and element’s content box.
366 const nsRect
& visibleDimensions
= aTargetRectRelativeToSelf
;
368 // Let clientContentRect be the smallest DOMRectReadOnly containing
369 // visibleDimensions with element’s transforms applied.
370 nsRect clientContentRect
= nsLayoutUtils::TransformFrameRectToAncestor(
371 frame
, visibleDimensions
, rootFrame
);
373 // Let intersectionRect be the value returned by the intersection rect
374 // algorithm using element as the target and viewport as the root.
375 // (From https://wicg.github.io/element-timing/#sec-report-image-element)
376 IntersectionInput input
= DOMIntersectionObserver::ComputeInput(
377 *frame
->PresContext()->Document(), rootFrame
->GetContent(), nullptr);
378 const IntersectionOutput output
=
379 DOMIntersectionObserver::Intersect(input
, *aContainingBlock
);
381 Maybe
<nsRect
> intersectionRect
= output
.mIntersectionRect
;
383 if (intersectionRect
.isNothing()) {
384 LOG(" The intersectionRect is nothing for Element=%p. return.",
389 // Let intersectingClientContentRect be the intersection of clientContentRect
390 // with intersectionRect.
391 Maybe
<nsRect
> intersectionWithContentRect
=
392 clientContentRect
.EdgeInclusiveIntersection(intersectionRect
.value());
394 if (intersectionWithContentRect
.isNothing()) {
395 LOG(" The intersectionWithContentRect is nothing for Element=%p. return.",
400 nsRect renderedRect
= intersectionWithContentRect
.value();
402 double area
= GetAreaInDoublePixelsFromAppUnits(renderedRect
);
404 double viewport
= GetAreaInDoublePixelsFromAppUnits(input
.mRootRect
);
406 LOG(" Viewport = %f, RenderRect = %f.", viewport
, area
);
407 // We don't want to report things that take the entire viewport.
408 if (area
>= viewport
) {
409 LOG(" The renderedRect is at least same as the area of the "
410 "viewport for Element=%p, return.",
415 Maybe
<nsSize
> intrinsicSize
= frame
->GetIntrinsicSize().ToSize();
416 const bool hasIntrinsicSize
= intrinsicSize
&& !intrinsicSize
->IsEmpty();
418 if (aIsImage
&& hasIntrinsicSize
) {
419 // Let (naturalWidth, naturalHeight) be imageRequest’s natural dimension.
420 // Let naturalArea be naturalWidth * naturalHeight.
422 GetAreaInDoublePixelsFromAppUnits(intrinsicSize
.value());
424 LOG(" naturalArea = %f", naturalArea
);
426 // Let boundingClientArea be clientContentRect’s width * clientContentRect’s
428 double boundingClientArea
=
429 NSAppUnitsToDoublePixels(clientContentRect
.Width(),
430 AppUnitsPerCSSPixel()) *
431 NSAppUnitsToDoublePixels(clientContentRect
.Height(),
432 AppUnitsPerCSSPixel());
433 LOG(" boundingClientArea = %f", boundingClientArea
);
435 // Let scaleFactor be boundingClientArea / naturalArea.
436 double scaleFactor
= boundingClientArea
/ naturalArea
;
437 LOG(" scaleFactor = %f", scaleFactor
);
439 // If scaleFactor is greater than 1, then divide area by scaleFactor.
440 if (scaleFactor
> 1) {
441 LOG(" area before sacled doown %f", area
);
442 area
= area
/ scaleFactor
;
450 void LCPTextFrameHelper::MaybeUnionTextFrame(
451 nsTextFrame
* aTextFrame
, const nsRect
& aRelativeToSelfRect
) {
452 if (!StaticPrefs::dom_enable_largest_contentful_paint() ||
453 aTextFrame
->PresContext()->HasStoppedGeneratingLCP()) {
457 Element
* containingBlock
=
458 LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame
);
459 if (!containingBlock
||
460 // If element is contained in doc’s set of elements with rendered text,
462 containingBlock
->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT
) ||
463 containingBlock
->ChromeOnlyAccess()) {
467 MOZ_ASSERT(containingBlock
->GetPrimaryFrame());
469 PerformanceMainThread
* perf
=
470 aTextFrame
->PresContext()->GetPerformanceMainThread();
475 auto& unionRect
= perf
->GetTextFrameUnions().LookupOrInsert(containingBlock
);
476 unionRect
= unionRect
.Union(aRelativeToSelfRect
);
479 void LCPHelpers::FinalizeLCPEntryForText(
480 PerformanceMainThread
* aPerformance
, const TimeStamp
& aRenderTime
,
481 Element
* aContainingBlock
, const nsRect
& aTargetRectRelativeToSelf
,
482 const nsPresContext
* aPresContext
) {
483 MOZ_ASSERT(aPerformance
);
484 LOG("FinalizeLCPEntryForText element=%p", aContainingBlock
);
486 if (!aContainingBlock
->GetPrimaryFrame()) {
489 MOZ_ASSERT(CanFinalizeLCPEntry(aContainingBlock
->GetPrimaryFrame()));
490 MOZ_ASSERT(!aContainingBlock
->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT
));
491 MOZ_ASSERT(!aContainingBlock
->ChromeOnlyAccess());
493 aContainingBlock
->SetFlags(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT
);
495 RefPtr
<LargestContentfulPaint
> entry
= new LargestContentfulPaint(
496 aPerformance
, aRenderTime
, Nothing(), 0, nullptr, aContainingBlock
, true);
498 entry
->UpdateSize(aContainingBlock
, aTargetRectRelativeToSelf
, aPerformance
,
500 // If area is less than or equal to document’s largest contentful paint size,
502 if (!aPerformance
->UpdateLargestContentfulPaintSize(entry
->Size())) {
503 LOG(" This paint(%lu) is not greater than the largest paint (%lf)that "
505 "reported so far, return",
506 entry
->Size(), aPerformance
->GetLargestContentfulPaintSize());
512 void LargestContentfulPaint::ReportLCPToNavigationTimings() {
513 nsCOMPtr
<Element
> element
= do_QueryReferent(mElement
);
518 const Document
* document
= element
->OwnerDoc();
520 MOZ_ASSERT(document
);
522 nsDOMNavigationTiming
* timing
= document
->GetNavigationTiming();
524 if (MOZ_UNLIKELY(!timing
)) {
528 if (document
->IsResourceDoc()) {
532 if (BrowsingContext
* browsingContext
= document
->GetBrowsingContext()) {
533 if (browsingContext
->GetEmbeddedInContentDocument()) {
538 if (!document
->IsTopLevelContentDocument()) {
541 timing
->NotifyLargestContentfulRenderForRootContentDocument(
542 GetReducedTimePrecisionDOMHighRes(mPerformance
, mRenderTime
));
544 } // namespace mozilla::dom