commit d0cad31feab6676471f18793c8feff24b2434ab0 parent 42e172e27aea0d89018c185bfeb8f82c32763815 Author: Cristina Horotan <chorotan@mozilla.com> Date: Sat, 13 Dec 2025 07:19:02 +0200 Revert "Bug 1817051 - Part 3: Start to sample scroll timeline in the HTML loop. r=firefox-style-system-reviewers,layout-reviewers,hiro,emilio" on request for causing timeout This reverts commit 42e172e27aea0d89018c185bfeb8f82c32763815. Revert "Bug 1817051 - Part 2: Add scroll timeline into AnimationTimelinesController. r=hiro,layout-reviewers" This reverts commit 4bdbdb5d7622614b5d37a815dc453664f5220e73. Revert "Bug 1817051 - Part 1: Add AnimationTimelinesController. r=layout-reviewers,hiro,dom-core,smaug" This reverts commit 85c927d79ed90ab7de6adb1e86acddb3d266c0b9. Diffstat:
23 files changed, 331 insertions(+), 272 deletions(-)
diff --git a/dom/animation/AnimationEventDispatcher.h b/dom/animation/AnimationEventDispatcher.h @@ -16,7 +16,6 @@ #include "mozilla/ProfilerMarkers.h" #include "mozilla/Variant.h" #include "mozilla/dom/AnimationPlaybackEvent.h" -#include "mozilla/dom/Document.h" #include "mozilla/dom/KeyframeEffect.h" #include "nsCycleCollectionParticipant.h" #include "nsPresContext.h" diff --git a/dom/animation/AnimationTimelinesController.cpp b/dom/animation/AnimationTimelinesController.cpp @@ -1,64 +0,0 @@ -/* -*- 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 "AnimationTimelinesController.h" - -#include "mozilla/dom/DocumentTimeline.h" -#include "mozilla/dom/ScrollTimeline.h" - -namespace mozilla::dom { - -void AnimationTimelinesController::AddDocumentTimeline( - DocumentTimeline& aTimeline) { - mDocumentTimelines.insertBack(&aTimeline); -} - -void AnimationTimelinesController::AddScrollTimeline( - ScrollTimeline& aTimeline) { - mScrollTimelines.insertBack(&aTimeline); -} - -void AnimationTimelinesController::WillRefresh() { - for (DocumentTimeline* tl : - ToTArray<AutoTArray<RefPtr<DocumentTimeline>, 32>>(mDocumentTimelines)) { - tl->WillRefresh(); - } - - // TODO: Switch to sample scroll timelines in HTML event loop in the following - // patches. - for (ScrollTimeline* tl : - ToTArray<AutoTArray<RefPtr<ScrollTimeline>, 32>>(mScrollTimelines)) { - tl->WillRefresh(); - } -} - -void AnimationTimelinesController::UpdateLastRefreshDriverTime() { - for (DocumentTimeline* timeline : mDocumentTimelines) { - timeline->UpdateLastRefreshDriverTime(); - } - // We don't use refresh driver time stamp for scroll timelines. -} - -void AnimationTimelinesController::TriggerAllPendingAnimationsNow() { - for (DocumentTimeline* timeline : mDocumentTimelines) { - timeline->TriggerAllPendingAnimationsNow(); - } - - // FIXME: Bug 1805950. This is used for testing and printing. We may have to - // revisit there when supporting printing for scroll-timelines () -} - -void AnimationTimelinesController::UpdateHiddenByContentVisibility() { - for (AnimationTimeline* timeline : mDocumentTimelines) { - timeline->UpdateHiddenByContentVisibility(); - } - - for (AnimationTimeline* timeline : mScrollTimelines) { - timeline->UpdateHiddenByContentVisibility(); - } -} - -} // namespace mozilla::dom diff --git a/dom/animation/AnimationTimelinesController.h b/dom/animation/AnimationTimelinesController.h @@ -1,49 +0,0 @@ -/* -*- 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/. */ - -#ifndef mozilla_dom_AnimationTimelinesController_h -#define mozilla_dom_AnimationTimelinesController_h - -#include "mozilla/LinkedList.h" - -namespace mozilla::dom { -class DocumentTimeline; -class ScrollTimeline; - -/** - * The controller which keeps track of all timelines in a document. So basically - * each document should have its own AnimationTimelinesController. - */ -class AnimationTimelinesController final { - public: - AnimationTimelinesController() = default; - ~AnimationTimelinesController() { - // We expect the timelines should remove themself from the controller - // already. - MOZ_ASSERT(mDocumentTimelines.isEmpty()); - MOZ_ASSERT(mScrollTimelines.isEmpty()); - } - - void AddDocumentTimeline(DocumentTimeline& aTimeline); - void AddScrollTimeline(ScrollTimeline& aTimeline); - - void WillRefresh(); - void UpdateLastRefreshDriverTime(); - void TriggerAllPendingAnimationsNow(); - void UpdateHiddenByContentVisibility(); - - private: - LinkedList<DocumentTimeline> mDocumentTimelines; - // Note: we use a separate linked list for scroll timelines (and - // view-timelines) because some utility functions don't need to traverse this - // list for now. If all functions have to check both lists, we should merge - // them. - LinkedList<ScrollTimeline> mScrollTimelines; -}; - -} // namespace mozilla::dom - -#endif // mozilla_dom_AnimationTimelinesController_h diff --git a/dom/animation/DocumentTimeline.cpp b/dom/animation/DocumentTimeline.cpp @@ -47,7 +47,7 @@ DocumentTimeline::DocumentTimeline(Document* aDocument, mDocument(aDocument), mOriginTime(aOriginTime) { if (mDocument) { - mDocument->TimelinesController().AddDocumentTimeline(*this); + mDocument->Timelines().insertBack(this); } // Ensure mLastRefreshDriverTime is valid. UpdateLastRefreshDriverTime(); diff --git a/dom/animation/ElementAnimationData.cpp b/dom/animation/ElementAnimationData.cpp @@ -51,6 +51,7 @@ void ElementAnimationData::ClearAllAnimationCollections() { mElementData.mTransitions = nullptr; mElementData.mScrollTimelines = nullptr; mElementData.mViewTimelines = nullptr; + mElementData.mProgressTimelineScheduler = nullptr; ClearAllPseudos(false); } @@ -76,6 +77,7 @@ void ElementAnimationData::ClearAllPseudos(bool aOnlyViewTransitions) { data->mTransitions = nullptr; data->mScrollTimelines = nullptr; data->mViewTimelines = nullptr; + data->mProgressTimelineScheduler = nullptr; if (data->IsEmpty()) { iter.Remove(); @@ -176,6 +178,13 @@ void ElementAnimationData::ClearViewTimelineCollectionFor( }); } +void ElementAnimationData::ClearProgressTimelineScheduler( + const PseudoStyleRequest& aRequest) { + WithDataForRemoval(aRequest, [](PerElementOrPseudoData& aData) { + aData.mProgressTimelineScheduler = nullptr; + }); +} + ElementAnimationData::PerElementOrPseudoData::PerElementOrPseudoData() = default; ElementAnimationData::PerElementOrPseudoData::~PerElementOrPseudoData() = @@ -228,4 +237,11 @@ ElementAnimationData::PerElementOrPseudoData::DoEnsureViewTimelines( return *mViewTimelines; } +dom::ProgressTimelineScheduler& ElementAnimationData::PerElementOrPseudoData:: + DoEnsureProgressTimelineScheduler() { + MOZ_ASSERT(!mProgressTimelineScheduler); + mProgressTimelineScheduler = MakeUnique<dom::ProgressTimelineScheduler>(); + return *mProgressTimelineScheduler; +} + } // namespace mozilla diff --git a/dom/animation/ElementAnimationData.h b/dom/animation/ElementAnimationData.h @@ -23,6 +23,7 @@ namespace dom { class Element; class CSSAnimation; class CSSTransition; +class ProgressTimelineScheduler; class ScrollTimeline; class ViewTimeline; } // namespace dom @@ -46,15 +47,31 @@ class ElementAnimationData { // // So it should be fine to create timeline objects only on the elements and // pseudo elements which support animations. - // - // Note: TimelineCollection owns and manages the named progress timeline - // generated by specifying scroll-timeline-name property and - // view-timeline-name property on this element. However, the anonymous - // progress timelines (e.g. animation-timeline:scroll()) are owned by - // Animation objects only. UniquePtr<ScrollTimelineCollection> mScrollTimelines; UniquePtr<ViewTimelineCollection> mViewTimelines; + // This is different from |mScrollTimelines|. We use this to schedule all + // scroll-driven animations (which use anonymous/named scroll timelines or + // anonymous/name view timelines) for a specific scroll source (which is the + // element with ScrollContainerFrame). + // + // TimelineCollection owns and manages the named progress timeline generated + // by specifying scroll-timeline-name property and view-timeline-name + // property on this element. However, the anonymous progress timelines (e.g. + // animation-timeline:scroll()) are owned by Animation objects only. + // + // Note: + // 1. For named scroll timelines. The element which specifies + // scroll-timeline-name is the scroll source. However, for named view + // timelines, the element which specifies view-timeline-name may not be + // the scroll source because we use its nearest scroll container as the + // scroll source. + // 2. For anonymous progress timelines, we don't keep their timeline obejcts + // in TimelineCollection. + // So, per 1) and 2), we use |mProgressTimelineScheduler| for the scroll + // source element to schedule scroll-driven animations. + UniquePtr<dom::ProgressTimelineScheduler> mProgressTimelineScheduler; + PerElementOrPseudoData(); ~PerElementOrPseudoData(); @@ -67,10 +84,12 @@ class ElementAnimationData { dom::Element&, const PseudoStyleRequest&); ViewTimelineCollection& DoEnsureViewTimelines(dom::Element&, const PseudoStyleRequest&); + dom::ProgressTimelineScheduler& DoEnsureProgressTimelineScheduler(); bool IsEmpty() const { return !mEffectSet && !mAnimations && !mTransitions && - !mScrollTimelines && !mViewTimelines; + !mScrollTimelines && !mViewTimelines && + !mProgressTimelineScheduler; } void Traverse(nsCycleCollectionTraversalCallback&); @@ -242,6 +261,25 @@ class ElementAnimationData { return data.DoEnsureViewTimelines(aOwner, aRequest); } + dom::ProgressTimelineScheduler* GetProgressTimelineScheduler( + const PseudoStyleRequest& aRequest) const { + if (auto* data = GetData(aRequest)) { + return data->mProgressTimelineScheduler.get(); + } + return nullptr; + } + + void ClearProgressTimelineScheduler(const PseudoStyleRequest& aRequest); + + dom::ProgressTimelineScheduler& EnsureProgressTimelineScheduler( + const PseudoStyleRequest& aRequest) { + auto& data = GetOrCreateData(aRequest); + if (auto* collection = data.mProgressTimelineScheduler.get()) { + return *collection; + } + return data.DoEnsureProgressTimelineScheduler(); + } + ElementAnimationData() = default; }; diff --git a/dom/animation/ScrollTimeline.cpp b/dom/animation/ScrollTimeline.cpp @@ -12,13 +12,9 @@ #include "mozilla/PresShell.h" #include "mozilla/ScrollContainerFrame.h" #include "mozilla/dom/Animation.h" -#include "mozilla/dom/AnimationTimelinesController.h" -#include "mozilla/dom/Document.h" -#include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/ElementInlines.h" #include "nsIFrame.h" #include "nsLayoutUtils.h" -#include "nsRefreshDriver.h" namespace mozilla::dom { @@ -50,12 +46,10 @@ ScrollTimeline::ScrollTimeline(Document* aDocument, const Scroller& aScroller, mSource(aScroller), mAxis(aAxis) { MOZ_ASSERT(aDocument); - - mDocument->TimelinesController().AddScrollTimeline(*this); + RegisterWithScrollSource(); } -/* static */ -std::pair<const Element*, PseudoStyleRequest> +/* static */ std::pair<const Element*, PseudoStyleRequest> ScrollTimeline::FindNearestScroller(Element* aSubject, const PseudoStyleRequest& aPseudoRequest) { MOZ_ASSERT(aSubject); @@ -85,13 +79,7 @@ already_AddRefed<ScrollTimeline> ScrollTimeline::MakeAnonymous( Scroller scroller; switch (aScroller) { case StyleScroller::Root: - // Specifies to use the document viewport as the scroll container. - // - // We use the owner doc of the animation target. This may be different - // from |mDocument| after we implement ScrollTimeline interface for - // script. - scroller = - Scroller::Root(aTarget.mElement->OwnerDoc()->GetDocumentElement()); + scroller = Scroller::Root(aTarget.mElement->OwnerDoc()); break; case StyleScroller::Nearest: { @@ -125,39 +113,43 @@ already_AddRefed<ScrollTimeline> ScrollTimeline::MakeNamed( } Nullable<TimeDuration> ScrollTimeline::GetCurrentTimeAsDuration() const { - if (!mCachedCurrentTime) { + // If no layout box, this timeline is inactive. + if (!mSource || !mSource.mElement->GetPrimaryFrame()) { return nullptr; } - const CurrentTimeData& data = mCachedCurrentTime.ref(); - // FIXME: Scroll offsets on the RTL container is complicated specifically on - // mobile, see https://github.com/w3c/csswg-drafts/issues/12893. For now, we - // use the absoluate value to make things simple. - double progress = - static_cast<double>(std::abs(data.mPosition) - data.mOffsets.mStart) / - static_cast<double>(data.mOffsets.mEnd - data.mOffsets.mStart); - return TimeDuration::FromMilliseconds(progress * - PROGRESS_TIMELINE_DURATION_MILLISEC); -} + // if this is not a scroller container, this timeline is inactive. + const ScrollContainerFrame* scrollContainerFrame = GetScrollContainerFrame(); + if (!scrollContainerFrame) { + return nullptr; + } -void ScrollTimeline::WillRefresh() { - UpdateCachedCurrentTime(); + const auto orientation = Axis(); - if (!mDocument->GetPresShell()) { - // If we're not displayed, don't tick animations. - return; + // If there is no scrollable overflow, then the ScrollTimeline is inactive. + // https://drafts.csswg.org/scroll-animations-1/#scrolltimeline-interface + if (!scrollContainerFrame->GetAvailableScrollingDirections().contains( + orientation)) { + return nullptr; } - if (mAnimationOrder.isEmpty()) { - return; + const bool isHorizontal = orientation == layers::ScrollDirection::eHorizontal; + const nsPoint& scrollPosition = scrollContainerFrame->GetScrollPosition(); + const Maybe<ScrollOffsets>& offsets = + ComputeOffsets(scrollContainerFrame, orientation); + if (!offsets) { + return nullptr; } - // FIXME: Bug 1737927: Need to check the animation mutation observers for - // animations with scroll timelines. - // nsAutoAnimationMutationBatch mb(mDocument); - - TickState dummyState; - Tick(dummyState); + // Note: For RTL, scrollPosition.x or scrollPosition.y may be negative, + // e.g. the range of its value is [0, -range], so we have to use the + // absolute value. + nscoord position = + std::abs(isHorizontal ? scrollPosition.x : scrollPosition.y); + double progress = static_cast<double>(position - offsets->mStart) / + static_cast<double>(offsets->mEnd - offsets->mStart); + return TimeDuration::FromMilliseconds(progress * + PROGRESS_TIMELINE_DURATION_MILLISEC); } layers::ScrollDirection ScrollTimeline::Axis() const { @@ -224,40 +216,41 @@ Maybe<ScrollTimeline::ScrollOffsets> ScrollTimeline::ComputeOffsets( return Some(ScrollOffsets{0, range}); } -void ScrollTimeline::UpdateCachedCurrentTime() { - mCachedCurrentTime.reset(); - - // If no layout box, this timeline is inactive. - if (!mSource || !mSource.mElement->GetPrimaryFrame()) { +void ScrollTimeline::RegisterWithScrollSource() { + if (!mSource) { return; } - // if this is not a scroller container, this timeline is inactive. - const ScrollContainerFrame* scrollContainerFrame = GetScrollContainerFrame(); - if (!scrollContainerFrame) { + auto& scheduler = ProgressTimelineScheduler::Ensure( + mSource.mElement, PseudoStyleRequest(mSource.mPseudoType)); + scheduler.AddTimeline(this); +} + +void ScrollTimeline::UnregisterFromScrollSource() { + if (!mSource) { return; } - const auto orientation = Axis(); - - // If there is no scrollable overflow, then the ScrollTimeline is inactive. - // https://drafts.csswg.org/scroll-animations-1/#scrolltimeline-interface - if (!scrollContainerFrame->GetAvailableScrollingDirections().contains( - orientation)) { + auto* scheduler = ProgressTimelineScheduler::Get( + mSource.mElement, PseudoStyleRequest(mSource.mPseudoType)); + if (!scheduler) { return; } - const nsPoint& scrollPosition = scrollContainerFrame->GetScrollPosition(); - const Maybe<ScrollOffsets>& offsets = - ComputeOffsets(scrollContainerFrame, orientation); - if (!offsets) { + // If we are trying to unregister this timeline from the scheduler in + // ProgressTimelineScheduler::ScheduleAnimations(), we have to unregister this + // after we finish the scheduling, to avoid mutating the hashtset + // and destroying the scheduler here. + if (scheduler->IsInScheduling()) { + mState = TimelineState::PendingRemove; return; } - mCachedCurrentTime.emplace(CurrentTimeData{ - orientation == layers::ScrollDirection::eHorizontal ? scrollPosition.x - : scrollPosition.y, - offsets.value()}); + scheduler->RemoveTimeline(this); + if (scheduler->IsEmpty()) { + ProgressTimelineScheduler::Destroy(mSource.mElement, + PseudoStyleRequest(mSource.mPseudoType)); + } } const ScrollContainerFrame* ScrollTimeline::GetScrollContainerFrame() const { @@ -282,36 +275,78 @@ const ScrollContainerFrame* ScrollTimeline::GetScrollContainerFrame() const { return nullptr; } -static nsRefreshDriver* GetRefreshDriver(Document* aDocument) { - nsPresContext* presContext = aDocument->GetPresContext(); - if (MOZ_UNLIKELY(!presContext)) { +void ScrollTimeline::NotifyAnimationContentVisibilityChanged( + Animation* aAnimation, bool aIsVisible) { + AnimationTimeline::NotifyAnimationContentVisibilityChanged(aAnimation, + aIsVisible); + if (mAnimationOrder.isEmpty()) { + UnregisterFromScrollSource(); + } else { + RegisterWithScrollSource(); + } +} + +// ------------------------------------ +// Methods of ProgressTimelineScheduler +// ------------------------------------ +/* static */ ProgressTimelineScheduler* ProgressTimelineScheduler::Get( + const Element* aElement, const PseudoStyleRequest& aPseudoRequest) { + MOZ_ASSERT(aElement); + auto* data = aElement->GetAnimationData(); + if (!data) { return nullptr; } - return presContext->RefreshDriver(); + + return data->GetProgressTimelineScheduler(aPseudoRequest); +} + +/* static */ ProgressTimelineScheduler& ProgressTimelineScheduler::Ensure( + Element* aElement, const PseudoStyleRequest& aPseudoRequest) { + MOZ_ASSERT(aElement); + return aElement->EnsureAnimationData().EnsureProgressTimelineScheduler( + aPseudoRequest); } -void ScrollTimeline::NotifyAnimationUpdated(Animation& aAnimation) { - AnimationTimeline::NotifyAnimationUpdated(aAnimation); +/* static */ +void ProgressTimelineScheduler::Destroy( + const Element* aElement, const PseudoStyleRequest& aPseudoRequest) { + auto* data = aElement->GetAnimationData(); + MOZ_ASSERT(data); + data->ClearProgressTimelineScheduler(aPseudoRequest); +} + +/* static */ +void ProgressTimelineScheduler::ScheduleAnimations( + const Element* aElement, const PseudoStyleRequest& aRequest) { + auto* scheduler = Get(aElement, aRequest); + if (!scheduler) { + return; + } + + // Note: We only need to handle the removal. It's impossible to iterate the + // non-existing timelines and add them into the hashset. + nsTArray<ScrollTimeline*> timelinesToBeRemoved; - if (!mAnimationOrder.isEmpty()) { - if (auto* rd = GetRefreshDriver(mDocument)) { - MOZ_ASSERT(isInList(), - "We should not register with the refresh driver if we are not" - " in the document's list of timelines"); - rd->EnsureAnimationUpdate(); + scheduler->mIsInScheduling = true; + for (auto iter = scheduler->mTimelines.iter(); !iter.done(); iter.next()) { + auto* timeline = iter.get(); + const auto state = timeline->ScheduleAnimations(); + if (state == ScrollTimeline::TimelineState::PendingRemove) { + timelinesToBeRemoved.AppendElement(timeline); } } -} + MOZ_ASSERT(Get(aElement, aRequest), "Make sure the scheduler still exists"); + scheduler->mIsInScheduling = false; + + // In the common case, this array is empty. It could be non-empty only when + // we change the content-visibility in their Tick()s. + for (auto* timeline : timelinesToBeRemoved) { + timeline->ResetState(); + scheduler->RemoveTimeline(timeline); + } -void ScrollTimeline::NotifyAnimationContentVisibilityChanged( - Animation* aAnimation, bool aIsVisible) { - AnimationTimeline::NotifyAnimationContentVisibilityChanged(aAnimation, - aIsVisible); - if (auto* rd = GetRefreshDriver(mDocument)) { - MOZ_ASSERT(isInList(), - "We should not register with the refresh driver if we are not" - " in the document's list of timelines"); - rd->EnsureAnimationUpdate(); + if (scheduler->IsEmpty()) { + ProgressTimelineScheduler::Destroy(aElement, aRequest); } } diff --git a/dom/animation/ScrollTimeline.h b/dom/animation/ScrollTimeline.h @@ -7,10 +7,11 @@ #ifndef mozilla_dom_ScrollTimeline_h #define mozilla_dom_ScrollTimeline_h -#include "mozilla/LinkedList.h" +#include "mozilla/HashTable.h" #include "mozilla/ServoStyleConsts.h" #include "mozilla/WritingModes.h" #include "mozilla/dom/AnimationTimeline.h" +#include "mozilla/dom/Document.h" #define PROGRESS_TIMELINE_DURATION_MILLISEC 100000 @@ -19,7 +20,6 @@ class ScrollContainerFrame; class ElementAnimationData; struct NonOwningAnimationTarget; namespace dom { -class Document; class Element; /** @@ -60,8 +60,7 @@ class Element; * ScrollTimelineSet, and iterates the set to schedule the animations * linked to the ScrollTimelines. */ -class ScrollTimeline : public AnimationTimeline, - public LinkedListElement<ScrollTimeline> { +class ScrollTimeline : public AnimationTimeline { template <typename T, typename... Args> friend already_AddRefed<T> mozilla::MakeAndAddRef(Args&&... aArgs); @@ -81,8 +80,16 @@ class ScrollTimeline : public AnimationTimeline, // PseudoStyleRequest. PseudoStyleType mPseudoType; - static Scroller Root(Element* aDocumentElement) { - return {Type::Root, aDocumentElement, PseudoStyleType::NotPseudo}; + // We use the owner doc of the animation target. This may be different from + // |mDocument| after we implement ScrollTimeline interface for script. + static Scroller Root(const Document* aOwnerDoc) { + // For auto, we use scrolling element as the default scroller. + // However, it's mutable, and we would like to keep things simple, so + // we always register the ScrollTimeline to the document element (i.e. + // root element) because the content of the root scroll frame is the root + // element. + return {Type::Root, aOwnerDoc->GetDocumentElement(), + PseudoStyleType::NotPseudo}; } static Scroller Nearest(Element* aElement, PseudoStyleType aPseudoType) { @@ -155,7 +162,24 @@ class ScrollTimeline : public AnimationTimeline, return TimeDuration::FromMilliseconds(PROGRESS_TIMELINE_DURATION_MILLISEC); } - void WillRefresh(); + enum class TimelineState : uint8_t { + // Normal case. The timeline is still in the scheduler. + None, + // This timeline is pending for removal from the scheduler. + PendingRemove, + }; + TimelineState ScheduleAnimations() { + MOZ_ASSERT(mState == TimelineState::None); + + // FIXME: Bug 1737927: Need to check the animation mutation observers for + // animations with scroll timelines. + // nsAutoAnimationMutationBatch mb(mDocument); + TickState state; + Tick(state); + // TODO: Do we need to synchronize scroll animations? + + return mState; + } // If the source of a ScrollTimeline is an element whose principal box does // not exist or is not a scroll container, then its phase is the timeline @@ -184,11 +208,11 @@ class ScrollTimeline : public AnimationTimeline, const PseudoStyleRequest& aPseudoRequest, const StyleScrollTimeline& aNew); - void NotifyAnimationUpdated(Animation& aAnimation) override; - void NotifyAnimationContentVisibilityChanged(Animation* aAnimation, bool aIsVisible) override; + void ResetState() { mState = TimelineState::None; } + protected: virtual ~ScrollTimeline() { Teardown(); } ScrollTimeline() = delete; @@ -203,16 +227,16 @@ class ScrollTimeline : public AnimationTimeline, const ScrollContainerFrame* aScrollFrame, layers::ScrollDirection aOrientation) const; - void UpdateCachedCurrentTime(); - // Note: This function is required to be idempotent, as it can be called from // both cycleCollection::Unlink() and ~ScrollTimeline(). When modifying this // function, be sure to preserve this property. - void Teardown() { - if (isInList()) { - remove(); - } - } + void Teardown() { UnregisterFromScrollSource(); } + + // Register this scroll timeline to the element property. + void RegisterWithScrollSource(); + + // Unregister this scroll timeline from the element property. + void UnregisterFromScrollSource(); const ScrollContainerFrame* GetScrollContainerFrame() const; @@ -227,14 +251,55 @@ class ScrollTimeline : public AnimationTimeline, Scroller mSource; StyleScrollAxis mAxis; - struct CurrentTimeData { - // The position of the scroller, and this may be negative for RTL or - // sideways, e.g. the range of its value could be [0, -range]. The user - // needs to take care of that. - nscoord mPosition; - ScrollOffsets mOffsets; - }; - Maybe<CurrentTimeData> mCachedCurrentTime; + // The current state of this timeline. We set this flag during scheduling + // because we shouldn't remove this timeline from the scheduler immediately + // while scheduling, to avoid any unexpected behaviors. + TimelineState mState = TimelineState::None; +}; + +/** + * A wrapper around a hashset of ScrollTimeline objects to handle the scheduling + * of scroll driven animations. This is used for all kinds of progress + * timelines, i.e. anonymous/named scroll timelines and anonymous/named view + * timelines. And this object is owned by the scroll source (See + * ElementAnimationData and ScrollContainerFrame for the usage). + */ +class ProgressTimelineScheduler { + public: + ProgressTimelineScheduler() { MOZ_COUNT_CTOR(ProgressTimelineScheduler); } + ~ProgressTimelineScheduler() { MOZ_COUNT_DTOR(ProgressTimelineScheduler); } + + static ProgressTimelineScheduler* Get( + const Element* aElement, const PseudoStyleRequest& aPseudoRequest); + static ProgressTimelineScheduler& Ensure( + Element* aElement, const PseudoStyleRequest& aPseudoRequest); + static void Destroy(const Element* aElement, + const PseudoStyleRequest& aPseudoRequest); + + void AddTimeline(ScrollTimeline* aScrollTimeline) { + MOZ_ASSERT(!mIsInScheduling, "Do not mutate the hashset during scheduling"); + (void)mTimelines.put(aScrollTimeline); + } + void RemoveTimeline(ScrollTimeline* aScrollTimeline) { + MOZ_ASSERT(!mIsInScheduling, "Do not mutate the hashset during scheduling"); + mTimelines.remove(aScrollTimeline); + } + + bool IsEmpty() const { return mTimelines.empty(); } + bool IsInScheduling() const { return mIsInScheduling; } + + // Note: Use static function because this may destroy the scheduler if all + // timelines are removed. + static void ScheduleAnimations(const Element* aElement, + const PseudoStyleRequest& aRequest); + + private: + // We let Animations own its scroll timeline or view timeline if it is + // anonymous. For named progress timelines, they are created and destroyed by + // TimelineCollection. + HashSet<ScrollTimeline*> mTimelines; + // Used to avoid mutating |mTimelines| while scheduling. + bool mIsInScheduling = false; }; } // namespace dom diff --git a/dom/animation/ViewTimeline.cpp b/dom/animation/ViewTimeline.cpp @@ -72,13 +72,9 @@ Maybe<ScrollTimeline::ScrollOffsets> ViewTimeline::ComputeOffsets( MOZ_ASSERT(mSubject); MOZ_ASSERT(aScrollContainerFrame); - // Note: We may fail to get the pseudo element (or its primary frame) if it is - // not generated yet or just get destroyed, while we are sampling this view - // timeline. const Element* subjectElement = mSubject->GetPseudoElement(PseudoStyleRequest(mSubjectPseudoType)); - const nsIFrame* subject = - subjectElement ? subjectElement->GetPrimaryFrame() : nullptr; + const nsIFrame* subject = subjectElement->GetPrimaryFrame(); if (!subject) { // No principal box of the subject, so we cannot compute the offset. This // may happen when we clear all animation collections during unbinding from diff --git a/dom/animation/moz.build b/dom/animation/moz.build @@ -14,7 +14,6 @@ EXPORTS.mozilla.dom += [ "Animation.h", "AnimationEffect.h", "AnimationTimeline.h", - "AnimationTimelinesController.h", "CSSAnimation.h", "CSSPseudoElement.h", "CSSTransition.h", @@ -51,7 +50,6 @@ UNIFIED_SOURCES += [ "AnimationEventDispatcher.cpp", "AnimationPerformanceWarning.cpp", "AnimationTimeline.cpp", - "AnimationTimelinesController.cpp", "AnimationUtils.cpp", "CSSAnimation.cpp", "CSSPseudoElement.cpp", diff --git a/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html @@ -24,7 +24,7 @@ <script> "use strict"; -promise_test(async (t) => { +test(function(t) { const div = addDiv(t, { style: "width: 10px; height: 100px; " + "animation: animWidth 100s, animTop 200s; " + @@ -35,10 +35,6 @@ promise_test(async (t) => { const scroller = document.scrollingElement; const maxScroll = scroller.scrollHeight - scroller.clientHeight; scroller.scrollTop = maxScroll; - // Wait for one frame to make sure getComputedStyle() works because we sample - // the scroll timeline during the HTML event loop. - await waitForNextFrame(); - assert_equals(getComputedStyle(div).width, "200px", "The scroll animation is there"); @@ -52,7 +48,7 @@ promise_test(async (t) => { 'scroll animation should not return scroll timeline'); }, 'Element.getAnimation() should include scroll animations'); -promise_test(async (t) => { +test(function(t) { const div = addDiv(t, { style: "width: 10px; height: 100px; " + "animation: animWidth 100s, animTop 100s; " + @@ -63,10 +59,6 @@ promise_test(async (t) => { const scroller = document.scrollingElement; const maxScroll = scroller.scrollHeight - scroller.clientHeight; scroller.scrollTop = maxScroll; - // Wait for one frame to make sure getComputedStyle() works because we sample - // the scroll timeline during the HTML event loop. - await waitForNextFrame(); - assert_equals(getComputedStyle(div).width, "200px", "The scroll animation is there"); diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -2566,6 +2566,8 @@ Document::~Document() { mAnimationController->Disconnect(); } + MOZ_ASSERT(mTimelines.isEmpty()); + mParentDocument = nullptr; // Kill the subdocument map, doing this will release its strong @@ -20827,7 +20829,9 @@ RadioGroupContainer& Document::OwnedRadioGroupContainer() { } void Document::UpdateHiddenByContentVisibilityForAnimations() { - mTimelinesController.UpdateHiddenByContentVisibility(); + for (AnimationTimeline* timeline : Timelines()) { + timeline->UpdateHiddenByContentVisibility(); + } } void Document::SetAllowDeclarativeShadowRoots( diff --git a/dom/base/Document.h b/dom/base/Document.h @@ -47,7 +47,6 @@ #include "mozilla/WeakPtr.h" #include "mozilla/css/StylePreloadKind.h" #include "mozilla/dom/AnimationFrameProvider.h" -#include "mozilla/dom/AnimationTimelinesController.h" #include "mozilla/dom/DocumentOrShadowRoot.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/EventTarget.h" @@ -3224,12 +3223,7 @@ class Document : public nsINode, using DocumentOrShadowRoot::GetElementsByTagNameNS; DocumentTimeline* Timeline(); - const AnimationTimelinesController& TimelinesController() const { - return mTimelinesController; - } - AnimationTimelinesController& TimelinesController() { - return mTimelinesController; - } + LinkedList<DocumentTimeline>& Timelines() { return mTimelines; } void UpdateHiddenByContentVisibilityForAnimations(); SVGSVGElement* GetSVGRootElement() const; @@ -5560,11 +5554,8 @@ class Document : public nsINode, // A set of responsive images keyed by address pointer. nsTHashSet<HTMLImageElement*> mResponsiveContent; - // The default document timeline associated to this document. RefPtr<DocumentTimeline> mDocumentTimeline; - // The timeline controller which holds the timelines attached to this - // document. - AnimationTimelinesController mTimelinesController; + LinkedList<DocumentTimeline> mTimelines; RefPtr<dom::ScriptLoader> mScriptLoader; diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp @@ -92,7 +92,6 @@ #include "mozilla/ViewportUtils.h" #include "mozilla/css/ImageLoader.h" #include "mozilla/dom/AncestorIterator.h" -#include "mozilla/dom/AnimationTimelinesController.h" #include "mozilla/dom/BrowserBridgeChild.h" #include "mozilla/dom/BrowserChild.h" #include "mozilla/dom/BrowsingContext.h" @@ -890,7 +889,9 @@ void PresShell::Init(nsPresContext* aPresContext) { } #endif - mDocument->TimelinesController().UpdateLastRefreshDriverTime(); + for (DocumentTimeline* timelines : mDocument->Timelines()) { + timelines->UpdateLastRefreshDriverTime(); + } // Get our activeness from the docShell. ActivenessMaybeChanged(); diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp @@ -3060,6 +3060,7 @@ void nsPresContext::DoUpdateHiddenByContentVisibilityForAnimations() { MOZ_ASSERT(NeedsToUpdateHiddenByContentVisibilityForAnimations()); mNeedsToUpdateHiddenByContentVisibilityForAnimations = false; mDocument->UpdateHiddenByContentVisibilityForAnimations(); + TimelineManager()->UpdateHiddenByContentVisibilityForAnimations(); } #ifdef DEBUG diff --git a/layout/base/nsRefreshDriver.cpp b/layout/base/nsRefreshDriver.cpp @@ -60,7 +60,6 @@ #include "mozilla/TaskController.h" #include "mozilla/VsyncDispatcher.h" #include "mozilla/VsyncTaskManager.h" -#include "mozilla/dom/AnimationTimelinesController.h" #include "mozilla/dom/BrowserChild.h" #include "mozilla/dom/CallbackDebuggerNotification.h" #include "mozilla/dom/ContentChild.h" @@ -2040,7 +2039,11 @@ void nsRefreshDriver::UpdateRemoteFrameEffects() { } static void UpdateAndReduceAnimations(Document& aDocument) { - aDocument.TimelinesController().WillRefresh(); + for (DocumentTimeline* tl : + ToTArray<AutoTArray<RefPtr<DocumentTimeline>, 32>>( + aDocument.Timelines())) { + tl->WillRefresh(); + } if (nsPresContext* pc = aDocument.GetPresContext()) { if (pc->EffectCompositor()->NeedsReducing()) { diff --git a/layout/generic/ScrollContainerFrame.cpp b/layout/generic/ScrollContainerFrame.cpp @@ -7851,10 +7851,22 @@ void ScrollContainerFrame::AppendScrollUpdate( } void ScrollContainerFrame::ScheduleScrollAnimations() { - auto* rd = PresContext()->RefreshDriver(); - MOZ_ASSERT(rd); - // This schedules UpdateAnimationsAndSendEvents in the HTML loop. - rd->EnsureAnimationUpdate(); + nsIContent* content = GetContent(); + MOZ_ASSERT(content && content->IsElement(), + "The ScrollContainerFrame should have the element."); + + const Element* elementOrPseudo = content->AsElement(); + PseudoStyleType pseudo = elementOrPseudo->GetPseudoElementType(); + if (pseudo != PseudoStyleType::NotPseudo && + !AnimationUtils::IsSupportedPseudoForAnimations(pseudo)) { + // This is not an animatable pseudo element, and so we don't generate + // scroll-timeline for it. + return; + } + + const auto [element, request] = + AnimationUtils::GetElementPseudoPair(elementOrPseudo); + ProgressTimelineScheduler::ScheduleAnimations(element, request); } nsSize ScrollContainerFrame::GetSizeForWindowInnerSize() const { diff --git a/layout/printing/nsPrintJob.cpp b/layout/printing/nsPrintJob.cpp @@ -15,7 +15,6 @@ #include "mozilla/PresShellInlines.h" #include "mozilla/StaticPrefs_print.h" #include "mozilla/Try.h" -#include "mozilla/dom/AnimationTimelinesController.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/CustomEvent.h" @@ -1375,7 +1374,9 @@ nsresult nsPrintJob::ReflowPrintObject(const UniquePtr<nsPrintObject>& aPO) { } } // Make sure animations are active. - aPO->mDocument->TimelinesController().TriggerAllPendingAnimationsNow(); + for (DocumentTimeline* tl : aPO->mDocument->Timelines()) { + tl->TriggerAllPendingAnimationsNow(); + } // Process the reflow event Initialize posted presShell->FlushPendingNotifications(FlushType::Layout); aPO->mDocument->UpdateRemoteFrameEffects(); diff --git a/layout/style/TimelineManager.cpp b/layout/style/TimelineManager.cpp @@ -170,4 +170,20 @@ void TimelineManager::DoUpdateTimelines( // siblings when mutating {scroll|view}-timeline-name. } +void TimelineManager::UpdateHiddenByContentVisibilityForAnimations() { + for (auto* scrollTimelineCollection : mScrollTimelineCollections) { + for (ScrollTimeline* timeline : + scrollTimelineCollection->Timelines().Values()) { + timeline->UpdateHiddenByContentVisibility(); + } + } + + for (auto* viewTimelineCollection : mViewTimelineCollections) { + for (ViewTimeline* timeline : + viewTimelineCollection->Timelines().Values()) { + timeline->UpdateHiddenByContentVisibility(); + } + } +} + } // namespace mozilla diff --git a/layout/style/TimelineManager.h b/layout/style/TimelineManager.h @@ -52,6 +52,8 @@ class TimelineManager { const ComputedStyle* aComputedStyle, ProgressTimelineType aType); + void UpdateHiddenByContentVisibilityForAnimations(); + private: template <typename StyleType, typename TimelineType> void DoUpdateTimelines(nsPresContext* aPresContext, dom::Element* aElement, diff --git a/testing/web-platform/meta/css/css-contain/content-visibility/content-visibility-animation-with-scroll-timeline-in-auto-subtree.html.ini b/testing/web-platform/meta/css/css-contain/content-visibility/content-visibility-animation-with-scroll-timeline-in-auto-subtree.html.ini @@ -1,4 +1,2 @@ [content-visibility-animation-with-scroll-timeline-in-auto-subtree.html] prefs: [layout.css.scroll-driven-animations.enabled:true] - [Animation with scroll-timeline should be affected c-v] - expected: FAIL diff --git a/testing/web-platform/meta/css/css-contain/content-visibility/content-visibility-animation-with-scroll-timeline-in-hidden-subtree.html.ini b/testing/web-platform/meta/css/css-contain/content-visibility/content-visibility-animation-with-scroll-timeline-in-hidden-subtree.html.ini @@ -1,4 +1,2 @@ [content-visibility-animation-with-scroll-timeline-in-hidden-subtree.html] prefs: [layout.css.scroll-driven-animations.enabled:true] - [Animation with scroll-timeline should be affected c-v] - expected: FAIL diff --git a/testing/web-platform/meta/scroll-animations/css/scroll-timeline-sampling.html.ini b/testing/web-platform/meta/scroll-animations/css/scroll-timeline-sampling.html.ini @@ -0,0 +1,6 @@ +[scroll-timeline-sampling.html] + expected: + if (os == "android") and fission: [OK, TIMEOUT] + [Scroll position is sampled once per frame] + expected: FAIL + bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1817051