Bug 1722322 - Implement LargestContentfulPaint r=emilio

Spec: https://w3c.github.io/largest-contentful-paint

Differential Revision: https://phabricator.services.mozilla.com/D151079
This commit is contained in:
Sean Feng 2023-11-02 17:04:47 +00:00
parent 62e8223aca
commit 611093989e
30 changed files with 1079 additions and 52 deletions

View File

@ -647,7 +647,7 @@ IntersectionInput DOMIntersectionObserver::ComputeInput(
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
// (steps 2.1 - 2.5)
IntersectionOutput DOMIntersectionObserver::Intersect(
const IntersectionInput& aInput, Element& aTarget,
const IntersectionInput& aInput, const Element& aTarget,
IsContentVisibilityObserver aIsContentVisibilityObserver) {
const bool isSimilarOrigin = SimilarOrigin(aTarget, aInput.mRootNode) ==
BrowsingContextOrigin::Similar;

View File

@ -154,7 +154,7 @@ class DOMIntersectionObserver final : public nsISupports,
enum class IsContentVisibilityObserver : bool { No, Yes };
static IntersectionOutput Intersect(
const IntersectionInput&, Element&,
const IntersectionInput&, const Element&,
IsContentVisibilityObserver = IsContentVisibilityObserver::No);
// Intersects with a given rect, already relative to the root frame.
static IntersectionOutput Intersect(const IntersectionInput&, const nsRect&);
@ -168,6 +168,9 @@ class DOMIntersectionObserver final : public nsISupports,
static already_AddRefed<DOMIntersectionObserver>
CreateContentVisibilityObserver(Document&);
static Maybe<nsRect> EdgeInclusiveIntersection(const nsRect& aRect,
const nsRect& aOtherRect);
protected:
void Connect();
void QueueIntersectionObserverEntry(Element* aTarget,

View File

@ -2539,6 +2539,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAll)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocGroup)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameRequestManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentIdentifiersForLCP)
// Traverse all our nsCOMArrays.
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPreloadingImages)
@ -2664,6 +2665,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAll)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mReferrerInfo)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadReferrerInfo)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentIdentifiersForLCP);
if (tmp->mDocGroup && tmp->mDocGroup->GetBrowsingContextGroup()) {
tmp->mDocGroup->GetBrowsingContextGroup()->RemoveDocument(tmp,

View File

@ -53,6 +53,7 @@
#include "mozilla/dom/RadioGroupContainer.h"
#include "mozilla/dom/TreeOrderedArray.h"
#include "mozilla/dom/ViewportMetaData.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/glean/GleanMetrics.h"
#include "nsAtom.h"
#include "nsCOMArray.h"
@ -2428,6 +2429,10 @@ class Document : public nsINode,
LinkedList<MediaQueryList>& MediaQueryLists() { return mDOMMediaQueryLists; }
nsTHashtable<LCPEntryHashEntry>& ContentIdentifiersForLCP() {
return mContentIdentifiersForLCP;
}
/**
* Get the compatibility mode for this document
*/
@ -5043,6 +5048,11 @@ class Document : public nsINode,
// Our live MediaQueryLists
LinkedList<MediaQueryList> mDOMMediaQueryLists;
// A hashset to keep track of which {element, imgRequestProxy}
// combination has been processed to avoid considering the same
// element twice for LargestContentfulPaint.
nsTHashtable<LCPEntryHashEntry> mContentIdentifiersForLCP;
// Array of observers
nsTObserverArray<nsIDocumentObserver*> mObservers;

View File

@ -181,8 +181,13 @@ enum : uint32_t {
// element or has a HTML datalist element ancestor.
ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR = ELEMENT_FLAG_BIT(4),
// If this flag is set on an element, that means this element
// has been considered by our LargestContentfulPaint algorithm and
// it's not going to be considered again.
ELEMENT_PROCESSED_BY_LCP_FOR_TEXT = ELEMENT_FLAG_BIT(5),
// Remaining bits are for subclasses
ELEMENT_TYPE_SPECIFIC_BITS_OFFSET = NODE_TYPE_SPECIFIC_BITS_OFFSET + 5
ELEMENT_TYPE_SPECIFIC_BITS_OFFSET = NODE_TYPE_SPECIFIC_BITS_OFFSET + 6
};
#undef ELEMENT_FLAG_BIT

View File

@ -55,6 +55,7 @@
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/intl/LocaleService.h"
#include "mozilla/intl/Locale.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/net/UrlClassifierFeatureFactory.h"
#include "mozilla/widget/TextRecognition.h"
@ -254,9 +255,11 @@ void nsImageLoadingContent::OnLoadComplete(imgIRequest* aRequest,
FireEvent(u"error"_ns);
}
SVGObserverUtils::InvalidateDirectRenderingObservers(
AsContent()->AsElement());
Element* element = AsContent()->AsElement();
SVGObserverUtils::InvalidateDirectRenderingObservers(element);
MaybeResolveDecodePromises();
LargestContentfulPaint::MaybeProcessImageForElementTiming(mCurrentRequest,
element);
}
void nsImageLoadingContent::OnUnlockedDraw() {

View File

@ -40,6 +40,7 @@
#include "mozilla/dom/NotifyPaintEvent.h"
#include "mozilla/dom/PageTransitionEvent.h"
#include "mozilla/dom/PerformanceEventTiming.h"
#include "mozilla/dom/PerformanceMainThread.h"
#include "mozilla/dom/PointerEvent.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/ScrollAreaEvent.h"
@ -838,6 +839,14 @@ nsresult EventDispatcher::Dispatch(EventTarget* aTarget,
if (aPresContext && !aPresContext->IsPrintingOrPrintPreview()) {
eventTimingEntry =
PerformanceEventTiming::TryGenerateEventTiming(target, aEvent);
if (aEvent->IsTrusted() && aEvent->mMessage == eScroll) {
if (auto* perf = aPresContext->GetPerformanceMainThread()) {
if (!perf->HasDispatchedScrollEvent()) {
perf->SetHasDispatchedScrollEvent();
}
}
}
}
bool retargeted = false;

View File

@ -5,15 +5,30 @@
* 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/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)
mPerformance, mURI, mElement,
mLCPImageEntryKey)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LargestContentfulPaint)
NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
@ -21,19 +36,34 @@ NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
NS_IMPL_ADDREF_INHERITED(LargestContentfulPaint, PerformanceEntry)
NS_IMPL_RELEASE_INHERITED(LargestContentfulPaint, PerformanceEntry)
LargestContentfulPaint::LargestContentfulPaint(Performance* aPerformance,
DOMHighResTimeStamp aRenderTime,
DOMHighResTimeStamp aLoadTime,
unsigned long aSize,
nsIURI* aURI, Element* aElement)
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());
}
ImagePendingRendering::ImagePendingRendering(
const LCPImageEntryKey& aLCPImageEntryKey, DOMHighResTimeStamp aLoadTime)
: mLCPImageEntryKey(aLCPImageEntryKey), mLoadTime(aLoadTime) {}
LargestContentfulPaint::LargestContentfulPaint(
PerformanceMainThread* aPerformance, const DOMHighResTimeStamp aRenderTime,
const DOMHighResTimeStamp aLoadTime, const unsigned long aSize,
nsIURI* aURI, Element* aElement,
const Maybe<const LCPImageEntryKey>& aLCPImageEntryKey)
: PerformanceEntry(aPerformance->GetParentObject(), u""_ns,
kLargestContentfulPaintName),
mPerformance(static_cast<PerformanceMainThread*>(aPerformance)),
mPerformance(aPerformance),
mRenderTime(aRenderTime),
mLoadTime(aLoadTime),
mSize(aSize),
mURI(aURI),
mElement(aElement) {
mElement(aElement),
mLCPImageEntryKey(aLCPImageEntryKey) {
MOZ_ASSERT(mPerformance);
MOZ_ASSERT(mElement);
// The element could be a pseudo-element
@ -58,6 +88,187 @@ Element* LargestContentfulPaint::GetElement() const {
: 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);
}
const LCPImageEntryKey entryKey = LCPImageEntryKey(aElement, aRequest);
if (!document->ContentIdentifiersForLCP().EnsureInserted(entryKey)) {
LOG(" The content identifier existed for element=%p and request=%p, "
"return.",
aElement, aRequest);
return;
}
#ifdef DEBUG
uint32_t status = imgIRequest::STATUS_NONE;
aRequest->GetImageStatus(&status);
MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE);
#endif
// 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.
DOMHighResTimeStamp nowTime =
performance->TimeStampToDOMHighResForRendering(TimeStamp::Now());
if (!request->IsData() && !request->ShouldReportRenderTimeForLCP()) {
// https://wicg.github.io/element-timing/#report-image-element-timing
LOG(" Added a pending image rendering (TAO FAILED)");
// For TAO failed requests, the renderTime is exposed as 0 for
// security reasons.
LCPHelpers::CreateLCPEntryForImage(performance, aElement, aRequest, nowTime,
0 /* aRenderTime */, entryKey);
return;
}
// Otherwise, add the triple (element, imageRequest, now) to roots images
// pending rendering.
//
// 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.
LOG(" Added a pending image rendering (TAO PASSED)");
performance->AddImagesPendingRendering(
ImagePendingRendering{entryKey, nowTime});
}
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", aContainingBlock);
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);
RefPtr<LargestContentfulPaint> entry =
performance->GetImageLCPEntry(aContainingBlock, aImgRequestProxy);
if (!entry) {
LOG(" No Image Entry");
return;
}
entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, performance,
true);
// If area is less than or equal to documents 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 {
return nsRFPService::ReduceTimePrecisionAsMSecs(
mRenderTime, mPerformance->GetRandomTimelineSeed(),
@ -77,9 +288,228 @@ DOMHighResTimeStamp LargestContentfulPaint::StartTime() const {
mPerformance->GetRTPCallerType());
}
/* 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);
}
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 elements content box.
const nsRect& visibleDimensions = aTargetRectRelativeToSelf;
// Let clientContentRect be the smallest DOMRectReadOnly containing
// visibleDimensions with elements 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 imageRequests natural dimension.
// Let naturalArea be naturalWidth * naturalHeight.
double naturalArea =
GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value());
LOG(" naturalArea = %f", naturalArea);
// Let boundingClientArea be clientContentRects width * clientContentRects
// 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 docs 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::CreateLCPEntryForImage(
PerformanceMainThread* aPerformance, Element* aElement,
imgRequestProxy* aRequestProxy, const DOMHighResTimeStamp aLoadTime,
const DOMHighResTimeStamp aRenderTime,
const LCPImageEntryKey& aImageEntryKey) {
MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
MOZ_ASSERT(aRequestProxy);
if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) {
nsCOMPtr<nsIURI> uri;
aRequestProxy->GetURI(getter_AddRefs(uri));
LOG("CreateLCPEntryForImage "
"Element=%p, aRequestProxy=%p, URI=%s loadTime=%f, "
"aRenderTime=%f\n",
aElement, aRequestProxy, uri->GetSpecOrDefault().get(), aLoadTime,
aRenderTime);
}
MOZ_ASSERT(aPerformance);
MOZ_ASSERT_IF(aRenderTime < aLoadTime, aRenderTime == 0);
if (aPerformance->HasDispatchedInputEvent() ||
aPerformance->HasDispatchedScrollEvent()) {
return;
}
// Let url be the empty string.
// If imageRequest is not null, set url to be imageRequests request URL.
nsCOMPtr<nsIURI> requestURI;
aRequestProxy->GetURI(getter_AddRefs(requestURI));
// At this point, we have all the information about the entry
// except the size.
RefPtr<LargestContentfulPaint> entry =
new LargestContentfulPaint(aPerformance, aRenderTime, aLoadTime, 0,
requestURI, aElement, Some(aImageEntryKey));
LOG(" Upsert a LargestContentfulPaint entry=%p to LCPEntryMap.",
entry.get());
aPerformance->StoreImageLCPEntry(aElement, aRequestProxy, entry);
}
void LCPHelpers::FinalizeLCPEntryForText(
PerformanceMainThread* aPerformance, const DOMHighResTimeStamp 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, 0, 0, nullptr, aContainingBlock, Nothing());
entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, aPerformance,
false);
// If area is less than or equal to documents 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();
}
} // namespace mozilla::dom

View File

@ -10,7 +10,8 @@
#include "nsCycleCollectionParticipant.h"
#include "mozilla/dom/PerformanceEntry.h"
#include "mozilla/dom/PerformanceLargestContentfulPaintBinding.h"
#include "nsIWeakReferenceUtils.h"
#include "imgRequestProxy.h"
class nsTextFrame;
namespace mozilla::dom {
@ -21,6 +22,143 @@ static constexpr nsLiteralString kLargestContentfulPaintName =
class Performance;
class PerformanceMainThread;
struct LCPImageEntryKey {
LCPImageEntryKey(Element* aElement, imgRequestProxy* aImgRequestProxy)
: mElement(aElement), mImageRequestProxy(aImgRequestProxy) {
MOZ_ASSERT(aElement);
MOZ_ASSERT(aImgRequestProxy);
}
LCPImageEntryKey(const LCPImageEntryKey& aLCPImageEntryKey) {
mElement = aLCPImageEntryKey.mElement;
mImageRequestProxy = aLCPImageEntryKey.mImageRequestProxy;
}
bool operator==(const LCPImageEntryKey& aOther) const {
return mElement == aOther.mElement &&
mImageRequestProxy == aOther.mImageRequestProxy;
}
bool Equals(const Element* aElement,
const imgRequestProxy* aImgRequestProxy) const {
return mElement == mElement && mImageRequestProxy == mImageRequestProxy;
}
RefPtr<Element> mElement;
RefPtr<imgRequestProxy> mImageRequestProxy;
~LCPImageEntryKey() = default;
};
inline void ImplCycleCollectionUnlink(LCPImageEntryKey& aField) {
aField.mElement = nullptr;
aField.mImageRequestProxy = nullptr;
}
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback, LCPImageEntryKey& aField,
const char* aName, uint32_t aFlags = 0) {
ImplCycleCollectionTraverse(aCallback, aField.mElement,
"LCPImageEntryKey.mElement", aFlags);
ImplCycleCollectionTraverse(aCallback, aField.mImageRequestProxy,
"LCPImageEntryKey.mImageRequestProxy", aFlags);
}
struct LCPTextFrameHelper final {
static void MaybeUnionTextFrame(nsTextFrame* aTextFrame,
const nsRect& aRelativeToSelfRect);
static void HasContainingElementBeenProcessed();
};
class ImagePendingRendering final {
public:
ImagePendingRendering(const LCPImageEntryKey& aLCPImageEntryKey,
DOMHighResTimeStamp aLoadTime);
Element* GetElement() const { return mLCPImageEntryKey.mElement; }
imgRequestProxy* GetImgRequestProxy() const {
return mLCPImageEntryKey.mImageRequestProxy;
}
LCPImageEntryKey mLCPImageEntryKey;
DOMHighResTimeStamp mLoadTime;
};
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback,
ImagePendingRendering& aField, const char* aName, uint32_t aFlags = 0) {
ImplCycleCollectionTraverse(aCallback, aField.mLCPImageEntryKey,
"ImagePendingRendering.mImageEntryKey",
aCallback.Flags());
}
class LCPEntryHashEntry : public PLDHashEntryHdr {
public:
typedef const LCPImageEntryKey& KeyType;
typedef const LCPImageEntryKey* KeyTypePointer;
explicit LCPEntryHashEntry(KeyTypePointer aKey) : mKey(*aKey) {}
LCPEntryHashEntry(LCPEntryHashEntry&&) = default;
~LCPEntryHashEntry() = default;
bool KeyEquals(KeyTypePointer aKey) const { return mKey == *aKey; }
KeyType GetKey() const { return mKey; }
static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
static PLDHashNumber HashKey(KeyTypePointer aKey) {
if (!aKey) {
return 0;
}
return mozilla::HashGeneric(
reinterpret_cast<uintptr_t>(aKey->mElement.get()),
reinterpret_cast<uintptr_t>(aKey->mImageRequestProxy.get()));
}
enum { ALLOW_MEMMOVE = true };
LCPImageEntryKey mKey;
};
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback, LCPEntryHashEntry& aField,
const char* aName, uint32_t aFlags = 0) {
ImplCycleCollectionTraverse(aCallback, aField.mKey, "LCPEntryHashEntry.mKey",
aFlags);
}
class LCPHelpers final {
public:
// Creates the LCP Entry for images with all information except the size of
// the element. The size of the image is unknown at the moment. The entry is
// not going to be queued in this function.
static void CreateLCPEntryForImage(
PerformanceMainThread* aPerformance, Element* aElement,
imgRequestProxy* aRequestProxy, const DOMHighResTimeStamp aLoadTime,
const DOMHighResTimeStamp aRenderTime,
const LCPImageEntryKey& aContentIdentifier);
// Called when the size of the image is known.
static void FinalizeLCPEntryForImage(Element* aContainingBlock,
imgRequestProxy* aImgRequestProxy,
const nsRect& aTargetRectRelativeToSelf);
static void FinalizeLCPEntryForText(PerformanceMainThread* aPerformance,
const DOMHighResTimeStamp aRenderTime,
Element* aContainingBlock,
const nsRect& aTargetRectRelativeToSelf,
const nsPresContext* aPresContext);
static bool IsQualifiedImageRequest(imgRequest* aRequest,
Element* aContainingElement);
private:
static bool CanFinalizeLCPEntry(const nsIFrame* aFrame);
};
// https://w3c.github.io/largest-contentful-paint/
class LargestContentfulPaint final : public PerformanceEntry {
public:
@ -29,10 +167,12 @@ class LargestContentfulPaint final : public PerformanceEntry {
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LargestContentfulPaint,
PerformanceEntry)
LargestContentfulPaint(Performance* aPerformance,
DOMHighResTimeStamp aRenderTime,
DOMHighResTimeStamp aLoadTime, unsigned long aSize,
nsIURI* aURI, Element* aElement);
LargestContentfulPaint(
PerformanceMainThread* aPerformance,
const DOMHighResTimeStamp aRenderTime,
const DOMHighResTimeStamp aLoadTime, const unsigned long aSize,
nsIURI* aURI, Element* aElement,
const Maybe<const LCPImageEntryKey>& aLCPImageEntryKey);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
@ -51,6 +191,23 @@ class LargestContentfulPaint final : public PerformanceEntry {
Element* GetElement() const;
static Element* GetContainingBlockForTextFrame(const nsTextFrame* aTextFrame);
void UpdateSize(const Element* aContainingBlock,
const nsRect& aTargetRectRelativeToSelf,
const PerformanceMainThread* aPerformance, bool aIsImage);
void BufferEntryIfNeeded() override;
static void MaybeProcessImageForElementTiming(imgRequestProxy* aRequest,
Element* aElement);
void QueueEntry();
const Maybe<LCPImageEntryKey>& GetLCPImageEntryKey() const {
return mLCPImageEntryKey;
}
private:
~LargestContentfulPaint() = default;
@ -63,6 +220,8 @@ class LargestContentfulPaint final : public PerformanceEntry {
RefPtr<Element> mElement;
RefPtr<nsAtom> mId;
Maybe<LCPImageEntryKey> mLCPImageEntryKey;
};
} // namespace mozilla::dom
#endif

View File

@ -12,6 +12,7 @@
#include "js/PropertyAndElement.h" // JS_DefineProperty
#include "mozilla/HoldDropJSObjects.h"
#include "PerformanceEventTiming.h"
#include "LargestContentfulPaint.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/EventCounts.h"
@ -25,9 +26,13 @@
#include "nsIChannel.h"
#include "nsIHttpChannel.h"
#include "nsIDocShell.h"
#include "nsTextFrame.h"
#include "nsContainerFrame.h"
namespace mozilla::dom {
extern mozilla::LazyLogModule gLCPLogging;
namespace {
void GetURLSpecFromChannel(nsITimedChannel* aChannel, nsAString& aSpec) {
@ -59,19 +64,24 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMainThread)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMainThread,
Performance)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTiming, mNavigation, mDocEntry, mFCPTiming,
mEventTimingEntries, mFirstInputEvent,
mPendingPointerDown,
mPendingEventTimingEntries, mEventCounts)
NS_IMPL_CYCLE_COLLECTION_UNLINK(
mTiming, mNavigation, mDocEntry, mFCPTiming, mEventTimingEntries,
mLargestContentfulPaintEntries, mFirstInputEvent, mPendingPointerDown,
mPendingEventTimingEntries, mEventCounts)
tmp->mImageLCPEntryMap.Clear();
tmp->mTextFrameUnions.Clear();
tmp->mImagesPendingRendering.Clear();
mozilla::DropJSObjects(tmp);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMainThread,
Performance)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTiming, mNavigation, mDocEntry, mFCPTiming,
mEventTimingEntries, mFirstInputEvent,
mPendingPointerDown,
mPendingEventTimingEntries, mEventCounts)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
mTiming, mNavigation, mDocEntry, mFCPTiming, mEventTimingEntries,
mLargestContentfulPaintEntries, mFirstInputEvent, mPendingPointerDown,
mPendingEventTimingEntries, mEventCounts, mImagesPendingRendering,
mImageLCPEntryMap, mTextFrameUnions)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMainThread,
@ -241,6 +251,15 @@ void PerformanceMainThread::BufferEventTimingEntryIfNeeded(
}
}
void PerformanceMainThread::BufferLargestContentfulPaintEntryIfNeeded(
LargestContentfulPaint* aEntry) {
MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
if (mLargestContentfulPaintEntries.Length() <
kMaxLargestContentfulPaintBufferSize) {
mLargestContentfulPaintEntries.AppendElement(aEntry);
}
}
void PerformanceMainThread::DispatchPendingEventTimingEntries() {
DOMHighResTimeStamp renderingTime = NowUnclamped();
@ -267,7 +286,7 @@ void PerformanceMainThread::DispatchPendingEventTimingEntries() {
MOZ_ASSERT(!mFirstInputEvent);
mFirstInputEvent = mPendingPointerDown.forget();
QueueEntry(mFirstInputEvent);
mHasDispatchedInputEvent = true;
SetHasDispatchedInputEvent();
}
break;
}
@ -277,7 +296,7 @@ void PerformanceMainThread::DispatchPendingEventTimingEntries() {
mFirstInputEvent = entry->Clone();
mFirstInputEvent->SetEntryType(u"first-input"_ns);
QueueEntry(mFirstInputEvent);
mHasDispatchedInputEvent = true;
SetHasDispatchedInputEvent();
break;
}
default:
@ -449,6 +468,12 @@ void PerformanceMainThread::QueueNavigationTimingEntry() {
QueueEntry(mDocEntry);
}
void PerformanceMainThread::QueueLargestContentfulPaintEntry(
LargestContentfulPaint* aEntry) {
MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
QueueEntry(aEntry);
}
EventCounts* PerformanceMainThread::EventCounts() {
MOZ_ASSERT(StaticPrefs::dom_enable_event_timing());
return mEventCounts;
@ -501,6 +526,14 @@ void PerformanceMainThread::GetEntriesByTypeForObserver(
aRetval.AppendElements(mEventTimingEntries);
return;
}
if (StaticPrefs::dom_enable_largest_contentful_paint()) {
if (aEntryType.EqualsLiteral("largest-contentful-paint")) {
aRetval.AppendElements(mLargestContentfulPaintEntries);
return;
}
}
return GetEntriesByType(aEntryType, aRetval);
}
@ -562,4 +595,129 @@ size_t PerformanceMainThread::SizeOfEventEntries(
}
return eventEntries;
}
void PerformanceMainThread::ProcessElementTiming() {
if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
return;
}
const bool shouldLCPDataEmpty =
HasDispatchedInputEvent() || HasDispatchedScrollEvent();
MOZ_ASSERT_IF(shouldLCPDataEmpty,
mTextFrameUnions.IsEmpty() && mImageLCPEntryMap.IsEmpty());
if (shouldLCPDataEmpty) {
return;
}
nsPresContext* presContext = GetPresShell()->GetPresContext();
MOZ_ASSERT(presContext);
// After https://github.com/w3c/largest-contentful-paint/issues/104 is
// resolved, LargestContentfulPaint and FirstContentfulPaint should
// be using the same timestamp, which should be the same timestamp
// as to what https://w3c.github.io/paint-timing/#mark-paint-timing step 2
// defines.
// TODO(sefeng): Check the timestamp after this issue is resolved.
DOMHighResTimeStamp rawNowTime =
TimeStampToDOMHighResForRendering(presContext->GetMarkPaintTimingStart());
MOZ_ASSERT(GetOwnerGlobal());
Document* document = GetOwnerGlobal()->GetAsInnerWindow()->GetExtantDoc();
if (!document ||
!nsContentUtils::GetInProcessSubtreeRootDocument(document)->IsActive()) {
return;
}
nsTArray<ImagePendingRendering> imagesPendingRendering =
std::move(mImagesPendingRendering);
for (const auto& imagePendingRendering : imagesPendingRendering) {
RefPtr<Element> element = imagePendingRendering.GetElement();
if (!element) {
continue;
}
MOZ_ASSERT(imagePendingRendering.mLoadTime < rawNowTime);
if (imgRequestProxy* requestProxy =
imagePendingRendering.GetImgRequestProxy()) {
LCPHelpers::CreateLCPEntryForImage(
this, element, requestProxy, imagePendingRendering.mLoadTime,
rawNowTime, imagePendingRendering.mLCPImageEntryKey);
}
}
MOZ_ASSERT(mImagesPendingRendering.IsEmpty());
}
void PerformanceMainThread::FinalizeLCPEntriesForText() {
nsPresContext* presContext = GetPresShell()->GetPresContext();
MOZ_ASSERT(presContext);
DOMHighResTimeStamp renderTime =
TimeStampToDOMHighResForRendering(presContext->GetMarkPaintTimingStart());
bool canFinalize = StaticPrefs::dom_enable_largest_contentful_paint() &&
!presContext->HasStoppedGeneratingLCP();
if (canFinalize) {
for (const auto& textFrameUnion : GetTextFrameUnions()) {
LCPHelpers::FinalizeLCPEntryForText(
this, renderTime, textFrameUnion.GetKey(), textFrameUnion.GetData(),
presContext);
}
}
ClearTextFrameUnions();
}
void PerformanceMainThread::StoreImageLCPEntry(
Element* aElement, imgRequestProxy* aImgRequestProxy,
LargestContentfulPaint* aEntry) {
mImageLCPEntryMap.InsertOrUpdate({aElement, aImgRequestProxy}, aEntry);
}
already_AddRefed<LargestContentfulPaint>
PerformanceMainThread::GetImageLCPEntry(Element* aElement,
imgRequestProxy* aImgRequestProxy) {
Maybe<RefPtr<LargestContentfulPaint>> entry =
mImageLCPEntryMap.Extract({aElement, aImgRequestProxy});
if (entry.isNothing()) {
return nullptr;
}
Document* doc = aElement->GetComposedDoc();
MOZ_ASSERT(doc, "Element should be connected when it's painted");
const Maybe<const LCPImageEntryKey>& contentIdentifier =
entry.value()->GetLCPImageEntryKey();
if (contentIdentifier.isSome()) {
doc->ContentIdentifiersForLCP().EnsureRemoved(contentIdentifier.value());
}
return entry.value().forget();
}
bool PerformanceMainThread::UpdateLargestContentfulPaintSize(double aSize) {
if (aSize > mLargestContentfulPaintSize) {
mLargestContentfulPaintSize = aSize;
return true;
}
return false;
}
void PerformanceMainThread::SetHasDispatchedScrollEvent() {
mHasDispatchedScrollEvent = true;
ClearGeneratedTempDataForLCP();
}
void PerformanceMainThread::SetHasDispatchedInputEvent() {
mHasDispatchedInputEvent = true;
mImageLCPEntryMap.Clear();
ClearGeneratedTempDataForLCP();
}
void PerformanceMainThread::ClearTextFrameUnions() { mTextFrameUnions.Clear(); }
void PerformanceMainThread::ClearGeneratedTempDataForLCP() {
ClearTextFrameUnions();
mImageLCPEntryMap.Clear();
}
} // namespace mozilla::dom

View File

@ -9,12 +9,19 @@
#include "Performance.h"
#include "PerformanceStorage.h"
#include "LargestContentfulPaint.h"
#include "nsTextFrame.h"
namespace mozilla::dom {
class PerformanceNavigationTiming;
class PerformanceEventTiming;
using ImageLCPEntryMap =
nsTHashMap<LCPEntryHashEntry, RefPtr<LargestContentfulPaint>>;
using TextFrameUnions = nsTHashMap<nsRefPtrHashKey<Element>, nsRect>;
class PerformanceMainThread final : public Performance,
public PerformanceStorage {
public:
@ -45,11 +52,14 @@ class PerformanceMainThread final : public Performance,
const nsAString& aInitiatorType,
const nsAString& aEntryName);
virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override;
bool HadFCPTimingEntry() const { return mFCPTiming; }
void InsertEventTimingEntry(PerformanceEventTiming*) override;
void BufferEventTimingEntryIfNeeded(PerformanceEventTiming*) override;
void DispatchPendingEventTimingEntries() override;
void BufferLargestContentfulPaintEntryIfNeeded(LargestContentfulPaint*);
TimeStamp CreationTimeStamp() const override;
DOMHighResTimeStamp CreationTime() const override;
@ -87,6 +97,7 @@ class PerformanceMainThread final : public Performance,
void UpdateNavigationTimingEntry() override;
void QueueNavigationTimingEntry() override;
void QueueLargestContentfulPaintEntry(LargestContentfulPaint* aEntry);
size_t SizeOfEventEntries(mozilla::MallocSizeOf aMallocSizeOf) const override;
@ -94,10 +105,40 @@ class PerformanceMainThread final : public Performance,
static constexpr uint32_t kDefaultEventTimingDurationThreshold = 104;
static constexpr double kDefaultEventTimingMinDuration = 16.0;
static constexpr uint32_t kMaxLargestContentfulPaintBufferSize = 150;
class EventCounts* EventCounts() override;
bool IsGlobalObjectWindow() const override { return true; };
bool HasDispatchedInputEvent() const { return mHasDispatchedInputEvent; }
void SetHasDispatchedScrollEvent();
bool HasDispatchedScrollEvent() const { return mHasDispatchedScrollEvent; }
void ProcessElementTiming();
void AddImagesPendingRendering(ImagePendingRendering aImagePendingRendering) {
mImagesPendingRendering.AppendElement(aImagePendingRendering);
}
void StoreImageLCPEntry(Element* aElement, imgRequestProxy* aImgRequestProxy,
LargestContentfulPaint* aEntry);
already_AddRefed<LargestContentfulPaint> GetImageLCPEntry(
Element* aElement, imgRequestProxy* aImgRequestProxy);
bool UpdateLargestContentfulPaintSize(double aSize);
double GetLargestContentfulPaintSize() const {
return mLargestContentfulPaintSize;
}
nsTHashMap<nsRefPtrHashKey<Element>, nsRect>& GetTextFrameUnions() {
return mTextFrameUnions;
}
void FinalizeLCPEntriesForText();
protected:
~PerformanceMainThread();
@ -119,23 +160,76 @@ class PerformanceMainThread final : public Performance,
JS::Heap<JSObject*> mMozMemory;
nsTArray<RefPtr<PerformanceEventTiming>> mEventTimingEntries;
nsTArray<RefPtr<LargestContentfulPaint>> mLargestContentfulPaintEntries;
AutoCleanLinkedList<RefPtr<PerformanceEventTiming>>
mPendingEventTimingEntries;
bool mHasDispatchedInputEvent = false;
bool mHasDispatchedScrollEvent = false;
RefPtr<PerformanceEventTiming> mFirstInputEvent;
RefPtr<PerformanceEventTiming> mPendingPointerDown;
private:
void ClearTextFrameUnions();
void ClearGeneratedTempDataForLCP();
void SetHasDispatchedInputEvent();
bool mHasQueuedRefreshdriverObserver = false;
RefPtr<class EventCounts> mEventCounts;
void IncEventCount(const nsAtom* aType);
PresShell* GetPresShell();
nsTArray<ImagePendingRendering> mImagesPendingRendering;
// The key is the pair of the element initiates the image loading
// and the imgRequestProxy of the image, and the value is
// the LCP entry for this image. When the image is
// completely loaded, we add it to mImageLCPEntryMap.
// Later, when the image is painted, we get the LCP entry from it
// to update the size and queue the entry if needed.
//
// When the initiating element is disconnected from the document,
// we keep the orphan entry because if the same memory address is
// reused by a different LCP candidate, it'll update
// mImageLCPEntryMap precedes before it tries to get the LCP entry.
ImageLCPEntryMap mImageLCPEntryMap;
// Keeps track of the rendered size of the largest contentful paint that
// we have processed so far.
double mLargestContentfulPaintSize = 0.0;
// When a text frame is painted, its area (relative to the
// containing block) is unioned with other text frames that
// belong to the same containing block.
// mTextFrameUnions's key is the containing block, and
// the value is the unioned area.
TextFrameUnions mTextFrameUnions;
};
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback, ImageLCPEntryMap& aField,
const char* aName, uint32_t aFlags = 0) {
for (auto& entry : aField) {
ImplCycleCollectionTraverse(aCallback, entry.mKey, "ImageLCPEntryMap.mKey", aCallback.Flags());
RefPtr<LargestContentfulPaint>* lcpEntry = entry.GetModifiableData();
ImplCycleCollectionTraverse(aCallback, *lcpEntry, "ImageLCPEntryMap.mData", aCallback.Flags());
}
}
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback, TextFrameUnions& aField,
const char* aName, uint32_t aFlags = 0) {
for (auto& entry : aField) {
ImplCycleCollectionTraverse(
aCallback, entry, "TextFrameUnions's key (nsRefPtrHashKey<Element>)",
aFlags);
}
}
} // namespace mozilla::dom
#endif // mozilla_dom_PerformanceMainThread_h

View File

@ -17,6 +17,7 @@
#include "nsQueryObject.h"
#include "nsString.h"
#include "PerformanceEntry.h"
#include "LargestContentfulPaint.h"
#include "PerformanceObserverEntryList.h"
using namespace mozilla;
@ -215,6 +216,12 @@ void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions,
}
}
}
if (StaticPrefs::dom_enable_largest_contentful_paint()) {
if (entryTypes.Contains(kLargestContentfulPaintName) &&
!validEntryTypes.Contains(kLargestContentfulPaintName)) {
validEntryTypes.AppendElement(kLargestContentfulPaintName);
}
}
for (const nsLiteralString& name : kValidTypeNames) {
if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) {
validEntryTypes.AppendElement(name);
@ -275,6 +282,12 @@ void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions,
}
}
if (StaticPrefs::dom_enable_largest_contentful_paint()) {
if (type == kLargestContentfulPaintName) {
typeValid = true;
}
}
if (!typeValid) {
ReportUnsupportedTypesErrorToConsole(
NS_IsMainThread(), UnsupportedEntryTypesIgnoredMsgId, type);
@ -328,6 +341,10 @@ void PerformanceObserver::GetSupportedEntryTypes(
validTypes.AppendElement(name);
}
}
if (StaticPrefs::dom_enable_largest_contentful_paint()) {
validTypes.AppendElement(u"largest-contentful-paint"_ns);
}
for (const nsLiteralString& name : kValidTypeNames) {
validTypes.AppendElement(name);
}

View File

@ -776,6 +776,8 @@ let interfaceNamesInGlobalScope = [
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "KeyframeEffect", insecureContext: true },
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "LargestContentfulPaint", insecureContext: true, nightly: true },
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "Location", insecureContext: true },
// IMPORTANT: Do not change this list without review from a DOM peer!
"Lock",

View File

@ -63,6 +63,7 @@ imgRequest::imgRequest(imgLoader* aLoader, const ImageCacheKey& aCacheKey)
mImageAvailable(false),
mIsDeniedCrossSiteCORSRequest(false),
mIsCrossSiteNoCORSRequest(false),
mShouldReportRenderTimeForLCP(false),
mMutex("imgRequest"),
mProgressTracker(new ProgressTracker()),
mIsMultiPartChannel(false),
@ -641,6 +642,7 @@ imgRequest::OnStartRequest(nsIRequest* aRequest) {
mIsCrossSiteNoCORSRequest = loadInfo->GetTainting() == LoadTainting::Opaque;
}
UpdateShouldReportRenderTimeForLCP();
// Figure out if we're multipart.
nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest);
{
@ -1232,3 +1234,13 @@ imgRequest::OnRedirectVerifyCallback(nsresult result) {
mRedirectCallback = nullptr;
return NS_OK;
}
void imgRequest::UpdateShouldReportRenderTimeForLCP() {
if (mTimedChannel) {
bool allRedirectPassTAO = false;
mTimedChannel->GetAllRedirectsPassTimingAllowCheck(&allRedirectPassTAO);
mShouldReportRenderTimeForLCP =
mTimedChannel->TimingAllowCheck(mTriggeringPrincipal) &&
allRedirectPassTAO;
}
}

View File

@ -202,6 +202,10 @@ class imgRequest final : public nsIThreadRetargetableStreamListener,
bool IsCrossSiteNoCORSRequest() const { return mIsCrossSiteNoCORSRequest; }
bool ShouldReportRenderTimeForLCP() const {
return mShouldReportRenderTimeForLCP;
}
private:
friend class FinishPreparingForNewPartRunnable;
@ -209,6 +213,8 @@ class imgRequest final : public nsIThreadRetargetableStreamListener,
void FinishPreparingForNewPart(const NewPartResult& aResult);
void UpdateShouldReportRenderTimeForLCP();
void Cancel(nsresult aStatus);
// Update the cache entry size based on the image container.
@ -276,6 +282,8 @@ class imgRequest final : public nsIThreadRetargetableStreamListener,
bool mIsDeniedCrossSiteCORSRequest;
bool mIsCrossSiteNoCORSRequest;
bool mShouldReportRenderTimeForLCP;
mutable mozilla::Mutex mMutex;
// Member variables protected by mMutex. Note that *all* flags in our bitfield

View File

@ -225,6 +225,10 @@ class imgRequestProxy : public mozilla::PreloaderBase,
bool mHadListener : 1;
};
inline nsISupports* ToSupports(imgRequestProxy* p) {
return NS_ISUPPORTS_CAST(imgIRequest*, p);
}
NS_DEFINE_STATIC_IID_ACCESSOR(imgRequestProxy, NS_IMGREQUESTPROXY_CID)
// Used for static image proxies for which no requests are available, so

View File

@ -11,6 +11,8 @@
#include "Units.h"
#include "mozilla/dom/FontFaceSet.h"
#include "mozilla/dom/ElementBinding.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/dom/PerformanceMainThread.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
@ -11839,6 +11841,13 @@ void PresShell::EndPaint() {
}
return CallState::Continue;
});
if (nsPresContext* presContext = GetPresContext()) {
if (PerformanceMainThread* perf =
presContext->GetPerformanceMainThread()) {
perf->FinalizeLCPEntriesForText();
}
}
}
}

View File

@ -85,6 +85,7 @@ struct RangePaintInfo;
class ReflowCountMgr;
#endif
class WeakFrame;
class nsTextFrame;
class ZoomConstraintsClient;
struct nsCallbackEventRequest;
@ -113,6 +114,7 @@ class Element;
class Event;
class HTMLSlotElement;
class Selection;
class PerformanceMainThread;
} // namespace dom
namespace gfx {

View File

@ -89,6 +89,7 @@
#include "mozilla/Telemetry.h"
#include "mozilla/TimelineManager.h"
#include "mozilla/dom/Performance.h"
#include "mozilla/dom/PerformanceMainThread.h"
#include "mozilla/dom/PerformanceTiming.h"
#include "mozilla/dom/PerformancePaintTiming.h"
#include "mozilla/layers/APZThreadUtils.h"
@ -2781,8 +2782,16 @@ void nsPresContext::NotifyNonBlankPaint() {
}
}
bool nsPresContext::HasStoppedGeneratingLCP() const {
if (auto* perf = GetPerformanceMainThread()) {
return perf->HasDispatchedInputEvent() || perf->HasDispatchedScrollEvent();
}
return true;
}
void nsPresContext::NotifyContentfulPaint() {
if (mHadFirstContentfulPaint) {
if (mHadFirstContentfulPaint && HasStoppedGeneratingLCP()) {
return;
}
nsRootPresContext* rootPresContext = GetRootPresContext();
@ -2804,19 +2813,24 @@ void nsPresContext::NotifyContentfulPaint() {
}
return;
}
mHadFirstContentfulPaint = true;
mFirstContentfulPaintTransactionId =
Some(rootPresContext->mRefreshDriver->LastTransactionId().Next());
if (nsPIDOMWindowInner* innerWindow = mDocument->GetInnerWindow()) {
if (Performance* perf = innerWindow->GetPerformance()) {
if (!mHadFirstContentfulPaint) {
mHadFirstContentfulPaint = true;
mFirstContentfulPaintTransactionId =
Some(rootPresContext->mRefreshDriver->LastTransactionId().Next());
}
if (auto* perf = GetPerformanceMainThread()) {
mMarkPaintTimingStart = TimeStamp::Now();
MOZ_ASSERT(rootPresContext->RefreshDriver()->IsInRefresh(),
"We should only notify contentful paint during refresh "
"driver ticks");
if (!perf->HadFCPTimingEntry()) {
TimeStamp nowTime = rootPresContext->RefreshDriver()->MostRecentRefresh(
/* aEnsureTimerStarted */ false);
MOZ_ASSERT(!nowTime.IsNull(),
"Most recent refresh timestamp should exist since we are in "
"a refresh driver tick");
MOZ_ASSERT(rootPresContext->RefreshDriver()->IsInRefresh(),
"We should only notify contentful paint during refresh "
"driver ticks");
RefPtr<PerformancePaintTiming> paintTiming = new PerformancePaintTiming(
perf, u"first-contentful-paint"_ns, nowTime);
perf->SetFCPTimingEntry(paintTiming);
@ -2833,12 +2847,15 @@ void nsPresContext::NotifyContentfulPaint() {
nsContentUtils::TruncatedURLForDisplay(docURI).get());
PROFILER_MARKER_TEXT(
"FirstContentfulPaint", DOM,
MarkerOptions(MarkerTiming::Interval(navigationStart, nowTime),
MarkerInnerWindowId(innerWindow->WindowID())),
MarkerOptions(
MarkerTiming::Interval(navigationStart, nowTime),
MarkerInnerWindowId(mDocument->GetInnerWindow()->WindowID())),
marker);
}
}
}
perf->ProcessElementTiming();
}
}
@ -3050,6 +3067,16 @@ void nsPresContext::SetSafeAreaInsets(const ScreenIntMargin& aSafeAreaInsets) {
RestyleHint::RecascadeSubtree());
}
PerformanceMainThread* nsPresContext::GetPerformanceMainThread() const {
if (nsPIDOMWindowInner* innerWindow = mDocument->GetInnerWindow()) {
if (auto* perf = static_cast<PerformanceMainThread*>(
innerWindow->GetPerformance())) {
return perf;
}
}
return nullptr;
}
#ifdef DEBUG
void nsPresContext::ValidatePresShellAndDocumentReleation() const {

View File

@ -92,6 +92,7 @@ class LayerManager;
namespace dom {
class Document;
class Element;
class PerformanceMainThread;
enum class PrefersColorSchemeOverride : uint8_t;
} // namespace dom
namespace gfx {
@ -214,6 +215,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
void DocumentCharSetChanged(NotNull<const Encoding*> aCharSet);
mozilla::dom::PerformanceMainThread* GetPerformanceMainThread() const;
/**
* Returns the parent prescontext for this one. Returns null if this is a
* root.
@ -1040,6 +1042,7 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
bool HadNonBlankPaint() const { return mHadNonBlankPaint; }
bool HadFirstContentfulPaint() const { return mHadFirstContentfulPaint; }
bool HasStoppedGeneratingLCP() const;
void NotifyNonBlankPaint();
void NotifyContentfulPaint();
void NotifyPaintStatusReset();
@ -1122,6 +1125,10 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
}
}
mozilla::TimeStamp GetMarkPaintTimingStart() const {
return mMarkPaintTimingStart;
}
protected:
// May be called multiple times (unlink, destructor)
void Destroy();
@ -1245,6 +1252,9 @@ class nsPresContext : public nsISupports, public mozilla::SupportsWeakPtr {
mozilla::TimeStamp mReflowStartTime;
// Defined in https://w3c.github.io/paint-timing/#mark-paint-timing step 2.
mozilla::TimeStamp mMarkPaintTimingStart;
Maybe<TransactionId> mFirstContentfulPaintTransactionId;
mozilla::UniquePtr<mozilla::MediaFeatureChange>

View File

@ -45,6 +45,7 @@
#include "nsITimer.h"
#include "nsLayoutUtils.h"
#include "nsPresContext.h"
#include "imgRequest.h"
#include "nsComponentManagerUtils.h"
#include "mozilla/Logging.h"
#include "mozilla/dom/Document.h"
@ -52,6 +53,7 @@
#include "nsIXULRuntime.h"
#include "jsapi.h"
#include "nsContentUtils.h"
#include "nsTextFrame.h"
#include "mozilla/PendingAnimationTracker.h"
#include "mozilla/PendingFullscreenEvent.h"
#include "mozilla/dom/PerformanceMainThread.h"
@ -71,6 +73,7 @@
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/VsyncMainChild.h"
#include "mozilla/dom/WindowBinding.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/TaskController.h"

View File

@ -143,6 +143,7 @@ EXPORTS.mozilla += [
"CSSAlignUtils.h",
"CSSOrderAwareFrameIterator.h",
"LayoutMessageUtils.h",
"nsVideoFrame.h",
"PrintedSheetFrame.h",
"ReflowInput.h",
"ReflowOutput.h",

View File

@ -5582,6 +5582,8 @@ class MOZ_HEAP_CLASS WeakFrame {
nsIFrame* operator->() { return mFrame; }
operator nsIFrame*() { return mFrame; }
bool operator==(nsIFrame* const aOther) const { return mFrame == aOther; }
void Clear(mozilla::PresShell* aPresShell);
bool IsAlive() const { return !!mFrame; }

View File

@ -27,6 +27,7 @@
#include "mozilla/dom/HTMLImageElement.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/dom/ResponsiveImageSelector.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/image/WebRenderImageProvider.h"
#include "mozilla/layers/RenderRootStateManager.h"
#include "mozilla/layers/WebRenderLayerManager.h"
@ -1115,6 +1116,8 @@ void nsImageFrame::Notify(imgIRequest* aRequest, int32_t aType,
}
if (aType == imgINotificationObserver::LOAD_COMPLETE) {
LargestContentfulPaint::MaybeProcessImageForElementTiming(
static_cast<imgRequestProxy*>(aRequest), GetContent()->AsElement());
uint32_t imgStatus;
aRequest->GetImageStatus(&imgStatus);
nsresult status =
@ -2365,6 +2368,13 @@ bool nsDisplayImage::CreateWebRenderCommands(
mImage->GetImageProvider(aManager->LayerManager(), decodeSize, svgContext,
region, flags, getter_AddRefs(provider));
if (nsCOMPtr<imgIRequest> currentRequest = frame->GetCurrentRequest()) {
LCPHelpers::FinalizeLCPEntryForImage(
frame->GetContent()->AsElement(),
static_cast<imgRequestProxy*>(currentRequest.get()),
GetDestRect() - ToReferenceFrame());
}
// While we got a container, it may not contain a fully decoded surface. If
// that is the case, and we have an image we were previously displaying which
// has a fully decoded surface, then we should prefer the previous image.

View File

@ -27,6 +27,7 @@
#include "mozilla/IntegerRange.h"
#include "mozilla/Unused.h"
#include "mozilla/PodOperations.h"
#include "mozilla/dom/PerformanceMainThread.h"
#include "nsCOMPtr.h"
#include "nsBlockFrame.h"

View File

@ -28,6 +28,7 @@
#include "mozilla/dom/ServiceWorkerRegistration.h"
#include "mozilla/dom/SVGElement.h"
#include "mozilla/dom/TouchEvent.h"
#include "mozilla/dom/PerformanceMainThread.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/PresShell.h"
#include "mozilla/ShapeUtils.h"
@ -1220,7 +1221,7 @@ void nsDisplayListBuilder::LeavePresShell(const nsIFrame* aReferenceFrame,
}
}
nsRootPresContext* rootPresContext = pc->GetRootPresContext();
if (!pc->HadFirstContentfulPaint() && rootPresContext) {
if (!pc->HasStoppedGeneratingLCP() && rootPresContext) {
if (!CurrentPresShellState()->mIsBackgroundOnly) {
if (pc->HasEverBuiltInvisibleText() ||
DisplayListIsContentful(this, aPaintedContents)) {
@ -3429,6 +3430,17 @@ bool nsDisplayBackgroundImage::CreateWebRenderCommands(
if (result == ImgDrawResult::NOT_SUPPORTED) {
return false;
}
if (nsIContent* content = StyleFrame()->GetContent()) {
if (imgRequestProxy* requestProxy = mBackgroundStyle->StyleBackground()
->mImage.mLayers[mLayer]
.mImage.GetImageRequest()) {
// LCP don't consider gradient backgrounds.
LCPHelpers::FinalizeLCPEntryForImage(content->AsElement(), requestProxy,
mBounds - ToReferenceFrame());
}
}
return true;
}
@ -7483,6 +7495,10 @@ void nsDisplayText::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
// the WebRender fallback painting path, and we don't want to issue
// recorded commands that are dependent on the visible/building rect.
RenderToContext(aCtx, aBuilder, GetPaintRect(aBuilder, aCtx));
auto* textFrame = static_cast<nsTextFrame*>(mFrame);
LCPTextFrameHelper::MaybeUnionTextFrame(textFrame,
mBounds - ToReferenceFrame());
}
bool nsDisplayText::CreateWebRenderCommands(
@ -7568,6 +7584,8 @@ bool nsDisplayText::CreateWebRenderCommands(
gfxContext* textDrawer = aBuilder.GetTextContext(aResources, aSc, aManager,
this, bounds, deviceOffset);
LCPTextFrameHelper::MaybeUnionTextFrame(f, bounds - ToReferenceFrame());
aBuilder.StartGroup(this);
RenderToContext(textDrawer, aDisplayListBuilder, mVisibleRect,

View File

@ -12,6 +12,7 @@
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/dom/ImageTracker.h"
#include "nsContentUtils.h"
#include "nsIReflowCallback.h"
@ -818,10 +819,16 @@ void ImageLoader::OnLoadComplete(imgIRequest* aRequest) {
// may happen during other network events.
UnblockOnloadIfNeeded(fwf);
}
if (fwf.mFrame->StyleVisibility()->IsVisible()) {
fwf.mFrame->SchedulePaint();
nsIFrame* frame = fwf.mFrame;
if (frame->StyleVisibility()->IsVisible()) {
frame->SchedulePaint();
}
if (StaticPrefs::dom_enable_largest_contentful_paint()) {
LargestContentfulPaint::MaybeProcessImageForElementTiming(
static_cast<imgRequestProxy*>(aRequest),
frame->GetContent()->AsElement());
}
}
}
} // namespace mozilla::css

View File

@ -28,6 +28,7 @@
#include "mozilla/SVGUtils.h"
#include "mozilla/dom/MutationEventBinding.h"
#include "mozilla/dom/SVGImageElement.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "nsIReflowCallback.h"
using namespace mozilla::dom;
@ -391,6 +392,12 @@ void SVGImageFrame::PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform,
LayoutDeviceSize devPxSize(width, height);
nsRect destRect(nsPoint(), LayoutDevicePixel::ToAppUnits(
devPxSize, appUnitsPerDevPx));
nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest();
if (currentRequest) {
LCPHelpers::FinalizeLCPEntryForImage(
GetContent()->AsElement(),
static_cast<imgRequestProxy*>(currentRequest.get()), destRect);
}
// Note: Can't use DrawSingleUnscaledImage for the TYPE_VECTOR case.
// That method needs our image to have a fixed native width & height,
@ -467,14 +474,7 @@ bool SVGImageFrame::CreateWebRenderCommands(
// try to setup the image
if (!mImageContainer) {
nsCOMPtr<imgIRequest> currentRequest;
nsCOMPtr<nsIImageLoadingContent> imageLoader =
do_QueryInterface(GetContent());
if (imageLoader) {
imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
getter_AddRefs(currentRequest));
}
nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest();
if (currentRequest) {
currentRequest->GetImage(getter_AddRefs(mImageContainer));
}
@ -633,6 +633,14 @@ bool SVGImageFrame::CreateWebRenderCommands(
mImageContainer, this, destRect, clipRect, aSc, flags, svgContext,
region);
if (nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest()) {
LCPHelpers::FinalizeLCPEntryForImage(
GetContent()->AsElement(),
static_cast<imgRequestProxy*>(currentRequest.get()),
LayoutDeviceRect::ToAppUnits(destRect, appUnitsPerDevPx) -
toReferenceFrame);
}
RefPtr<image::WebRenderImageProvider> provider;
ImgDrawResult drawResult = mImageContainer->GetImageProvider(
aManager->LayerManager(), decodeSize, svgContext, region, flags,
@ -783,6 +791,17 @@ bool SVGImageFrame::ReflowFinished() {
void SVGImageFrame::ReflowCallbackCanceled() { mReflowCallbackPosted = false; }
already_AddRefed<imgIRequest> SVGImageFrame::GetCurrentRequest() const {
nsCOMPtr<imgIRequest> request;
nsCOMPtr<nsIImageLoadingContent> imageLoader =
do_QueryInterface(GetContent());
if (imageLoader) {
imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
getter_AddRefs(request));
}
return request.forget();
}
bool SVGImageFrame::IgnoreHitTest() const {
switch (Style()->PointerEvents()) {
case StylePointerEvents::None:

View File

@ -118,6 +118,8 @@ class SVGImageFrame final : public nsIFrame,
private:
bool IgnoreHitTest() const;
already_AddRefed<imgIRequest> GetCurrentRequest() const;
gfx::Matrix GetRasterImageTransform(int32_t aNativeWidth,
int32_t aNativeHeight);
gfx::Matrix GetVectorImageTransform();

View File

@ -2392,7 +2392,7 @@
# Whether the LargestContentfulPaint API will be gathered and returned by performance observer*
- name: dom.enable_largest_contentful_paint
type: RelaxedAtomicBool
value: false
value: @IS_NIGHTLY_BUILD@
mirror: always
# Whether performance.GetEntries* will contain an entry for the active document