no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / dom / performance / LargestContentfulPaint.cpp
blob3382a2fbade8f6a0457c21b5a78b73a8e6f808ac
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),
72 mLoadTime(aLoadTime),
73 mShouldExposeRenderTime(aShouldExposeRenderTime),
74 mSize(aSize),
75 mURI(aURI),
76 mLCPImageEntryKey(aLCPImageEntryKey) {
77 MOZ_ASSERT(mPerformance);
78 MOZ_ASSERT(aElement);
79 // The element could be a pseudo-element
80 if (aElement->ChromeOnlyAccess()) {
81 mElement = do_GetWeakReference(Element::FromNodeOrNull(
82 aElement->FindFirstNonChromeOnlyAccessContent()));
83 } else {
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)
101 : nullptr;
104 void LargestContentfulPaint::BufferEntryIfNeeded() {
105 MOZ_ASSERT(mLCPImageEntryKey.isNothing());
106 mPerformance->BufferLargestContentfulPaintEntryIfNeeded(this);
109 /* static*/
110 bool LCPHelpers::IsQualifiedImageRequest(imgRequest* aRequest,
111 Element* aContainingElement) {
112 MOZ_ASSERT(aContainingElement);
113 if (!aRequest) {
114 return false;
117 if (aRequest->IsChrome()) {
118 return false;
121 if (!aContainingElement->ChromeOnlyAccess()) {
122 return true;
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) {
129 return true;
133 // Exception: CSS generated images
134 if (aContainingElement->IsInNativeAnonymousSubtree()) {
135 if (nsINode* rootParentOrHost =
136 aContainingElement
137 ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
138 if (!rootParentOrHost->ChromeOnlyAccess()) {
139 return true;
143 return false;
145 void LargestContentfulPaint::MaybeProcessImageForElementTiming(
146 imgRequestProxy* aRequest, Element* aElement) {
147 if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
148 return;
151 MOZ_ASSERT(aRequest);
152 imgRequest* request = aRequest->GetOwner();
153 if (!LCPHelpers::IsQualifiedImageRequest(request, aElement)) {
154 return;
157 Document* document = aElement->GetComposedDoc();
158 if (!document) {
159 return;
162 nsPresContext* pc =
163 aElement->GetPresContext(Element::PresContextFor::eForComposedDoc);
164 if (!pc) {
165 return;
168 PerformanceMainThread* performance = pc->GetPerformanceMainThread();
169 if (!performance) {
170 return;
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, "
177 "performance=%p ",
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, "
184 "return.",
185 aElement, aRequest);
186 return;
189 #ifdef DEBUG
190 uint32_t status = imgIRequest::STATUS_NONE;
191 aRequest->GetImageStatus(&status);
192 MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE);
193 #endif
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
204 // new.
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()) {
212 return false;
215 if (!aFrame) {
216 return false;
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) {
229 return;
232 if (!IsQualifiedImageRequest(aImgRequestProxy->GetOwner(),
233 aContainingBlock)) {
234 return;
237 nsIFrame* frame = aContainingBlock->GetPrimaryFrame();
239 if (!CanFinalizeLCPEntry(frame)) {
240 return;
243 PerformanceMainThread* performance =
244 frame->PresContext()->GetPerformanceMainThread();
245 MOZ_ASSERT(performance);
247 RefPtr<LargestContentfulPaint> entry =
248 performance->GetImageLCPEntry(aContainingBlock, aImgRequestProxy);
249 if (!entry) {
250 LOG(" No Image Entry");
251 return;
253 entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, performance,
254 true);
255 // If area is less than or equal to document’s largest contentful paint size,
256 // return.
257 if (!performance->UpdateLargestContentfulPaintSize(entry->Size())) {
258 LOG(
260 " This paint(%lu) is not greater than the largest paint (%lf)that "
261 "we've "
262 "reported so far, return",
263 entry->Size(), performance->GetLargestContentfulPaintSize());
264 return;
267 entry->QueueEntry();
270 DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const {
271 if (!mShouldExposeRenderTime) {
272 return 0;
274 return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
277 DOMHighResTimeStamp LargestContentfulPaint::LoadTime() const {
278 if (mLoadTime.isNothing()) {
279 return 0;
282 return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
285 DOMHighResTimeStamp LargestContentfulPaint::StartTime() const {
286 if (mShouldExposeRenderTime) {
287 return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
290 if (mLoadTime.isNothing()) {
291 return 0;
294 return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
297 /* static */
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) {
313 if (mURI) {
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();
322 MOZ_ASSERT(frame);
324 nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
325 if (!rootFrame) {
326 return;
329 if (frame->Style()->IsInOpacityZeroSubtree()) {
330 LOG(" Opacity:0 return");
331 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.",
358 aContainingBlock);
359 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.",
369 aContainingBlock);
370 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.",
384 aContainingBlock);
385 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.
394 double naturalArea =
395 GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value());
397 LOG(" naturalArea = %f", naturalArea);
399 // Let boundingClientArea be clientContentRect’s width * clientContentRect’s
400 // height.
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;
419 MOZ_ASSERT(!mSize);
420 mSize = area;
423 void LCPTextFrameHelper::MaybeUnionTextFrame(
424 nsTextFrame* aTextFrame, const nsRect& aRelativeToSelfRect) {
425 if (!StaticPrefs::dom_enable_largest_contentful_paint() ||
426 aTextFrame->PresContext()->HasStoppedGeneratingLCP()) {
427 return;
430 Element* containingBlock =
431 LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame);
432 if (!containingBlock ||
433 // If element is contained in doc’s set of elements with rendered text,
434 // continue
435 containingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT) ||
436 containingBlock->ChromeOnlyAccess()) {
437 return;
440 MOZ_ASSERT(containingBlock->GetPrimaryFrame());
442 PerformanceMainThread* perf =
443 aTextFrame->PresContext()->GetPerformanceMainThread();
444 if (!perf) {
445 return;
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, "
464 "aRenderTime=%f\n",
465 aElement, aRequestProxy, uri->GetSpecOrDefault().get(),
466 GetReducedTimePrecisionDOMHighRes(aPerformance, aLoadTime),
467 GetReducedTimePrecisionDOMHighRes(aPerformance, aRenderTime));
469 if (aPerformance->HasDispatchedInputEvent() ||
470 aPerformance->HasDispatchedScrollEvent()) {
471 return;
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.
481 MOZ_ASSERT(request);
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
486 // security reasons.
488 // At this point, we have all the information about the entry
489 // except the size.
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.",
495 entry.get());
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()) {
507 return;
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,
520 false);
521 // If area is less than or equal to document’s largest contentful paint size,
522 // return.
523 if (!aPerformance->UpdateLargestContentfulPaintSize(entry->Size())) {
524 LOG(" This paint(%lu) is not greater than the largest paint (%lf)that "
525 "we've "
526 "reported so far, return",
527 entry->Size(), aPerformance->GetLargestContentfulPaintSize());
528 return;
530 entry->QueueEntry();
533 void LargestContentfulPaint::ReportLCPToNavigationTimings() {
534 nsCOMPtr<Element> element = do_QueryReferent(mElement);
535 if (!element) {
536 return;
539 const Document* document = element->OwnerDoc();
541 MOZ_ASSERT(document);
543 nsDOMNavigationTiming* timing = document->GetNavigationTiming();
545 if (MOZ_UNLIKELY(!timing)) {
546 return;
549 if (document->IsResourceDoc()) {
550 return;
553 if (BrowsingContext* browsingContext = document->GetBrowsingContext()) {
554 if (browsingContext->GetEmbeddedInContentDocument()) {
555 return;
559 if (!document->IsTopLevelContentDocument()) {
560 return;
562 timing->NotifyLargestContentfulRenderForRootContentDocument(
563 GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime));
565 } // namespace mozilla::dom