tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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