LargestContentfulPaint.cpp (17624B)
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 "LargestContentfulPaint.h" 7 8 #include "Performance.h" 9 #include "PerformanceMainThread.h" 10 #include "imgRequest.h" 11 #include "mozilla/Logging.h" 12 #include "mozilla/PresShell.h" 13 #include "mozilla/dom/BrowsingContext.h" 14 #include "mozilla/dom/DOMIntersectionObserver.h" 15 #include "mozilla/dom/Document.h" 16 #include "mozilla/dom/DocumentInlines.h" 17 #include "mozilla/dom/Element.h" 18 #include "mozilla/nsVideoFrame.h" 19 #include "nsContentUtils.h" 20 #include "nsGkAtoms.h" 21 #include "nsLayoutUtils.h" 22 #include "nsRFPService.h" 23 24 namespace mozilla::dom { 25 26 static LazyLogModule gLCPLogging("LargestContentfulPaint"); 27 28 #define LOG(...) MOZ_LOG(gLCPLogging, LogLevel::Debug, (__VA_ARGS__)) 29 30 NS_IMPL_CYCLE_COLLECTION_INHERITED(LargestContentfulPaint, PerformanceEntry, 31 mPerformance, mURI, mElement) 32 33 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LargestContentfulPaint) 34 NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry) 35 36 NS_IMPL_ADDREF_INHERITED(LargestContentfulPaint, PerformanceEntry) 37 NS_IMPL_RELEASE_INHERITED(LargestContentfulPaint, PerformanceEntry) 38 39 static double GetAreaInDoublePixelsFromAppUnits(const nsSize& aSize) { 40 return NSAppUnitsToDoublePixels(aSize.Width(), AppUnitsPerCSSPixel()) * 41 NSAppUnitsToDoublePixels(aSize.Height(), AppUnitsPerCSSPixel()); 42 } 43 44 static double GetAreaInDoublePixelsFromAppUnits(const nsRect& aRect) { 45 return NSAppUnitsToDoublePixels(aRect.Width(), AppUnitsPerCSSPixel()) * 46 NSAppUnitsToDoublePixels(aRect.Height(), AppUnitsPerCSSPixel()); 47 } 48 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()); 57 } 58 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 nsGkAtoms::largestContentfulPaint), 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); 79 } 80 81 if (const Element* element = GetElement()) { 82 mId = element->GetID(); 83 } 84 } 85 86 JSObject* LargestContentfulPaint::WrapObject( 87 JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { 88 return LargestContentfulPaint_Binding::Wrap(aCx, this, aGivenProto); 89 } 90 91 Element* LargestContentfulPaint::GetElement() const { 92 nsCOMPtr<Element> element = do_QueryReferent(mElement); 93 return element ? nsContentUtils::GetAnElementForTiming( 94 element, element->GetComposedDoc(), nullptr) 95 : nullptr; 96 } 97 98 void LargestContentfulPaint::BufferEntryIfNeeded() { 99 mPerformance->BufferLargestContentfulPaintEntryIfNeeded(this); 100 } 101 102 /* static*/ 103 bool LCPHelpers::IsQualifiedImageRequest(imgRequest* aRequest, 104 Element* aContainingElement) { 105 MOZ_ASSERT(aContainingElement); 106 if (!aRequest) { 107 return false; 108 } 109 110 if (aRequest->IsChrome()) { 111 return false; 112 } 113 114 if (!aContainingElement->ChromeOnlyAccess()) { 115 return true; 116 } 117 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; 123 } 124 } 125 126 // Exception: CSS generated images 127 if (aContainingElement->IsInNativeAnonymousSubtree()) { 128 if (nsINode* rootParentOrHost = 129 aContainingElement 130 ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) { 131 if (!rootParentOrHost->ChromeOnlyAccess()) { 132 return true; 133 } 134 } 135 } 136 return false; 137 } 138 void LargestContentfulPaint::MaybeProcessImageForElementTiming( 139 imgRequestProxy* aRequest, Element* aElement) { 140 if (!StaticPrefs::dom_enable_largest_contentful_paint()) { 141 return; 142 } 143 144 MOZ_ASSERT(aRequest); 145 imgRequest* request = aRequest->GetOwner(); 146 if (!LCPHelpers::IsQualifiedImageRequest(request, aElement)) { 147 return; 148 } 149 150 Document* document = aElement->GetComposedDoc(); 151 if (!document) { 152 return; 153 } 154 155 nsPresContext* pc = document->GetPresContext(); 156 if (!pc || pc->HasStoppedGeneratingLCP()) { 157 return; 158 } 159 160 PerformanceMainThread* performance = pc->GetPerformanceMainThread(); 161 if (!performance) { 162 return; 163 } 164 165 if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) { 166 nsCOMPtr<nsIURI> uri; 167 aRequest->GetURI(getter_AddRefs(uri)); 168 LOG("MaybeProcessImageForElementTiming, Element=%p, URI=%s, " 169 "performance=%p ", 170 aElement, uri ? uri->GetSpecOrDefault().get() : "", performance); 171 } 172 173 aElement->SetFlags(ELEMENT_IN_CONTENT_IDENTIFIER_FOR_LCP); 174 175 nsTArray<WeakPtr<PreloaderBase>>& imageRequestProxiesForElement = 176 document->ContentIdentifiersForLCP().LookupOrInsert(aElement); 177 178 if (imageRequestProxiesForElement.Contains(aRequest)) { 179 LOG(" The content identifier existed for element=%p and request=%p, " 180 "return.", 181 aElement, aRequest); 182 return; 183 } 184 185 imageRequestProxiesForElement.AppendElement(aRequest); 186 187 #ifdef DEBUG 188 uint32_t status = imgIRequest::STATUS_NONE; 189 aRequest->GetImageStatus(&status); 190 MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE); 191 #endif 192 193 // At this point, the loadTime of the image is known, but 194 // the renderTime is unknown, so it's added to ImagesPendingRendering 195 // as a placeholder, and the corresponding LCP entry will be created 196 // when the renderTime is known. 197 // Here we are exposing the load time of the image which could be 198 // a privacy concern. The spec talks about it at 199 // https://wicg.github.io/element-timing/#sec-security 200 // TLDR: The similar metric can be obtained by ResourceTiming 201 // API and onload handlers already, so this is not exposing anything 202 // new. 203 LOG(" Added a pending image rendering"); 204 performance->AddImagesPendingRendering( 205 ImagePendingRendering{aElement, aRequest, TimeStamp::Now()}); 206 } 207 208 bool LCPHelpers::CanFinalizeLCPEntry(const nsIFrame* aFrame) { 209 if (!StaticPrefs::dom_enable_largest_contentful_paint()) { 210 return false; 211 } 212 213 if (!aFrame) { 214 return false; 215 } 216 217 nsPresContext* presContext = aFrame->PresContext(); 218 return !presContext->HasStoppedGeneratingLCP() && 219 presContext->GetPerformanceMainThread(); 220 } 221 222 void LCPHelpers::FinalizeLCPEntryForImage( 223 Element* aContainingBlock, imgRequestProxy* aImgRequestProxy, 224 const nsRect& aTargetRectRelativeToSelf) { 225 LOG("FinalizeLCPEntryForImage element=%p image=%p", aContainingBlock, 226 aImgRequestProxy); 227 if (!aImgRequestProxy) { 228 return; 229 } 230 231 if (!IsQualifiedImageRequest(aImgRequestProxy->GetOwner(), 232 aContainingBlock)) { 233 return; 234 } 235 236 nsIFrame* frame = aContainingBlock->GetPrimaryFrame(); 237 238 if (!CanFinalizeLCPEntry(frame)) { 239 return; 240 } 241 242 PerformanceMainThread* performance = 243 frame->PresContext()->GetPerformanceMainThread(); 244 MOZ_ASSERT(performance); 245 246 if (performance->HasDispatchedInputEvent() || 247 performance->HasDispatchedScrollEvent()) { 248 return; 249 } 250 251 if (!performance->IsPendingLCPCandidate(aContainingBlock, aImgRequestProxy)) { 252 return; 253 } 254 255 imgRequestProxy::LCPTimings& lcpTimings = aImgRequestProxy->GetLCPTimings(); 256 if (!lcpTimings.AreSet()) { 257 return; 258 } 259 260 imgRequest* request = aImgRequestProxy->GetOwner(); 261 MOZ_ASSERT(request); 262 263 nsCOMPtr<nsIURI> requestURI; 264 aImgRequestProxy->GetURI(getter_AddRefs(requestURI)); 265 266 const bool taoPassed = 267 request->ShouldReportRenderTimeForLCP() || request->IsData(); 268 269 RefPtr<LargestContentfulPaint> entry = new LargestContentfulPaint( 270 performance, lcpTimings.mRenderTime.ref(), lcpTimings.mLoadTime, 0, 271 requestURI, aContainingBlock, 272 taoPassed || 273 StaticPrefs:: 274 dom_performance_largest_contentful_paint_coarsened_rendertime_enabled()); 275 276 entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, performance, 277 true); 278 279 // Resets the LCPTiming so that unless this (element, image) pair goes 280 // through PerformanceMainThread::ProcessElementTiming again, they 281 // won't generate new LCP entries. 282 lcpTimings.Reset(); 283 284 // If area is less than or equal to document’s largest contentful paint size, 285 // return. 286 if (!performance->UpdateLargestContentfulPaintSize(entry->Size())) { 287 LOG( 288 289 " This paint(%lu) is not greater than the largest paint (%lf)that " 290 "we've " 291 "reported so far, return", 292 entry->Size(), performance->GetLargestContentfulPaintSize()); 293 return; 294 } 295 296 entry->QueueEntry(); 297 } 298 299 DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const { 300 if (!mShouldExposeRenderTime) { 301 return 0; 302 } 303 return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime); 304 } 305 306 DOMHighResTimeStamp LargestContentfulPaint::LoadTime() const { 307 if (mLoadTime.isNothing()) { 308 return 0; 309 } 310 311 return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref()); 312 } 313 314 DOMHighResTimeStamp LargestContentfulPaint::StartTime() const { 315 return mShouldExposeRenderTime ? RenderTime() : LoadTime(); 316 } 317 318 /* static */ 319 Element* LargestContentfulPaint::GetContainingBlockForTextFrame( 320 const nsTextFrame* aTextFrame) { 321 nsIFrame* containingFrame = aTextFrame->GetContainingBlock(); 322 MOZ_ASSERT(containingFrame); 323 return Element::FromNodeOrNull(containingFrame->GetContent()); 324 } 325 326 void LargestContentfulPaint::QueueEntry() { 327 LOG("QueueEntry entry=%p", this); 328 mPerformance->QueueLargestContentfulPaintEntry(this); 329 330 ReportLCPToNavigationTimings(); 331 } 332 333 void LargestContentfulPaint::GetUrl(nsAString& aUrl) { 334 if (mURI) { 335 CopyUTF8toUTF16(mURI->GetSpecOrDefault(), aUrl); 336 } 337 } 338 339 void LargestContentfulPaint::UpdateSize( 340 const Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf, 341 const PerformanceMainThread* aPerformance, bool aIsImage) { 342 nsIFrame* frame = aContainingBlock->GetPrimaryFrame(); 343 MOZ_ASSERT(frame); 344 345 nsIFrame* rootFrame = frame->PresShell()->GetRootFrame(); 346 if (!rootFrame) { 347 return; 348 } 349 350 if (frame->Style()->IsInOpacityZeroSubtree()) { 351 LOG(" Opacity:0 return"); 352 return; 353 } 354 355 // The following size computation is based on a pending pull request 356 // https://github.com/w3c/largest-contentful-paint/pull/99 357 358 // Let visibleDimensions be concreteDimensions, adjusted for positioning 359 // by object-position or background-position and element’s content box. 360 const nsRect& visibleDimensions = aTargetRectRelativeToSelf; 361 362 // Let clientContentRect be the smallest DOMRectReadOnly containing 363 // visibleDimensions with element’s transforms applied. 364 nsRect clientContentRect = nsLayoutUtils::TransformFrameRectToAncestor( 365 frame, visibleDimensions, rootFrame); 366 367 // Let intersectionRect be the value returned by the intersection rect 368 // algorithm using element as the target and viewport as the root. 369 // (From https://wicg.github.io/element-timing/#sec-report-image-element) 370 IntersectionInput input = DOMIntersectionObserver::ComputeInput( 371 *frame->PresContext()->Document(), rootFrame->GetContent(), nullptr, 372 nullptr); 373 const IntersectionOutput output = 374 DOMIntersectionObserver::Intersect(input, *aContainingBlock); 375 376 Maybe<nsRect> intersectionRect = output.mIntersectionRect; 377 378 if (intersectionRect.isNothing()) { 379 LOG(" The intersectionRect is nothing for Element=%p. return.", 380 aContainingBlock); 381 return; 382 } 383 384 // Let intersectingClientContentRect be the intersection of clientContentRect 385 // with intersectionRect. 386 Maybe<nsRect> intersectionWithContentRect = 387 clientContentRect.EdgeInclusiveIntersection(intersectionRect.value()); 388 389 if (intersectionWithContentRect.isNothing()) { 390 LOG(" The intersectionWithContentRect is nothing for Element=%p. return.", 391 aContainingBlock); 392 return; 393 } 394 395 nsRect renderedRect = intersectionWithContentRect.value(); 396 397 double area = GetAreaInDoublePixelsFromAppUnits(renderedRect); 398 399 double viewport = GetAreaInDoublePixelsFromAppUnits(input.mRootRect); 400 401 LOG(" Viewport = %f, RenderRect = %f.", viewport, area); 402 // We don't want to report things that take the entire viewport. 403 if (area >= viewport) { 404 LOG(" The renderedRect is at least same as the area of the " 405 "viewport for Element=%p, return.", 406 aContainingBlock); 407 return; 408 } 409 410 Maybe<nsSize> intrinsicSize = frame->GetIntrinsicSize().ToSize(); 411 const bool hasIntrinsicSize = intrinsicSize && !intrinsicSize->IsEmpty(); 412 413 if (aIsImage && hasIntrinsicSize) { 414 // Let (naturalWidth, naturalHeight) be imageRequest’s natural dimension. 415 // Let naturalArea be naturalWidth * naturalHeight. 416 double naturalArea = 417 GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value()); 418 419 LOG(" naturalArea = %f", naturalArea); 420 421 // Let boundingClientArea be clientContentRect’s width * clientContentRect’s 422 // height. 423 double boundingClientArea = 424 NSAppUnitsToDoublePixels(clientContentRect.Width(), 425 AppUnitsPerCSSPixel()) * 426 NSAppUnitsToDoublePixels(clientContentRect.Height(), 427 AppUnitsPerCSSPixel()); 428 LOG(" boundingClientArea = %f", boundingClientArea); 429 430 // If the scale factor is greater than 1, then adjust area. 431 if (boundingClientArea > naturalArea) { 432 LOG(" area before scaled down %f", area); 433 area *= (naturalArea / boundingClientArea); 434 } 435 } 436 437 MOZ_ASSERT(!mSize); 438 mSize = area; 439 } 440 441 void LCPTextFrameHelper::MaybeUnionTextFrame( 442 nsTextFrame* aTextFrame, const nsRect& aRelativeToSelfRect) { 443 if (!StaticPrefs::dom_enable_largest_contentful_paint() || 444 aTextFrame->PresContext()->HasStoppedGeneratingLCP()) { 445 return; 446 } 447 448 Element* containingBlock = 449 LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame); 450 if (!containingBlock || 451 // If element is contained in doc’s set of elements with rendered text, 452 // continue 453 containingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT) || 454 containingBlock->ChromeOnlyAccess()) { 455 return; 456 } 457 458 MOZ_ASSERT(containingBlock->GetPrimaryFrame()); 459 460 PerformanceMainThread* perf = 461 aTextFrame->PresContext()->GetPerformanceMainThread(); 462 if (!perf) { 463 return; 464 } 465 466 auto& unionRect = perf->GetTextFrameUnions().LookupOrInsert(containingBlock); 467 unionRect = unionRect.Union(aRelativeToSelfRect); 468 } 469 470 void LCPHelpers::FinalizeLCPEntryForText( 471 PerformanceMainThread* aPerformance, const TimeStamp& aRenderTime, 472 Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf, 473 const nsPresContext* aPresContext) { 474 MOZ_ASSERT(aPerformance); 475 LOG("FinalizeLCPEntryForText element=%p", aContainingBlock); 476 477 if (!aContainingBlock->GetPrimaryFrame()) { 478 return; 479 } 480 MOZ_ASSERT(CanFinalizeLCPEntry(aContainingBlock->GetPrimaryFrame())); 481 MOZ_ASSERT(!aContainingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT)); 482 MOZ_ASSERT(!aContainingBlock->ChromeOnlyAccess()); 483 484 aContainingBlock->SetFlags(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT); 485 486 RefPtr<LargestContentfulPaint> entry = new LargestContentfulPaint( 487 aPerformance, aRenderTime, Nothing(), 0, nullptr, aContainingBlock, true); 488 489 entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, aPerformance, 490 false); 491 // If area is less than or equal to document’s largest contentful paint size, 492 // return. 493 if (!aPerformance->UpdateLargestContentfulPaintSize(entry->Size())) { 494 LOG(" This paint(%lu) is not greater than the largest paint (%lf)that " 495 "we've " 496 "reported so far, return", 497 entry->Size(), aPerformance->GetLargestContentfulPaintSize()); 498 return; 499 } 500 entry->QueueEntry(); 501 } 502 503 void LargestContentfulPaint::ReportLCPToNavigationTimings() { 504 nsCOMPtr<Element> element = do_QueryReferent(mElement); 505 if (!element) { 506 return; 507 } 508 509 const Document* document = element->OwnerDoc(); 510 511 MOZ_ASSERT(document); 512 513 nsDOMNavigationTiming* timing = document->GetNavigationTiming(); 514 515 if (MOZ_UNLIKELY(!timing)) { 516 return; 517 } 518 519 if (document->IsResourceDoc()) { 520 return; 521 } 522 523 if (BrowsingContext* browsingContext = document->GetBrowsingContext()) { 524 if (browsingContext->GetEmbeddedInContentDocument()) { 525 return; 526 } 527 } 528 529 if (!document->IsTopLevelContentDocument()) { 530 return; 531 } 532 timing->NotifyLargestContentfulRenderForRootContentDocument( 533 GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime)); 534 } 535 } // namespace mozilla::dom