There was an update to LCP spec such that the `renderTime` is no longer protected by Timing-Allow-Origin due to it's easy to side channel attack it and it isn't needed for CORS protected images. The solution was to coarsen the timestamp by 4ms (this is what Chrome did). To do something similar in Firefox, we don't need to coarsen the timestamp because we already reduce more precisions. We just need to always expose this. Spec discussion: https://github.com/w3c/largest-contentful-paint/issues/111 Differential Revision: https://phabricator.services.mozilla.com/D247492
540 lines
17 KiB
C++
540 lines
17 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
#include "mozilla/dom/Element.h"
|
||
#include "nsContentUtils.h"
|
||
#include "nsLayoutUtils.h"
|
||
#include "nsRFPService.h"
|
||
#include "Performance.h"
|
||
#include "imgRequest.h"
|
||
#include "PerformanceMainThread.h"
|
||
#include "LargestContentfulPaint.h"
|
||
|
||
#include "mozilla/dom/BrowsingContext.h"
|
||
#include "mozilla/dom/DOMIntersectionObserver.h"
|
||
#include "mozilla/dom/Document.h"
|
||
#include "mozilla/dom/Element.h"
|
||
|
||
#include "mozilla/PresShell.h"
|
||
#include "mozilla/Logging.h"
|
||
#include "mozilla/nsVideoFrame.h"
|
||
|
||
namespace mozilla::dom {
|
||
|
||
static LazyLogModule gLCPLogging("LargestContentfulPaint");
|
||
|
||
#define LOG(...) MOZ_LOG(gLCPLogging, LogLevel::Debug, (__VA_ARGS__))
|
||
|
||
NS_IMPL_CYCLE_COLLECTION_INHERITED(LargestContentfulPaint, PerformanceEntry,
|
||
mPerformance, mURI, mElement)
|
||
|
||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LargestContentfulPaint)
|
||
NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
|
||
|
||
NS_IMPL_ADDREF_INHERITED(LargestContentfulPaint, PerformanceEntry)
|
||
NS_IMPL_RELEASE_INHERITED(LargestContentfulPaint, PerformanceEntry)
|
||
|
||
static double GetAreaInDoublePixelsFromAppUnits(const nsSize& aSize) {
|
||
return NSAppUnitsToDoublePixels(aSize.Width(), AppUnitsPerCSSPixel()) *
|
||
NSAppUnitsToDoublePixels(aSize.Height(), AppUnitsPerCSSPixel());
|
||
}
|
||
|
||
static double GetAreaInDoublePixelsFromAppUnits(const nsRect& aRect) {
|
||
return NSAppUnitsToDoublePixels(aRect.Width(), AppUnitsPerCSSPixel()) *
|
||
NSAppUnitsToDoublePixels(aRect.Height(), AppUnitsPerCSSPixel());
|
||
}
|
||
|
||
static DOMHighResTimeStamp GetReducedTimePrecisionDOMHighRes(
|
||
Performance* aPerformance, const TimeStamp& aRawTimeStamp) {
|
||
MOZ_ASSERT(aPerformance);
|
||
DOMHighResTimeStamp rawValue =
|
||
aPerformance->GetDOMTiming()->TimeStampToDOMHighRes(aRawTimeStamp);
|
||
return nsRFPService::ReduceTimePrecisionAsMSecs(
|
||
rawValue, aPerformance->GetRandomTimelineSeed(),
|
||
aPerformance->GetRTPCallerType());
|
||
}
|
||
|
||
LargestContentfulPaint::LargestContentfulPaint(
|
||
PerformanceMainThread* aPerformance, const TimeStamp& aRenderTime,
|
||
const Maybe<TimeStamp>& aLoadTime, const unsigned long aSize, nsIURI* aURI,
|
||
Element* aElement, bool aShouldExposeRenderTime)
|
||
: PerformanceEntry(aPerformance->GetParentObject(), u""_ns,
|
||
kLargestContentfulPaintName),
|
||
mPerformance(aPerformance),
|
||
mRenderTime(aRenderTime),
|
||
mLoadTime(aLoadTime),
|
||
mShouldExposeRenderTime(aShouldExposeRenderTime),
|
||
mSize(aSize),
|
||
mURI(aURI) {
|
||
MOZ_ASSERT(mPerformance);
|
||
MOZ_ASSERT(aElement);
|
||
// The element could be a pseudo-element
|
||
if (aElement->ChromeOnlyAccess()) {
|
||
mElement = do_GetWeakReference(Element::FromNodeOrNull(
|
||
aElement->FindFirstNonChromeOnlyAccessContent()));
|
||
} else {
|
||
mElement = do_GetWeakReference(aElement);
|
||
}
|
||
|
||
if (const Element* element = GetElement()) {
|
||
mId = element->GetID();
|
||
}
|
||
}
|
||
|
||
JSObject* LargestContentfulPaint::WrapObject(
|
||
JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
|
||
return LargestContentfulPaint_Binding::Wrap(aCx, this, aGivenProto);
|
||
}
|
||
|
||
Element* LargestContentfulPaint::GetElement() const {
|
||
nsCOMPtr<Element> element = do_QueryReferent(mElement);
|
||
return element ? nsContentUtils::GetAnElementForTiming(
|
||
element, element->GetComposedDoc(), nullptr)
|
||
: nullptr;
|
||
}
|
||
|
||
void LargestContentfulPaint::BufferEntryIfNeeded() {
|
||
mPerformance->BufferLargestContentfulPaintEntryIfNeeded(this);
|
||
}
|
||
|
||
/* static*/
|
||
bool LCPHelpers::IsQualifiedImageRequest(imgRequest* aRequest,
|
||
Element* aContainingElement) {
|
||
MOZ_ASSERT(aContainingElement);
|
||
if (!aRequest) {
|
||
return false;
|
||
}
|
||
|
||
if (aRequest->IsChrome()) {
|
||
return false;
|
||
}
|
||
|
||
if (!aContainingElement->ChromeOnlyAccess()) {
|
||
return true;
|
||
}
|
||
|
||
// Exception: this is a poster image of video element
|
||
if (nsIContent* parent = aContainingElement->GetParent()) {
|
||
nsVideoFrame* videoFrame = do_QueryFrame(parent->GetPrimaryFrame());
|
||
if (videoFrame && videoFrame->GetPosterImage() == aContainingElement) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Exception: CSS generated images
|
||
if (aContainingElement->IsInNativeAnonymousSubtree()) {
|
||
if (nsINode* rootParentOrHost =
|
||
aContainingElement
|
||
->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
|
||
if (!rootParentOrHost->ChromeOnlyAccess()) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
void LargestContentfulPaint::MaybeProcessImageForElementTiming(
|
||
imgRequestProxy* aRequest, Element* aElement) {
|
||
if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
|
||
return;
|
||
}
|
||
|
||
MOZ_ASSERT(aRequest);
|
||
imgRequest* request = aRequest->GetOwner();
|
||
if (!LCPHelpers::IsQualifiedImageRequest(request, aElement)) {
|
||
return;
|
||
}
|
||
|
||
Document* document = aElement->GetComposedDoc();
|
||
if (!document) {
|
||
return;
|
||
}
|
||
|
||
nsPresContext* pc =
|
||
aElement->GetPresContext(Element::PresContextFor::eForComposedDoc);
|
||
if (!pc) {
|
||
return;
|
||
}
|
||
|
||
PerformanceMainThread* performance = pc->GetPerformanceMainThread();
|
||
if (!performance) {
|
||
return;
|
||
}
|
||
|
||
if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) {
|
||
nsCOMPtr<nsIURI> uri;
|
||
aRequest->GetURI(getter_AddRefs(uri));
|
||
LOG("MaybeProcessImageForElementTiming, Element=%p, URI=%s, "
|
||
"performance=%p ",
|
||
aElement, uri ? uri->GetSpecOrDefault().get() : "", performance);
|
||
}
|
||
|
||
aElement->SetFlags(ELEMENT_IN_CONTENT_IDENTIFIER_FOR_LCP);
|
||
|
||
nsTArray<WeakPtr<PreloaderBase>>& imageRequestProxiesForElement =
|
||
document->ContentIdentifiersForLCP().LookupOrInsert(aElement);
|
||
|
||
if (imageRequestProxiesForElement.Contains(aRequest)) {
|
||
LOG(" The content identifier existed for element=%p and request=%p, "
|
||
"return.",
|
||
aElement, aRequest);
|
||
return;
|
||
}
|
||
|
||
imageRequestProxiesForElement.AppendElement(aRequest);
|
||
|
||
#ifdef DEBUG
|
||
uint32_t status = imgIRequest::STATUS_NONE;
|
||
aRequest->GetImageStatus(&status);
|
||
MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE);
|
||
#endif
|
||
|
||
// At this point, the loadTime of the image is known, but
|
||
// the renderTime is unknown, so it's added to ImagesPendingRendering
|
||
// as a placeholder, and the corresponding LCP entry will be created
|
||
// when the renderTime is known.
|
||
// Here we are exposing the load time of the image which could be
|
||
// a privacy concern. The spec talks about it at
|
||
// https://wicg.github.io/element-timing/#sec-security
|
||
// TLDR: The similar metric can be obtained by ResourceTiming
|
||
// API and onload handlers already, so this is not exposing anything
|
||
// new.
|
||
LOG(" Added a pending image rendering");
|
||
performance->AddImagesPendingRendering(
|
||
ImagePendingRendering{aElement, aRequest, TimeStamp::Now()});
|
||
}
|
||
|
||
bool LCPHelpers::CanFinalizeLCPEntry(const nsIFrame* aFrame) {
|
||
if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
|
||
return false;
|
||
}
|
||
|
||
if (!aFrame) {
|
||
return false;
|
||
}
|
||
|
||
nsPresContext* presContext = aFrame->PresContext();
|
||
return !presContext->HasStoppedGeneratingLCP() &&
|
||
presContext->GetPerformanceMainThread();
|
||
}
|
||
|
||
void LCPHelpers::FinalizeLCPEntryForImage(
|
||
Element* aContainingBlock, imgRequestProxy* aImgRequestProxy,
|
||
const nsRect& aTargetRectRelativeToSelf) {
|
||
LOG("FinalizeLCPEntryForImage element=%p image=%p", aContainingBlock,
|
||
aImgRequestProxy);
|
||
if (!aImgRequestProxy) {
|
||
return;
|
||
}
|
||
|
||
if (!IsQualifiedImageRequest(aImgRequestProxy->GetOwner(),
|
||
aContainingBlock)) {
|
||
return;
|
||
}
|
||
|
||
nsIFrame* frame = aContainingBlock->GetPrimaryFrame();
|
||
|
||
if (!CanFinalizeLCPEntry(frame)) {
|
||
return;
|
||
}
|
||
|
||
PerformanceMainThread* performance =
|
||
frame->PresContext()->GetPerformanceMainThread();
|
||
MOZ_ASSERT(performance);
|
||
|
||
if (performance->HasDispatchedInputEvent() ||
|
||
performance->HasDispatchedScrollEvent()) {
|
||
return;
|
||
}
|
||
|
||
if (!performance->IsPendingLCPCandidate(aContainingBlock, aImgRequestProxy)) {
|
||
return;
|
||
}
|
||
|
||
imgRequestProxy::LCPTimings& lcpTimings = aImgRequestProxy->GetLCPTimings();
|
||
if (!lcpTimings.AreSet()) {
|
||
return;
|
||
}
|
||
|
||
imgRequest* request = aImgRequestProxy->GetOwner();
|
||
MOZ_ASSERT(request);
|
||
|
||
nsCOMPtr<nsIURI> requestURI;
|
||
aImgRequestProxy->GetURI(getter_AddRefs(requestURI));
|
||
|
||
const bool taoPassed =
|
||
request->ShouldReportRenderTimeForLCP() || request->IsData();
|
||
|
||
RefPtr<LargestContentfulPaint> entry = new LargestContentfulPaint(
|
||
performance, lcpTimings.mRenderTime.ref(), lcpTimings.mLoadTime, 0,
|
||
requestURI, aContainingBlock,
|
||
taoPassed ||
|
||
StaticPrefs::
|
||
dom_performance_largest_contentful_paint_coarsened_rendertime_enabled());
|
||
|
||
entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, performance,
|
||
true);
|
||
|
||
// Resets the LCPTiming so that unless this (element, image) pair goes
|
||
// through PerformanceMainThread::ProcessElementTiming again, they
|
||
// won't generate new LCP entries.
|
||
lcpTimings.Reset();
|
||
|
||
// If area is less than or equal to document’s largest contentful paint size,
|
||
// return.
|
||
if (!performance->UpdateLargestContentfulPaintSize(entry->Size())) {
|
||
LOG(
|
||
|
||
" This paint(%lu) is not greater than the largest paint (%lf)that "
|
||
"we've "
|
||
"reported so far, return",
|
||
entry->Size(), performance->GetLargestContentfulPaintSize());
|
||
return;
|
||
}
|
||
|
||
entry->QueueEntry();
|
||
}
|
||
|
||
DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const {
|
||
if (!mShouldExposeRenderTime) {
|
||
return 0;
|
||
}
|
||
return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
|
||
}
|
||
|
||
DOMHighResTimeStamp LargestContentfulPaint::LoadTime() const {
|
||
if (mLoadTime.isNothing()) {
|
||
return 0;
|
||
}
|
||
|
||
return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
|
||
}
|
||
|
||
DOMHighResTimeStamp LargestContentfulPaint::StartTime() const {
|
||
return mShouldExposeRenderTime ? RenderTime() : LoadTime();
|
||
}
|
||
|
||
/* static */
|
||
Element* LargestContentfulPaint::GetContainingBlockForTextFrame(
|
||
const nsTextFrame* aTextFrame) {
|
||
nsIFrame* containingFrame = aTextFrame->GetContainingBlock();
|
||
MOZ_ASSERT(containingFrame);
|
||
return Element::FromNodeOrNull(containingFrame->GetContent());
|
||
}
|
||
|
||
void LargestContentfulPaint::QueueEntry() {
|
||
LOG("QueueEntry entry=%p", this);
|
||
mPerformance->QueueLargestContentfulPaintEntry(this);
|
||
|
||
ReportLCPToNavigationTimings();
|
||
}
|
||
|
||
void LargestContentfulPaint::GetUrl(nsAString& aUrl) {
|
||
if (mURI) {
|
||
CopyUTF8toUTF16(mURI->GetSpecOrDefault(), aUrl);
|
||
}
|
||
}
|
||
|
||
void LargestContentfulPaint::UpdateSize(
|
||
const Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf,
|
||
const PerformanceMainThread* aPerformance, bool aIsImage) {
|
||
nsIFrame* frame = aContainingBlock->GetPrimaryFrame();
|
||
MOZ_ASSERT(frame);
|
||
|
||
nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
|
||
if (!rootFrame) {
|
||
return;
|
||
}
|
||
|
||
if (frame->Style()->IsInOpacityZeroSubtree()) {
|
||
LOG(" Opacity:0 return");
|
||
return;
|
||
}
|
||
|
||
// The following size computation is based on a pending pull request
|
||
// https://github.com/w3c/largest-contentful-paint/pull/99
|
||
|
||
// Let visibleDimensions be concreteDimensions, adjusted for positioning
|
||
// by object-position or background-position and element’s content box.
|
||
const nsRect& visibleDimensions = aTargetRectRelativeToSelf;
|
||
|
||
// Let clientContentRect be the smallest DOMRectReadOnly containing
|
||
// visibleDimensions with element’s transforms applied.
|
||
nsRect clientContentRect = nsLayoutUtils::TransformFrameRectToAncestor(
|
||
frame, visibleDimensions, rootFrame);
|
||
|
||
// Let intersectionRect be the value returned by the intersection rect
|
||
// algorithm using element as the target and viewport as the root.
|
||
// (From https://wicg.github.io/element-timing/#sec-report-image-element)
|
||
IntersectionInput input = DOMIntersectionObserver::ComputeInput(
|
||
*frame->PresContext()->Document(), rootFrame->GetContent(), nullptr);
|
||
const IntersectionOutput output =
|
||
DOMIntersectionObserver::Intersect(input, *aContainingBlock);
|
||
|
||
Maybe<nsRect> intersectionRect = output.mIntersectionRect;
|
||
|
||
if (intersectionRect.isNothing()) {
|
||
LOG(" The intersectionRect is nothing for Element=%p. return.",
|
||
aContainingBlock);
|
||
return;
|
||
}
|
||
|
||
// Let intersectingClientContentRect be the intersection of clientContentRect
|
||
// with intersectionRect.
|
||
Maybe<nsRect> intersectionWithContentRect =
|
||
clientContentRect.EdgeInclusiveIntersection(intersectionRect.value());
|
||
|
||
if (intersectionWithContentRect.isNothing()) {
|
||
LOG(" The intersectionWithContentRect is nothing for Element=%p. return.",
|
||
aContainingBlock);
|
||
return;
|
||
}
|
||
|
||
nsRect renderedRect = intersectionWithContentRect.value();
|
||
|
||
double area = GetAreaInDoublePixelsFromAppUnits(renderedRect);
|
||
|
||
double viewport = GetAreaInDoublePixelsFromAppUnits(input.mRootRect);
|
||
|
||
LOG(" Viewport = %f, RenderRect = %f.", viewport, area);
|
||
// We don't want to report things that take the entire viewport.
|
||
if (area >= viewport) {
|
||
LOG(" The renderedRect is at least same as the area of the "
|
||
"viewport for Element=%p, return.",
|
||
aContainingBlock);
|
||
return;
|
||
}
|
||
|
||
Maybe<nsSize> intrinsicSize = frame->GetIntrinsicSize().ToSize();
|
||
const bool hasIntrinsicSize = intrinsicSize && !intrinsicSize->IsEmpty();
|
||
|
||
if (aIsImage && hasIntrinsicSize) {
|
||
// Let (naturalWidth, naturalHeight) be imageRequest’s natural dimension.
|
||
// Let naturalArea be naturalWidth * naturalHeight.
|
||
double naturalArea =
|
||
GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value());
|
||
|
||
LOG(" naturalArea = %f", naturalArea);
|
||
|
||
// Let boundingClientArea be clientContentRect’s width * clientContentRect’s
|
||
// height.
|
||
double boundingClientArea =
|
||
NSAppUnitsToDoublePixels(clientContentRect.Width(),
|
||
AppUnitsPerCSSPixel()) *
|
||
NSAppUnitsToDoublePixels(clientContentRect.Height(),
|
||
AppUnitsPerCSSPixel());
|
||
LOG(" boundingClientArea = %f", boundingClientArea);
|
||
|
||
// Let scaleFactor be boundingClientArea / naturalArea.
|
||
double scaleFactor = boundingClientArea / naturalArea;
|
||
LOG(" scaleFactor = %f", scaleFactor);
|
||
|
||
// If scaleFactor is greater than 1, then divide area by scaleFactor.
|
||
if (scaleFactor > 1) {
|
||
LOG(" area before sacled doown %f", area);
|
||
area = area / scaleFactor;
|
||
}
|
||
}
|
||
|
||
MOZ_ASSERT(!mSize);
|
||
mSize = area;
|
||
}
|
||
|
||
void LCPTextFrameHelper::MaybeUnionTextFrame(
|
||
nsTextFrame* aTextFrame, const nsRect& aRelativeToSelfRect) {
|
||
if (!StaticPrefs::dom_enable_largest_contentful_paint() ||
|
||
aTextFrame->PresContext()->HasStoppedGeneratingLCP()) {
|
||
return;
|
||
}
|
||
|
||
Element* containingBlock =
|
||
LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame);
|
||
if (!containingBlock ||
|
||
// If element is contained in doc’s set of elements with rendered text,
|
||
// continue
|
||
containingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT) ||
|
||
containingBlock->ChromeOnlyAccess()) {
|
||
return;
|
||
}
|
||
|
||
MOZ_ASSERT(containingBlock->GetPrimaryFrame());
|
||
|
||
PerformanceMainThread* perf =
|
||
aTextFrame->PresContext()->GetPerformanceMainThread();
|
||
if (!perf) {
|
||
return;
|
||
}
|
||
|
||
auto& unionRect = perf->GetTextFrameUnions().LookupOrInsert(containingBlock);
|
||
unionRect = unionRect.Union(aRelativeToSelfRect);
|
||
}
|
||
|
||
void LCPHelpers::FinalizeLCPEntryForText(
|
||
PerformanceMainThread* aPerformance, const TimeStamp& aRenderTime,
|
||
Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf,
|
||
const nsPresContext* aPresContext) {
|
||
MOZ_ASSERT(aPerformance);
|
||
LOG("FinalizeLCPEntryForText element=%p", aContainingBlock);
|
||
|
||
if (!aContainingBlock->GetPrimaryFrame()) {
|
||
return;
|
||
}
|
||
MOZ_ASSERT(CanFinalizeLCPEntry(aContainingBlock->GetPrimaryFrame()));
|
||
MOZ_ASSERT(!aContainingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT));
|
||
MOZ_ASSERT(!aContainingBlock->ChromeOnlyAccess());
|
||
|
||
aContainingBlock->SetFlags(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT);
|
||
|
||
RefPtr<LargestContentfulPaint> entry = new LargestContentfulPaint(
|
||
aPerformance, aRenderTime, Nothing(), 0, nullptr, aContainingBlock, true);
|
||
|
||
entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, aPerformance,
|
||
false);
|
||
// If area is less than or equal to document’s largest contentful paint size,
|
||
// return.
|
||
if (!aPerformance->UpdateLargestContentfulPaintSize(entry->Size())) {
|
||
LOG(" This paint(%lu) is not greater than the largest paint (%lf)that "
|
||
"we've "
|
||
"reported so far, return",
|
||
entry->Size(), aPerformance->GetLargestContentfulPaintSize());
|
||
return;
|
||
}
|
||
entry->QueueEntry();
|
||
}
|
||
|
||
void LargestContentfulPaint::ReportLCPToNavigationTimings() {
|
||
nsCOMPtr<Element> element = do_QueryReferent(mElement);
|
||
if (!element) {
|
||
return;
|
||
}
|
||
|
||
const Document* document = element->OwnerDoc();
|
||
|
||
MOZ_ASSERT(document);
|
||
|
||
nsDOMNavigationTiming* timing = document->GetNavigationTiming();
|
||
|
||
if (MOZ_UNLIKELY(!timing)) {
|
||
return;
|
||
}
|
||
|
||
if (document->IsResourceDoc()) {
|
||
return;
|
||
}
|
||
|
||
if (BrowsingContext* browsingContext = document->GetBrowsingContext()) {
|
||
if (browsingContext->GetEmbeddedInContentDocument()) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!document->IsTopLevelContentDocument()) {
|
||
return;
|
||
}
|
||
timing->NotifyLargestContentfulRenderForRootContentDocument(
|
||
GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime));
|
||
}
|
||
} // namespace mozilla::dom
|