Bug 1880928 - Remove last remembered size observer, handle it in Document::DetermineProximityToViewportAndNotifyResizeObservers. r=emilio,layout-reviewers

The CSS Box Sizing specification indicates that last remembered sizes
are recorded "at the time that ResizeObserver events are determined and
delivered" [1].

In bug 1807253, we changed the implementation of when proximity to the
viewport of `content-visibility: auto` nodes are determined and of when
resize observations are broadcast, in order to align with the latest
version of the HTML specification [2]. We continue to use an internal
`Document::mLastRememberedSizeObserver` to update last remembered sizes
but it has been causing issues (e.g. bug 1867090 and bug 1880928) and
could be replaced by a direct update before broadcasting resize
observations.

This is what the current patch is doing. The elements currently observed
by `Document::mLastRememberedSizeObserver` are now stored on a
`Document::mElementsWithLastRememberedSize` hashset and a new function
`Document::UpdateLastRememberedSizes` is called before broadcasting
resize observations, and peforms the work of `LastRememberedSizeCallback`
and of `CalculateBoxSize` (with `aBox=Content_box`).

The only behavior change is in the `while(true)` loop from
`DetermineProximityToViewportAndNotifyResizeObservers`: at each step
we update the last remember sizes for elements of arbitrary depth, and
don't use these depths for calculating `shallowestTargetDepth`. This is
fine, since our `LastRememberedSizeCallback` only records current box
sizes without causing significant side effects (e.g. execution of
JavaScript code) that may require a relayout.

[1] https://drafts.csswg.org/css-sizing-4/#last-remembered
[2] https://html.spec.whatwg.org/#update-the-rendering

Differential Revision: https://phabricator.services.mozilla.com/D202571
This commit is contained in:
Frédéric Wang 2024-02-29 08:50:55 +00:00
parent d748552981
commit c592b5761b
6 changed files with 110 additions and 101 deletions

View File

