From 71ce0cfc2a475733fa809697739c38f6052ce7c9 Mon Sep 17 00:00:00 2001 From: Sean Feng Date: Fri, 29 Jan 2021 17:03:25 +0000 Subject: [PATCH] Bug 1687358 - Implement telemetry probes to determine the best possible threshold for r=emilio This patch implements five telemetry probes to help us learn how lazyload thresholds perform in the wild. Differential Revision: https://phabricator.services.mozilla.com/D102845 --- dom/base/DOMIntersectionObserver.cpp | 21 ++++- dom/base/DOMIntersectionObserver.h | 2 + dom/base/Document.cpp | 41 ++++++++++ dom/base/Document.h | 29 +++++++ dom/html/HTMLImageElement.cpp | 38 ++++++++-- dom/html/HTMLImageElement.h | 3 +- layout/base/tests/browser.ini | 4 + .../base/tests/browser_lazyload_telemetry.js | 76 +++++++++++++++++++ .../base/tests/file_lazyload_telemetry.html | 9 +++ toolkit/components/telemetry/Histograms.json | 60 +++++++++++++++ 10 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 layout/base/tests/browser_lazyload_telemetry.js create mode 100644 layout/base/tests/file_lazyload_telemetry.html diff --git a/dom/base/DOMIntersectionObserver.cpp b/dom/base/DOMIntersectionObserver.cpp index 5fb70f5c25a2..2aa1494de36e 100644 --- a/dom/base/DOMIntersectionObserver.cpp +++ b/dom/base/DOMIntersectionObserver.cpp @@ -152,7 +152,18 @@ static void LazyLoadCallback( MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img)); if (entry->IsIntersecting()) { static_cast(entry->Target()) - ->StopLazyLoadingAndStartLoadIfNeeded(); + ->StopLazyLoadingAndStartLoadIfNeeded(true); + } + } +} + +static void LazyLoadCallbackReachViewport( + const Sequence>& aEntries) { + for (const auto& entry : aEntries) { + MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img)); + if (entry->IsIntersecting()) { + static_cast(entry->Target()) + ->LazyLoadImageReachedViewport(); } } } @@ -189,6 +200,14 @@ DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) { return observer.forget(); } +already_AddRefed +DOMIntersectionObserver::CreateLazyLoadObserverViewport(Document& aDocument) { + RefPtr observer = + new DOMIntersectionObserver(aDocument, LazyLoadCallbackReachViewport); + observer->mThresholds.AppendElement(std::numeric_limits::min()); + return observer.forget(); +} + bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) { return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin); } diff --git a/dom/base/DOMIntersectionObserver.h b/dom/base/DOMIntersectionObserver.h index 627c41ecbe0d..1a50dd043def 100644 --- a/dom/base/DOMIntersectionObserver.h +++ b/dom/base/DOMIntersectionObserver.h @@ -127,6 +127,8 @@ class DOMIntersectionObserver final : public nsISupports, static already_AddRefed CreateLazyLoadObserver( Document&); + static already_AddRefed + CreateLazyLoadObserverViewport(Document&); protected: void Connect(); diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index a1d89b61129c..aa671dc64752 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -1403,6 +1403,10 @@ Document::Document(const char* aContentType) mOnloadBlockCount(0), mAsyncOnloadBlockCount(0), mWriteLevel(0), + mLazyLoadImageCount(0), + mLazyLoadImageStarted(0), + mLazyLoadImageReachViewportLoading(0), + mLazyLoadImageReachViewportLoaded(0), mContentEditableCount(0), mEditingState(EditingState::eOff), mCompatMode(eCompatibility_FullStandards), @@ -2374,6 +2378,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOnloadBlocker) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLazyLoadImageObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLazyLoadImageObserverViewport) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMImplementation) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageMaps) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOrientationPendingPromise) @@ -2496,6 +2501,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document) NS_IMPL_CYCLE_COLLECTION_UNLINK(mSecurityInfo) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDisplayDocument) NS_IMPL_CYCLE_COLLECTION_UNLINK(mLazyLoadImageObserver) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLazyLoadImageObserverViewport) NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet) NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n) @@ -15065,6 +15071,25 @@ void Document::ReportDocumentUseCounters() { (" > %s\n", Telemetry::GetHistogramName(id))); Telemetry::Accumulate(id, 1); } + + ReportDocumentLazyLoadCounters(); +} + +void Document::ReportDocumentLazyLoadCounters() { + if (!mLazyLoadImageCount) { + return; + } + Telemetry::Accumulate(Telemetry::LAZYLOAD_IMAGE_TOTAL, mLazyLoadImageCount); + Telemetry::Accumulate(Telemetry::LAZYLOAD_IMAGE_STARTED, + mLazyLoadImageStarted); + Telemetry::Accumulate(Telemetry::LAZYLOAD_IMAGE_NOT_VIEWPORT, + mLazyLoadImageStarted - + mLazyLoadImageReachViewportLoading - + mLazyLoadImageReachViewportLoaded); + Telemetry::Accumulate(Telemetry::LAZYLOAD_IMAGE_VIEWPORT_LOADING, + mLazyLoadImageReachViewportLoading); + Telemetry::Accumulate(Telemetry::LAZYLOAD_IMAGE_VIEWPORT_LOADED, + mLazyLoadImageReachViewportLoaded); } void Document::SendPageUseCounters() { @@ -15193,6 +15218,22 @@ DOMIntersectionObserver& Document::EnsureLazyLoadImageObserver() { return *mLazyLoadImageObserver; } +DOMIntersectionObserver& Document::EnsureLazyLoadImageObserverViewport() { + if (!mLazyLoadImageObserverViewport) { + mLazyLoadImageObserverViewport = + DOMIntersectionObserver::CreateLazyLoadObserverViewport(*this); + } + return *mLazyLoadImageObserverViewport; +} + +void Document::IncLazyLoadImageReachViewport(bool aLoading) { + if (aLoading) { + ++mLazyLoadImageReachViewportLoading; + } else { + ++mLazyLoadImageReachViewportLoaded; + } +} + void Document::NotifyLayerManagerRecreated() { EnumerateActivityObservers(NotifyActivityChanged); EnumerateSubDocuments([](Document& aSubDoc) { diff --git a/dom/base/Document.h b/dom/base/Document.h index 74088f1c45d8..ee1ec957c656 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -3611,6 +3611,9 @@ class Document : public nsINode, // effect once per document, and so is called during document destruction. void ReportDocumentUseCounters(); + // Report how lazyload performs for this document. + void ReportDocumentLazyLoadCounters(); + // Sends page use counters to the parent process to accumulate against the // top-level document. Must be called while we still have access to our // WindowContext. This method has an effect each time it is called, and we @@ -3724,7 +3727,18 @@ class Document : public nsINode, DOMIntersectionObserver* GetLazyLoadImageObserver() { return mLazyLoadImageObserver; } + DOMIntersectionObserver* GetLazyLoadImageObserverViewport() { + return mLazyLoadImageObserverViewport; + } DOMIntersectionObserver& EnsureLazyLoadImageObserver(); + DOMIntersectionObserver& EnsureLazyLoadImageObserverViewport(); + void IncLazyLoadImageCount() { ++mLazyLoadImageCount; } + void DecLazyLoadImageCount() { + MOZ_DIAGNOSTIC_ASSERT(mLazyLoadImageCount > 0); + --mLazyLoadImageCount; + } + void IncLazyLoadImageStarted() { ++mLazyLoadImageStarted; } + void IncLazyLoadImageReachViewport(bool aLoading); // Dispatch a runnable related to the document. nsresult Dispatch(TaskCategory aCategory, @@ -4710,6 +4724,19 @@ class Document : public nsINode, // finishes processing that script. uint32_t mWriteLevel; + // The amount of images that have `loading="lazy"` on the page or have loaded + // lazily already. + uint32_t mLazyLoadImageCount; + // Number of lazy-loaded images that we've started loading as a result of + // triggering the lazy-load observer threshold. + uint32_t mLazyLoadImageStarted; + // Number of lazy-loaded images that reached the viewport which were not done + // loading when they did so. + uint32_t mLazyLoadImageReachViewportLoading; + // Number of lazy-loaded images that reached the viewport and were done + // loading when they did so. + uint32_t mLazyLoadImageReachViewportLoaded; + uint32_t mContentEditableCount; EditingState mEditingState; @@ -5005,6 +5032,8 @@ class Document : public nsINode, nsTHashtable> mIntersectionObservers; RefPtr mLazyLoadImageObserver; + // Used to measure how effective the lazyload thresholds are. + RefPtr mLazyLoadImageObserverViewport; // Stack of top layer elements. nsTArray mTopLayer; diff --git a/dom/html/HTMLImageElement.cpp b/dom/html/HTMLImageElement.cpp index 670513ca0545..0c061aedacf0 100644 --- a/dom/html/HTMLImageElement.cpp +++ b/dom/html/HTMLImageElement.cpp @@ -352,7 +352,7 @@ nsresult HTMLImageElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, static_cast( aOldValue->GetEnumValue()) == Loading::Lazy && !ImageState().HasState(NS_EVENT_STATE_LOADING)) { - StopLazyLoadingAndStartLoadIfNeeded(); + StopLazyLoadingAndStartLoadIfNeeded(false); } } else if (aName == nsGkAtoms::src && !aValue) { // NOTE: regular src value changes are handled in AfterMaybeChangeAttr, so @@ -653,8 +653,18 @@ EventStates HTMLImageElement::IntrinsicState() const { void HTMLImageElement::NodeInfoChanged(Document* aOldDoc) { nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + // Unlike the LazyLoadImageObserver, the intersection observer + // for the viewport could contain the element even if + // it's not lazy-loading. For instance, the element has + // started to load, but haven't reached to the viewport. + // So here we always try to unobserve it. + if (auto* observer = aOldDoc->GetLazyLoadImageObserverViewport()) { + observer->Unobserve(*this); + } + if (mLazyLoading) { aOldDoc->GetLazyLoadImageObserver()->Unobserve(*this); + aOldDoc->DecLazyLoadImageCount(); mLazyLoading = false; SetLazyLoading(); } @@ -1256,10 +1266,9 @@ void HTMLImageElement::SetLazyLoading() { return; } - // There (maybe) is a race condition that we have no LazyLoadImageObserver - // when the root document has been removed from the docshell. - // In the case we don't need to worry about lazy-loading. - OwnerDoc()->EnsureLazyLoadImageObserver().Observe(*this); + doc->EnsureLazyLoadImageObserver().Observe(*this); + doc->EnsureLazyLoadImageObserverViewport().Observe(*this); + doc->IncLazyLoadImageCount(); mLazyLoading = true; UpdateImageState(true); } @@ -1279,13 +1288,28 @@ void HTMLImageElement::StartLoadingIfNeeded() { } } -void HTMLImageElement::StopLazyLoadingAndStartLoadIfNeeded() { +void HTMLImageElement::StopLazyLoadingAndStartLoadIfNeeded( + bool aFromIntersectionObserver) { if (!mLazyLoading) { return; } mLazyLoading = false; - OwnerDoc()->GetLazyLoadImageObserver()->Unobserve(*this); + Document* doc = OwnerDoc(); + doc->GetLazyLoadImageObserver()->Unobserve(*this); StartLoadingIfNeeded(); + + if (aFromIntersectionObserver) { + doc->IncLazyLoadImageStarted(); + } else { + doc->DecLazyLoadImageCount(); + doc->GetLazyLoadImageObserverViewport()->Unobserve(*this); + } +} + +void HTMLImageElement::LazyLoadImageReachedViewport() { + Document* doc = OwnerDoc(); + doc->GetLazyLoadImageObserverViewport()->Unobserve(*this); + doc->IncLazyLoadImageReachViewport(!Complete()); } } // namespace mozilla::dom diff --git a/dom/html/HTMLImageElement.h b/dom/html/HTMLImageElement.h index 45ee2234c55e..8d78b466f999 100644 --- a/dom/html/HTMLImageElement.h +++ b/dom/html/HTMLImageElement.h @@ -261,7 +261,8 @@ class HTMLImageElement final : public nsGenericHTMLElement, const nsAString& aTypeAttr, const nsAString& aMediaAttr, nsAString& aResult); - void StopLazyLoadingAndStartLoadIfNeeded(); + void StopLazyLoadingAndStartLoadIfNeeded(bool aFromIntersectionObserver); + void LazyLoadImageReachedViewport(); protected: virtual ~HTMLImageElement(); diff --git a/layout/base/tests/browser.ini b/layout/base/tests/browser.ini index 258c9f8852bb..41f79638cf54 100644 --- a/layout/base/tests/browser.ini +++ b/layout/base/tests/browser.ini @@ -22,3 +22,7 @@ skip-if = (verify && (os == 'mac')) # bug 1627874 support-files = !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js !/browser/base/content/test/forms/head.js +[browser_lazyload_telemetry.js] +support-files = + file_lazyload_telemetry.html + image_rgrg-256x256.png diff --git a/layout/base/tests/browser_lazyload_telemetry.js b/layout/base/tests/browser_lazyload_telemetry.js new file mode 100644 index 000000000000..5bebd72bdb22 --- /dev/null +++ b/layout/base/tests/browser_lazyload_telemetry.js @@ -0,0 +1,76 @@ +const baseURL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const testFileURL = `${baseURL}file_lazyload_telemetry.html`; + +const { TelemetryTestUtils } = ChromeUtils.import( + "resource://testing-common/TelemetryTestUtils.jsm" +); + +function dataIsReported() { + const snapshot = Services.telemetry.getSnapshotForHistograms("main", false) + .content; + return ( + snapshot.LAZYLOAD_IMAGE_STARTED && snapshot.LAZYLOAD_IMAGE_STARTED.values[4] + ); +} + +add_task(async function testTelemetryCollection() { + Services.telemetry.getHistogramById("LAZYLOAD_IMAGE_TOTAL").clear(); + Services.telemetry.getHistogramById("LAZYLOAD_IMAGE_STARTED").clear(); + Services.telemetry.getHistogramById("LAZYLOAD_IMAGE_NOT_VIEWPORT").clear(); + Services.telemetry + .getHistogramById("LAZYLOAD_IMAGE_VIEWPORT_LOADING") + .clear(); + Services.telemetry.getHistogramById("LAZYLOAD_IMAGE_VIEWPORT_LOADED").clear(); + + const testTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + testFileURL, + true + ); + + await SpecialPowers.spawn( + testTab.linkedBrowser.browsingContext, + [], + async () => { + const image2 = content.document.getElementById("image2"); + image2.scrollIntoView(); + + await new Promise(resolve => { + content.requestAnimationFrame(() => { + content.requestAnimationFrame(resolve); + }); + }); + } + ); + + gBrowser.removeTab(testTab); + + await BrowserTestUtils.waitForCondition(dataIsReported); + + const snapshot = Services.telemetry.getSnapshotForHistograms("main", false) + .content; + + // Ensures we have 4 lazyload images. + is(snapshot.LAZYLOAD_IMAGE_TOTAL.values[4], 1, "total images"); + // All 4 images should be lazy-loaded. + is(snapshot.LAZYLOAD_IMAGE_STARTED.values[4], 1, "started to load"); + // The last image didn't reach to the viewport. + is( + snapshot.LAZYLOAD_IMAGE_NOT_VIEWPORT.values[1], + 1, + "images didn't reach viewport" + ); + // The sum of the images that reached to the viewport + // should be three. This includes all images except + // the last one. + is( + snapshot.LAZYLOAD_IMAGE_VIEWPORT_LOADING.sum + + snapshot.LAZYLOAD_IMAGE_VIEWPORT_LOADED.sum, + 3, + "images reached viewport" + ); +}); diff --git a/layout/base/tests/file_lazyload_telemetry.html b/layout/base/tests/file_lazyload_telemetry.html new file mode 100644 index 000000000000..473822c37fb5 --- /dev/null +++ b/layout/base/tests/file_lazyload_telemetry.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 13ca1b4dc70c..8498d61f34a7 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -4768,6 +4768,66 @@ "releaseChannelCollection": "opt-out", "description": "Number of tabs opened across all windows, collected at most every 5 minutes whenever the user interacts with the browser in the following ways: open tab/window, page load." }, + "LAZYLOAD_IMAGE_TOTAL": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "alert_emails": ["sefeng@mozilla.com"], + "bug_numbers": [1687358], + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "releaseChannelCollection": "opt-out", + "description": "The amount of images that have `loading='lazy'` on the page or have loaded lazily already." + }, + "LAZYLOAD_IMAGE_STARTED": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "alert_emails": ["sefeng@mozilla.com"], + "bug_numbers": [1687358], + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "releaseChannelCollection": "opt-out", + "description": "Number of lazy-loaded images that we've started loading as a result of triggering the lazy-load observer threshold." + }, + "LAZYLOAD_IMAGE_NOT_VIEWPORT": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "alert_emails": ["sefeng@mozilla.com"], + "bug_numbers": [1687358], + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "releaseChannelCollection": "opt-out", + "description": "Number of lazy-loaded images that were started to load but didn't reach to the viewport eventually" + }, + "LAZYLOAD_IMAGE_VIEWPORT_LOADING": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "alert_emails": ["sefeng@mozilla.com"], + "bug_numbers": [1687358], + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "releaseChannelCollection": "opt-out", + "description": "Number of lazy-loaded images that reached the viewport which were not done loading when they did so." + }, + "LAZYLOAD_IMAGE_VIEWPORT_LOADED": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "alert_emails": ["sefeng@mozilla.com"], + "bug_numbers": [1687358], + "expires_in_version": "never", + "kind": "exponential", + "high": 2000, + "n_buckets": 100, + "releaseChannelCollection": "opt-out", + "description": "Number of lazy-loaded images that reached the viewport and were done loading when they did so." + }, "LOADED_TAB_COUNT": { "record_in_processes": ["main"], "products": ["firefox"],