Bug 1936278 - Prevent search mode chiclet from being dismissed when clicking in page...
[gecko.git] / dom / performance / LargestContentfulPaint.cpp
blob3484d47d92644272ff762306cad54f249b555bc2
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),
67 mLoadTime(aLoadTime),
68 mShouldExposeRenderTime(aShouldExposeRenderTime),
69 mSize(aSize),
70 mURI(aURI) {
71 MOZ_ASSERT(mPerformance);
72 MOZ_ASSERT(aElement);
73 // The element could be a pseudo-element
74 if (aElement->ChromeOnlyAccess()) {
75 mElement = do_GetWeakReference(Element::FromNodeOrNull(
76 aElement->FindFirstNonChromeOnlyAccessContent()));
77 } else {
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)
95 : nullptr;
98 void LargestContentfulPaint::BufferEntryIfNeeded() {
99 mPerformance->BufferLargestContentfulPaintEntryIfNeeded(this);
102 /* static*/
103 bool LCPHelpers::IsQualifiedImageRequest(imgRequest* aRequest,
104 Element* aContainingElement) {
105 MOZ_ASSERT(aContainingElement);
106 if (!aRequest) {
107 return false;
110 if (aRequest->IsChrome()) {
111 return false;
114 if (!aContainingElement->ChromeOnlyAccess()) {
115 return true;
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) {
122 return true;
126 // Exception: CSS generated images
127 if (aContainingElement->IsInNativeAnonymousSubtree()) {
128 if (nsINode* rootParentOrHost =
129 aContainingElement
130 ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
131 if (!rootParentOrHost->ChromeOnlyAccess()) {
132 return true;
136 return false;
138 void LargestContentfulPaint::MaybeProcessImageForElementTiming(
139 imgRequestProxy* aRequest, Element* aElement) {
140 if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
141 return;
144 MOZ_ASSERT(aRequest);
145 imgRequest* request = aRequest->GetOwner();
146 if (!LCPHelpers::IsQualifiedImageRequest(request, aElement)) {
147 return;
150 Document* document = aElement->GetComposedDoc();
151 if (!document) {
152 return;
155 nsPresContext* pc =
156 aElement->GetPresContext(Element::PresContextFor::eForComposedDoc);
157 if (!pc) {
158 return;
161 PerformanceMainThread* performance = pc->GetPerformanceMainThread();
162 if (!performance) {
163 return;
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, "
170 "performance=%p ",
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, "
181 "return.",
182 aElement, aRequest);
183 return;
186 imageRequestProxiesForElement.AppendElement(aRequest);
188 #ifdef DEBUG
189 uint32_t status = imgIRequest::STATUS_NONE;
190 aRequest->GetImageStatus(&status);
191 MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE);
192 #endif
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
203 // new.
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()) {
211 return false;
214 if (!aFrame) {
215 return false;
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,
227 aImgRequestProxy);
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 if (performance->HasDispatchedInputEvent() ||
248 performance->HasDispatchedScrollEvent()) {
249 return;
252 if (!performance->IsPendingLCPCandidate(aContainingBlock, aImgRequestProxy)) {
253 return;
256 imgRequestProxy::LCPTimings& lcpTimings = aImgRequestProxy->GetLCPTimings();
257 if (!lcpTimings.AreSet()) {
258 return;
261 imgRequest* request = aImgRequestProxy->GetOwner();
262 MOZ_ASSERT(request);
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,
275 true);
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.
280 lcpTimings.Reset();
282 // If area is less than or equal to document’s largest contentful paint size,
283 // return.
284 if (!performance->UpdateLargestContentfulPaintSize(entry->Size())) {
285 LOG(
287 " This paint(%lu) is not greater than the largest paint (%lf)that "
288 "we've "
289 "reported so far, return",
290 entry->Size(), performance->GetLargestContentfulPaintSize());
291 return;
294 entry->QueueEntry();
297 DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const {
298 if (!mShouldExposeRenderTime) {
299 return 0;
301 return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
304 DOMHighResTimeStamp LargestContentfulPaint::LoadTime() const {
305 if (mLoadTime.isNothing()) {
306 return 0;
309 return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
312 DOMHighResTimeStamp LargestContentfulPaint::StartTime() const {
313 if (mShouldExposeRenderTime) {
314 return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
317 if (mLoadTime.isNothing()) {
318 return 0;
321 return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
324 /* static */
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) {
340 if (mURI) {
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();
349 MOZ_ASSERT(frame);
351 nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
352 if (!rootFrame) {
353 return;
356 if (frame->Style()->IsInOpacityZeroSubtree()) {
357 LOG(" Opacity:0 return");
358 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.",
385 aContainingBlock);
386 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.",
396 aContainingBlock);
397 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.",
411 aContainingBlock);
412 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.
421 double naturalArea =
422 GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value());
424 LOG(" naturalArea = %f", naturalArea);
426 // Let boundingClientArea be clientContentRect’s width * clientContentRect’s
427 // height.
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;
446 MOZ_ASSERT(!mSize);
447 mSize = area;
450 void LCPTextFrameHelper::MaybeUnionTextFrame(
451 nsTextFrame* aTextFrame, const nsRect& aRelativeToSelfRect) {
452 if (!StaticPrefs::dom_enable_largest_contentful_paint() ||
453 aTextFrame->PresContext()->HasStoppedGeneratingLCP()) {
454 return;
457 Element* containingBlock =
458 LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame);
459 if (!containingBlock ||
460 // If element is contained in doc’s set of elements with rendered text,
461 // continue
462 containingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT) ||
463 containingBlock->ChromeOnlyAccess()) {
464 return;
467 MOZ_ASSERT(containingBlock->GetPrimaryFrame());
469 PerformanceMainThread* perf =
470 aTextFrame->PresContext()->GetPerformanceMainThread();
471 if (!perf) {
472 return;
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()) {
487 return;
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,
499 false);
500 // If area is less than or equal to document’s largest contentful paint size,
501 // return.
502 if (!aPerformance->UpdateLargestContentfulPaintSize(entry->Size())) {
503 LOG(" This paint(%lu) is not greater than the largest paint (%lf)that "
504 "we've "
505 "reported so far, return",
506 entry->Size(), aPerformance->GetLargestContentfulPaintSize());
507 return;
509 entry->QueueEntry();
512 void LargestContentfulPaint::ReportLCPToNavigationTimings() {
513 nsCOMPtr<Element> element = do_QueryReferent(mElement);
514 if (!element) {
515 return;
518 const Document* document = element->OwnerDoc();
520 MOZ_ASSERT(document);
522 nsDOMNavigationTiming* timing = document->GetNavigationTiming();
524 if (MOZ_UNLIKELY(!timing)) {
525 return;
528 if (document->IsResourceDoc()) {
529 return;
532 if (BrowsingContext* browsingContext = document->GetBrowsingContext()) {
533 if (browsingContext->GetEmbeddedInContentDocument()) {
534 return;
538 if (!document->IsTopLevelContentDocument()) {
539 return;
541 timing->NotifyLargestContentfulRenderForRootContentDocument(
542 GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime));
544 } // namespace mozilla::dom