@ -104,6 +104,7 @@
#include "mozilla/SMILTimeContainer.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Components.h"
#include "mozilla/SVGUtils.h"
#include "mozilla/ServoStyleConsts.h"
#include "mozilla/ServoTypes.h"
#include "mozilla/SizeOfState.h"
@ -2503,7 +2504,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOnloadBlocker)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLazyLoadObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastRememberedSizeObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElementsObservedForLastRememberedSize)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMImplementation)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageMaps)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOrientationPendingPromise)
@ -2625,7 +2626,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(mLazyLoadObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastRememberedSizeObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mElementsObservedForLastRememberedSize);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n)
@ -16447,27 +16448,79 @@ DOMIntersectionObserver& Document::EnsureLazyLoadObserver() {
return *mLazyLoadObserver;
}
ResizeObserver& Document::EnsureLastRememberedSizeObserver() {
if (!mLastRememberedSizeObserver) {
mLastRememberedSizeObserver =
ResizeObserver::CreateLastRememberedSizeObserver(*this);
}
return *mLastRememberedSizeObserver;
}
void Document::ObserveForLastRememberedSize(Element& aElement) {
if (NS_WARN_IF(!IsActive())) {
return;
}
// Options are initialized with ResizeObserverBoxOptions::Content_box by
// default, which is what we want.
static ResizeObserverOptions options;
EnsureLastRememberedSizeObserver().Observe(aElement, options);
mElementsObservedForLastRememberedSize.Insert(&aElement);
}
void Document::UnobserveForLastRememberedSize(Element& aElement) {
if (mLastRememberedSizeObserver) {
mLastRememberedSizeObserver->Unobserve(aElement);
mElementsObservedForLastRememberedSize.Remove(&aElement);
}
void Document::UpdateLastRememberedSizes() {
auto shouldRemoveElement = [&](auto* element) {
if (element->GetComposedDoc() != this) {
element->RemoveLastRememberedBSize();
element->RemoveLastRememberedISize();
return true;
}
return !element->GetPrimaryFrame();
};
for (auto it = mElementsObservedForLastRememberedSize.begin(),
end = mElementsObservedForLastRememberedSize.end();
it != end; ++it) {
if (shouldRemoveElement(*it)) {
mElementsObservedForLastRememberedSize.Remove(it);
continue;
}
const auto element = *it;
MOZ_ASSERT(element->GetComposedDoc() == this);
nsIFrame* frame = element->GetPrimaryFrame();
MOZ_ASSERT(frame);
// As for ResizeObserver, skip nodes hidden `content-visibility`.
if (frame->IsHiddenByContentVisibilityOnAnyAncestor()) {
continue;
}
MOZ_ASSERT(!frame->IsLineParticipant() || frame->IsReplaced(),
"Should have unobserved non-replaced inline.");
MOZ_ASSERT(!frame->HidesContent(),
"Should have unobserved element skipping its contents.");
const nsStylePosition* stylePos = frame->StylePosition();
const WritingMode wm = frame->GetWritingMode();
bool canUpdateBSize = stylePos->ContainIntrinsicBSize(wm).HasAuto();
bool canUpdateISize = stylePos->ContainIntrinsicISize(wm).HasAuto();
MOZ_ASSERT(canUpdateBSize || !element->HasLastRememberedBSize(),
"Should have removed the last remembered block size.");
MOZ_ASSERT(canUpdateISize || !element->HasLastRememberedISize(),
"Should have removed the last remembered inline size.");
MOZ_ASSERT(canUpdateBSize || canUpdateISize,
"Should have unobserved if we can't update any size.");
AutoTArray<LogicalPixelSize, 1> contentSizeList =
ResizeObserver::CalculateBoxSize(element,
ResizeObserverBoxOptions::Content_box,
/* aForceFragmentHandling */ true);
MOZ_ASSERT(!contentSizeList.IsEmpty());
if (canUpdateBSize) {
float bSize = 0;
for (const auto& current : contentSizeList) {
bSize += current.BSize();
}
element->SetLastRememberedBSize(bSize);
}
if (canUpdateISize) {
float iSize = 0;
for (const auto& current : contentSizeList) {
iSize = std::max(iSize, current.ISize());
}
element->SetLastRememberedISize(iSize);
}
}
}
@ -17200,6 +17253,11 @@ void Document::DetermineProximityToViewportAndNotifyResizeObservers() {
}
}
// Last remembered sizes are recorded "at the time that ResizeObserver
// events are determined and delivered".
// https://drafts.csswg.org/css-sizing-4/#last-remembered
UpdateLastRememberedSizes();
// To avoid infinite resize loop, we only gather all active observations
// that have the depth of observed target element more than current
// shallowestTargetDepth.

View File

@ -3723,12 +3723,12 @@ class Document : public nsINode,
DOMIntersectionObserver* GetLazyLoadObserver() { return mLazyLoadObserver; }
DOMIntersectionObserver& EnsureLazyLoadObserver();
ResizeObserver* GetLastRememberedSizeObserver() {
return mLastRememberedSizeObserver;
bool HasElementsWithLastRememberedSize() const {
return !mElementsObservedForLastRememberedSize.IsEmpty();
}
ResizeObserver& EnsureLastRememberedSizeObserver();
void ObserveForLastRememberedSize(Element&);
void UnobserveForLastRememberedSize(Element&);
void UpdateLastRememberedSizes();
// Dispatch a runnable related to the document.
nsresult Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) const;
@ -5167,9 +5167,9 @@ class Document : public nsINode,
RefPtr<DOMIntersectionObserver> mLazyLoadObserver;
// ResizeObserver for storing and removing the last remembered size.
// Elements observed for a last remembered size.
// @see {@link https://drafts.csswg.org/css-sizing-4/#last-remembered}
RefPtr<ResizeObserver> mLastRememberedSizeObserver;
nsTHashSet<RefPtr<Element>> mElementsObservedForLastRememberedSize;
// Stack of top layer elements.
nsTArray<nsWeakPtr> mTopLayer;

