зеркало из https://github.com/mozilla/gecko-dev.git
545 строки
17 KiB
C++
545 строки
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);
|
||
|
||
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 {
|
||
if (mShouldExposeRenderTime) {
|
||
return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
|
||
}
|
||
|
||
if (mLoadTime.isNothing()) {
|
||
return 0;
|
||
}
|
||
|
||
return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
|
||
}
|
||
|
||
/* 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
|