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 ImagePendingRendering::ImagePendingRendering(
60 const LCPImageEntryKey
& aLCPImageEntryKey
, const TimeStamp
& aLoadTime
)
61 : mLCPImageEntryKey(aLCPImageEntryKey
), mLoadTime(aLoadTime
) {}
63 LargestContentfulPaint::LargestContentfulPaint(
64 PerformanceMainThread
* aPerformance
, const TimeStamp
& aRenderTime
,
65 const Maybe
<TimeStamp
>& aLoadTime
, const unsigned long aSize
, nsIURI
* aURI
,
66 Element
* aElement
, const Maybe
<const LCPImageEntryKey
>& aLCPImageEntryKey
,
67 bool aShouldExposeRenderTime
)
68 : PerformanceEntry(aPerformance
->GetParentObject(), u
""_ns
,
69 kLargestContentfulPaintName
),
70 mPerformance(aPerformance
),
71 mRenderTime(aRenderTime
),
73 mShouldExposeRenderTime(aShouldExposeRenderTime
),
76 mLCPImageEntryKey(aLCPImageEntryKey
) {
77 MOZ_ASSERT(mPerformance
);
79 // The element could be a pseudo-element
80 if (aElement
->ChromeOnlyAccess()) {
81 mElement
= do_GetWeakReference(Element::FromNodeOrNull(
82 aElement
->FindFirstNonChromeOnlyAccessContent()));
84 mElement
= do_GetWeakReference(aElement
);
87 if (const Element
* element
= GetElement()) {
88 mId
= element
->GetID();
92 JSObject
* LargestContentfulPaint::WrapObject(
93 JSContext
* aCx
, JS::Handle
<JSObject
*> aGivenProto
) {
94 return LargestContentfulPaint_Binding::Wrap(aCx
, this, aGivenProto
);
97 Element
* LargestContentfulPaint::GetElement() const {
98 nsCOMPtr
<Element
> element
= do_QueryReferent(mElement
);
99 return element
? nsContentUtils::GetAnElementForTiming(
100 element
, element
->GetComposedDoc(), nullptr)
104 void LargestContentfulPaint::BufferEntryIfNeeded() {
105 MOZ_ASSERT(mLCPImageEntryKey
.isNothing());
106 mPerformance
->BufferLargestContentfulPaintEntryIfNeeded(this);
110 bool LCPHelpers::IsQualifiedImageRequest(imgRequest
* aRequest
,
111 Element
* aContainingElement
) {
112 MOZ_ASSERT(aContainingElement
);
117 if (aRequest
->IsChrome()) {
121 if (!aContainingElement
->ChromeOnlyAccess()) {
125 // Exception: this is a poster image of video element
126 if (nsIContent
* parent
= aContainingElement
->GetParent()) {
127 nsVideoFrame
* videoFrame
= do_QueryFrame(parent
->GetPrimaryFrame());
128 if (videoFrame
&& videoFrame
->GetPosterImage() == aContainingElement
) {
133 // Exception: CSS generated images
134 if (aContainingElement
->IsInNativeAnonymousSubtree()) {
135 if (nsINode
* rootParentOrHost
=
137 ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
138 if (!rootParentOrHost
->ChromeOnlyAccess()) {
145 void LargestContentfulPaint::MaybeProcessImageForElementTiming(
146 imgRequestProxy
* aRequest
, Element
* aElement
) {
147 if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
151 MOZ_ASSERT(aRequest
);
152 imgRequest
* request
= aRequest
->GetOwner();
153 if (!LCPHelpers::IsQualifiedImageRequest(request
, aElement
)) {
157 Document
* document
= aElement
->GetComposedDoc();
163 aElement
->GetPresContext(Element::PresContextFor::eForComposedDoc
);
168 PerformanceMainThread
* performance
= pc
->GetPerformanceMainThread();
173 if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging
, LogLevel::Debug
))) {
174 nsCOMPtr
<nsIURI
> uri
;
175 aRequest
->GetURI(getter_AddRefs(uri
));
176 LOG("MaybeProcessImageForElementTiming, Element=%p, URI=%s, "
178 aElement
, uri
? uri
->GetSpecOrDefault().get() : "", performance
);
181 const LCPImageEntryKey entryKey
= LCPImageEntryKey(aElement
, aRequest
);
182 if (!document
->ContentIdentifiersForLCP().EnsureInserted(entryKey
)) {
183 LOG(" The content identifier existed for element=%p and request=%p, "
190 uint32_t status
= imgIRequest::STATUS_NONE
;
191 aRequest
->GetImageStatus(&status
);
192 MOZ_ASSERT(status
& imgIRequest::STATUS_LOAD_COMPLETE
);
195 // At this point, the loadTime of the image is known, but
196 // the renderTime is unknown, so it's added to ImagesPendingRendering
197 // as a placeholder, and the corresponding LCP entry will be created
198 // when the renderTime is known.
199 // Here we are exposing the load time of the image which could be
200 // a privacy concern. The spec talks about it at
201 // https://wicg.github.io/element-timing/#sec-security
202 // TLDR: The similar metric can be obtained by ResourceTiming
203 // API and onload handlers already, so this is not exposing anything
205 LOG(" Added a pending image rendering");
206 performance
->AddImagesPendingRendering(
207 ImagePendingRendering
{entryKey
, TimeStamp::Now()});
210 bool LCPHelpers::CanFinalizeLCPEntry(const nsIFrame
* aFrame
) {
211 if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
219 nsPresContext
* presContext
= aFrame
->PresContext();
220 return !presContext
->HasStoppedGeneratingLCP() &&
221 presContext
->GetPerformanceMainThread();
224 void LCPHelpers::FinalizeLCPEntryForImage(
225 Element
* aContainingBlock
, imgRequestProxy
* aImgRequestProxy
,
226 const nsRect
& aTargetRectRelativeToSelf
) {
227 LOG("FinalizeLCPEntryForImage element=%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 RefPtr
<LargestContentfulPaint
> entry
=
248 performance
->GetImageLCPEntry(aContainingBlock
, aImgRequestProxy
);
250 LOG(" No Image Entry");
253 entry
->UpdateSize(aContainingBlock
, aTargetRectRelativeToSelf
, performance
,
255 // If area is less than or equal to document’s largest contentful paint size,
257 if (!performance
->UpdateLargestContentfulPaintSize(entry
->Size())) {
260 " This paint(%lu) is not greater than the largest paint (%lf)that "
262 "reported so far, return",
263 entry
->Size(), performance
->GetLargestContentfulPaintSize());
270 DOMHighResTimeStamp
LargestContentfulPaint::RenderTime() const {
271 if (!mShouldExposeRenderTime
) {
274 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mRenderTime
);
277 DOMHighResTimeStamp
LargestContentfulPaint::LoadTime() const {
278 if (mLoadTime
.isNothing()) {
282 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mLoadTime
.ref());
285 DOMHighResTimeStamp
LargestContentfulPaint::StartTime() const {
286 if (mShouldExposeRenderTime
) {
287 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mRenderTime
);
290 if (mLoadTime
.isNothing()) {
294 return GetReducedTimePrecisionDOMHighRes(mPerformance
, mLoadTime
.ref());
298 Element
* LargestContentfulPaint::GetContainingBlockForTextFrame(
299 const nsTextFrame
* aTextFrame
) {
300 nsIFrame
* containingFrame
= aTextFrame
->GetContainingBlock();
301 MOZ_ASSERT(containingFrame
);
302 return Element::FromNodeOrNull(containingFrame
->GetContent());
305 void LargestContentfulPaint::QueueEntry() {
306 LOG("QueueEntry entry=%p", this);
307 mPerformance
->QueueLargestContentfulPaintEntry(this);
309 ReportLCPToNavigationTimings();
312 void LargestContentfulPaint::GetUrl(nsAString
& aUrl
) {
314 CopyUTF8toUTF16(mURI
->GetSpecOrDefault(), aUrl
);
318 void LargestContentfulPaint::UpdateSize(
319 const Element
* aContainingBlock
, const nsRect
& aTargetRectRelativeToSelf
,
320 const PerformanceMainThread
* aPerformance
, bool aIsImage
) {
321 nsIFrame
* frame
= aContainingBlock
->GetPrimaryFrame();
324 nsIFrame
* rootFrame
= frame
->PresShell()->GetRootFrame();
329 if (frame
->Style()->IsInOpacityZeroSubtree()) {
330 LOG(" Opacity:0 return");
334 // The following size computation is based on a pending pull request
335 // https://github.com/w3c/largest-contentful-paint/pull/99
337 // Let visibleDimensions be concreteDimensions, adjusted for positioning
338 // by object-position or background-position and element’s content box.
339 const nsRect
& visibleDimensions
= aTargetRectRelativeToSelf
;
341 // Let clientContentRect be the smallest DOMRectReadOnly containing
342 // visibleDimensions with element’s transforms applied.
343 nsRect clientContentRect
= nsLayoutUtils::TransformFrameRectToAncestor(
344 frame
, visibleDimensions
, rootFrame
);
346 // Let intersectionRect be the value returned by the intersection rect
347 // algorithm using element as the target and viewport as the root.
348 // (From https://wicg.github.io/element-timing/#sec-report-image-element)
349 IntersectionInput input
= DOMIntersectionObserver::ComputeInput(
350 *frame
->PresContext()->Document(), rootFrame
->GetContent(), nullptr);
351 const IntersectionOutput output
=
352 DOMIntersectionObserver::Intersect(input
, *aContainingBlock
);
354 Maybe
<nsRect
> intersectionRect
= output
.mIntersectionRect
;
356 if (intersectionRect
.isNothing()) {
357 LOG(" The intersectionRect is nothing for Element=%p. return.",
362 // Let intersectingClientContentRect be the intersection of clientContentRect
363 // with intersectionRect.
364 Maybe
<nsRect
> intersectionWithContentRect
=
365 clientContentRect
.EdgeInclusiveIntersection(intersectionRect
.value());
367 if (intersectionWithContentRect
.isNothing()) {
368 LOG(" The intersectionWithContentRect is nothing for Element=%p. return.",
373 nsRect renderedRect
= intersectionWithContentRect
.value();
375 double area
= GetAreaInDoublePixelsFromAppUnits(renderedRect
);
377 double viewport
= GetAreaInDoublePixelsFromAppUnits(input
.mRootRect
);
379 LOG(" Viewport = %f, RenderRect = %f.", viewport
, area
);
380 // We don't want to report things that take the entire viewport.
381 if (area
>= viewport
) {
382 LOG(" The renderedRect is at least same as the area of the "
383 "viewport for Element=%p, return.",
388 Maybe
<nsSize
> intrinsicSize
= frame
->GetIntrinsicSize().ToSize();
389 const bool hasIntrinsicSize
= intrinsicSize
&& !intrinsicSize
->IsEmpty();
391 if (aIsImage
&& hasIntrinsicSize
) {
392 // Let (naturalWidth, naturalHeight) be imageRequest’s natural dimension.
393 // Let naturalArea be naturalWidth * naturalHeight.
395 GetAreaInDoublePixelsFromAppUnits(intrinsicSize
.value());
397 LOG(" naturalArea = %f", naturalArea
);
399 // Let boundingClientArea be clientContentRect’s width * clientContentRect’s
401 double boundingClientArea
=
402 NSAppUnitsToDoublePixels(clientContentRect
.Width(),
403 AppUnitsPerCSSPixel()) *
404 NSAppUnitsToDoublePixels(clientContentRect
.Height(),
405 AppUnitsPerCSSPixel());
406 LOG(" boundingClientArea = %f", boundingClientArea
);
408 // Let scaleFactor be boundingClientArea / naturalArea.
409 double scaleFactor
= boundingClientArea
/ naturalArea
;
410 LOG(" scaleFactor = %f", scaleFactor
);
412 // If scaleFactor is greater than 1, then divide area by scaleFactor.
413 if (scaleFactor
> 1) {
414 LOG(" area before sacled doown %f", area
);
415 area
= area
/ scaleFactor
;
423 void LCPTextFrameHelper::MaybeUnionTextFrame(
424 nsTextFrame
* aTextFrame
, const nsRect
& aRelativeToSelfRect
) {
425 if (!StaticPrefs::dom_enable_largest_contentful_paint() ||
426 aTextFrame
->PresContext()->HasStoppedGeneratingLCP()) {
430 Element
* containingBlock
=
431 LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame
);
432 if (!containingBlock
||
433 // If element is contained in doc’s set of elements with rendered text,
435 containingBlock
->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT
) ||
436 containingBlock
->ChromeOnlyAccess()) {
440 MOZ_ASSERT(containingBlock
->GetPrimaryFrame());
442 PerformanceMainThread
* perf
=
443 aTextFrame
->PresContext()->GetPerformanceMainThread();
448 auto& unionRect
= perf
->GetTextFrameUnions().LookupOrInsert(containingBlock
);
449 unionRect
= unionRect
.Union(aRelativeToSelfRect
);
452 void LCPHelpers::CreateLCPEntryForImage(
453 PerformanceMainThread
* aPerformance
, Element
* aElement
,
454 imgRequestProxy
* aRequestProxy
, const TimeStamp
& aLoadTime
,
455 const TimeStamp
& aRenderTime
, const LCPImageEntryKey
& aImageEntryKey
) {
456 MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
457 MOZ_ASSERT(aRequestProxy
);
458 MOZ_ASSERT(aPerformance
);
459 if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging
, LogLevel::Debug
))) {
460 nsCOMPtr
<nsIURI
> uri
;
461 aRequestProxy
->GetURI(getter_AddRefs(uri
));
462 LOG("CreateLCPEntryForImage "
463 "Element=%p, aRequestProxy=%p, URI=%s loadTime=%f, "
465 aElement
, aRequestProxy
, uri
->GetSpecOrDefault().get(),
466 GetReducedTimePrecisionDOMHighRes(aPerformance
, aLoadTime
),
467 GetReducedTimePrecisionDOMHighRes(aPerformance
, aRenderTime
));
469 if (aPerformance
->HasDispatchedInputEvent() ||
470 aPerformance
->HasDispatchedScrollEvent()) {
474 // Let url be the empty string.
475 // If imageRequest is not null, set url to be imageRequest’s request URL.
476 nsCOMPtr
<nsIURI
> requestURI
;
477 aRequestProxy
->GetURI(getter_AddRefs(requestURI
));
479 imgRequest
* request
= aRequestProxy
->GetOwner();
480 // We should never get here unless request is valid.
483 bool taoPassed
= request
->ShouldReportRenderTimeForLCP() || request
->IsData();
484 // https://wicg.github.io/element-timing/#report-image-element-timing
485 // For TAO failed requests, the renderTime is exposed as 0 for
488 // At this point, we have all the information about the entry
490 RefPtr
<LargestContentfulPaint
> entry
= new LargestContentfulPaint(
491 aPerformance
, aRenderTime
, Some(aLoadTime
), 0, requestURI
, aElement
,
492 Some(aImageEntryKey
), taoPassed
);
494 LOG(" Upsert a LargestContentfulPaint entry=%p to LCPEntryMap.",
496 aPerformance
->StoreImageLCPEntry(aElement
, aRequestProxy
, entry
);
499 void LCPHelpers::FinalizeLCPEntryForText(
500 PerformanceMainThread
* aPerformance
, const TimeStamp
& aRenderTime
,
501 Element
* aContainingBlock
, const nsRect
& aTargetRectRelativeToSelf
,
502 const nsPresContext
* aPresContext
) {
503 MOZ_ASSERT(aPerformance
);
504 LOG("FinalizeLCPEntryForText element=%p", aContainingBlock
);
506 if (!aContainingBlock
->GetPrimaryFrame()) {
509 MOZ_ASSERT(CanFinalizeLCPEntry(aContainingBlock
->GetPrimaryFrame()));
510 MOZ_ASSERT(!aContainingBlock
->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT
));
511 MOZ_ASSERT(!aContainingBlock
->ChromeOnlyAccess());
513 aContainingBlock
->SetFlags(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT
);
515 RefPtr
<LargestContentfulPaint
> entry
=
516 new LargestContentfulPaint(aPerformance
, aRenderTime
, Nothing(), 0,
517 nullptr, aContainingBlock
, Nothing(), true);
519 entry
->UpdateSize(aContainingBlock
, aTargetRectRelativeToSelf
, aPerformance
,
521 // If area is less than or equal to document’s largest contentful paint size,
523 if (!aPerformance
->UpdateLargestContentfulPaintSize(entry
->Size())) {
524 LOG(" This paint(%lu) is not greater than the largest paint (%lf)that "
526 "reported so far, return",
527 entry
->Size(), aPerformance
->GetLargestContentfulPaintSize());
533 void LargestContentfulPaint::ReportLCPToNavigationTimings() {
534 nsCOMPtr
<Element
> element
= do_QueryReferent(mElement
);
539 const Document
* document
= element
->OwnerDoc();
541 MOZ_ASSERT(document
);
543 nsDOMNavigationTiming
* timing
= document
->GetNavigationTiming();
545 if (MOZ_UNLIKELY(!timing
)) {
549 if (document
->IsResourceDoc()) {
553 if (BrowsingContext
* browsingContext
= document
->GetBrowsingContext()) {
554 if (browsingContext
->GetEmbeddedInContentDocument()) {
559 if (!document
->IsTopLevelContentDocument()) {
562 timing
->NotifyLargestContentfulRenderForRootContentDocument(
563 GetReducedTimePrecisionDOMHighRes(mPerformance
, mRenderTime
));
565 } // namespace mozilla::dom