View File

@ -2144,11 +2144,10 @@ void Element::UnbindFromTree(UnbindContext& aContext) {
}
if (HasLastRememberedBSize() || HasLastRememberedISize()) {
// Need to remove the last remembered size at the next ResizeObserver
// opportunity, so observe the element. But if already observed, we still
// want the callback to be invoked even if the size was already 0x0, so
// unobserve it first.
document->UnobserveForLastRememberedSize(*this);
// Make sure the element is observed so that remembered sizes are kept
// until the next time "ResizeObserver events are determined and
// delivered". See "Disconnected element" tests from
// css/css-sizing/contain-intrinsic-size/auto-006.html
document->ObserveForLastRememberedSize(*this);
}
}

View File

@ -66,16 +66,9 @@ static nsSize GetContentRectSize(const nsIFrame& aFrame) {
return aFrame.GetContentRectRelativeToSelf().Size();
}
/**
* Returns |aTarget|'s size in the form of gfx::Size (in pixels).
* If the target is an SVG that does not participate in CSS layout,
* its width and height are determined from bounding box.
*
* https://www.w3.org/TR/resize-observer-1/#calculate-box-size
*/
static AutoTArray<LogicalPixelSize, 1> CalculateBoxSize(
AutoTArray<LogicalPixelSize, 1> ResizeObserver::CalculateBoxSize(
Element* aTarget, ResizeObserverBoxOptions aBox,
const ResizeObserver& aObserver) {
bool aForceFragmentHandling) {
nsIFrame* frame = aTarget->GetPrimaryFrame();
if (!frame) {
@ -158,7 +151,7 @@ static AutoTArray<LogicalPixelSize, 1> CalculateBoxSize(
return CSSPixel::FromAppUnits(GetContentRectSize(*aFrame)).ToUnknownSize();
};
if (!StaticPrefs::dom_resize_observer_support_fragments() &&
!aObserver.HasNativeCallback()) {
!aForceFragmentHandling) {
return {LogicalPixelSize(frame->GetWritingMode(), GetFrameSize(frame))};
}
AutoTArray<LogicalPixelSize, 1> size;
@ -209,7 +202,7 @@ bool ResizeObservation::IsActive() const {
}
return mLastReportedSize !=
CalculateBoxSize(mTarget, mObservedBox, *mObserver);
ResizeObserver::CalculateBoxSize(mTarget, mObservedBox);
}
void ResizeObservation::UpdateLastReportedSize(
@ -383,12 +376,12 @@ uint32_t ResizeObserver::BroadcastActiveObservations() {
for (auto& observation : mActiveTargets) {
Element* target = observation->Target();
auto borderBoxSize =
CalculateBoxSize(target, ResizeObserverBoxOptions::Border_box, *this);
auto contentBoxSize =
CalculateBoxSize(target, ResizeObserverBoxOptions::Content_box, *this);
auto devicePixelContentBoxSize = CalculateBoxSize(
target, ResizeObserverBoxOptions::Device_pixel_content_box, *this);
auto borderBoxSize = ResizeObserver::CalculateBoxSize(
target, ResizeObserverBoxOptions::Border_box);
auto contentBoxSize = ResizeObserver::CalculateBoxSize(
target, ResizeObserverBoxOptions::Content_box);
auto devicePixelContentBoxSize = ResizeObserver::CalculateBoxSize(
target, ResizeObserverBoxOptions::Device_pixel_content_box);
RefPtr<ResizeObserverEntry> entry =
new ResizeObserverEntry(mOwner, *target, borderBoxSize, contentBoxSize,
devicePixelContentBoxSize);
@ -524,61 +517,6 @@ void ResizeObserverEntry::SetDevicePixelContentSize(
}
}
static void LastRememberedSizeCallback(
const Sequence<OwningNonNull<ResizeObserverEntry>>& aEntries,
ResizeObserver& aObserver) {
for (const auto& entry : aEntries) {
Element* target = entry->Target();
if (!target->IsInComposedDoc()) {
aObserver.Unobserve(*target);
target->RemoveLastRememberedBSize();
target->RemoveLastRememberedISize();
continue;
}
nsIFrame* frame = target->GetPrimaryFrame();
if (!frame) {
aObserver.Unobserve(*target);
continue;
}
MOZ_ASSERT(!frame->IsLineParticipant() || frame->IsReplaced(),
"Should have unobserved non-replaced inline.");
MOZ_ASSERT(!frame->HidesContent(),
"Should have unobserved element skipping its contents.");
const nsStylePosition* stylePos = frame->StylePosition();
const WritingMode wm = frame->GetWritingMode();
bool canUpdateBSize = stylePos->ContainIntrinsicBSize(wm).HasAuto();
bool canUpdateISize = stylePos->ContainIntrinsicISize(wm).HasAuto();
MOZ_ASSERT(canUpdateBSize || !target->HasLastRememberedBSize(),
"Should have removed the last remembered block size.");
MOZ_ASSERT(canUpdateISize || !target->HasLastRememberedISize(),
"Should have removed the last remembered inline size.");
MOZ_ASSERT(canUpdateBSize || canUpdateISize,
"Should have unobserved if we can't update any size.");
AutoTArray<RefPtr<ResizeObserverSize>, 1> contentSizeList;
entry->GetContentBoxSize(contentSizeList);
MOZ_ASSERT(!contentSizeList.IsEmpty());
if (canUpdateBSize) {
float bSize = 0;
for (const auto& current : contentSizeList) {
bSize += current->BlockSize();
}
target->SetLastRememberedBSize(bSize);
}
if (canUpdateISize) {
float iSize = 0;
for (const auto& current : contentSizeList) {
iSize = std::max(iSize, current->InlineSize());
}
target->SetLastRememberedISize(iSize);
}
}
}
/* static */ already_AddRefed<ResizeObserver>
ResizeObserver::CreateLastRememberedSizeObserver(Document& aDocument) {
return do_AddRef(new ResizeObserver(aDocument, LastRememberedSizeCallback));
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverSize, mOwner)
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverSize)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverSize)

View File

@ -191,8 +191,21 @@ class ResizeObserver final : public nsISupports, public nsWrapperCache {
*/
MOZ_CAN_RUN_SCRIPT uint32_t BroadcastActiveObservations();
static already_AddRefed<ResizeObserver> CreateLastRememberedSizeObserver(
Document&);
/**
* Returns |aTarget|'s size in the form of gfx::Size (in pixels).
* If the target is an SVG that does not participate in CSS layout,
* its width and height are determined from bounding box. Otherwise, the
* relevant box is determined according to the |aBox| parameter.
*
* If dom.resize_observer.support_fragments is enabled, or if
* |aForceFragmentHandling| is true then the function reports the size of all
* fragments, and not just the first one.
*
* https://www.w3.org/TR/resize-observer-1/#calculate-box-size
*/
static AutoTArray<LogicalPixelSize, 1> CalculateBoxSize(
Element* aTarget, ResizeObserverBoxOptions aBox,
bool aForceFragmentHandling = false);
protected:
~ResizeObserver() { Disconnect(); }

View File

@ -2264,7 +2264,8 @@ void nsRefreshDriver::DetermineProximityToViewportAndNotifyResizeObservers() {
return false;
}
return ps->HasContentVisibilityAutoFrames() ||
aDocument->HasResizeObservers();
aDocument->HasResizeObservers() ||
aDocument->HasElementsWithLastRememberedSize();
};
AutoTArray<RefPtr<Document>, 32> documents;