gecko-dev/dom/animation/DocumentTimeline.cpp

329 строки
10 KiB
C++
Исходник Обычный вид История

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "DocumentTimeline.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/dom/DocumentTimelineBinding.h"
#include "AnimationUtils.h"
#include "nsContentUtils.h"
#include "nsDOMMutationObserver.h"
#include "nsDOMNavigationTiming.h"
#include "nsIPresShell.h"
#include "nsPresContext.h"
#include "nsRefreshDriver.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentTimeline)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentTimeline,
AnimationTimeline)
tmp->UnregisterFromRefreshDriver();
if (tmp->isInList()) {
tmp->remove();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentTimeline,
AnimationTimeline)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DocumentTimeline,
AnimationTimeline)
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentTimeline)
NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline)
NS_IMPL_ADDREF_INHERITED(DocumentTimeline, AnimationTimeline)
NS_IMPL_RELEASE_INHERITED(DocumentTimeline, AnimationTimeline)
JSObject*
DocumentTimeline::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return DocumentTimeline_Binding::Wrap(aCx, this, aGivenProto);
}
/* static */ already_AddRefed<DocumentTimeline>
DocumentTimeline::Constructor(const GlobalObject& aGlobal,
const DocumentTimelineOptions& aOptions,
ErrorResult& aRv)
{
nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
if (!doc) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
TimeDuration originTime =
TimeDuration::FromMilliseconds(aOptions.mOriginTime);
if (originTime == TimeDuration::Forever() ||
originTime == -TimeDuration::Forever()) {
aRv.ThrowTypeError<dom::MSG_TIME_VALUE_OUT_OF_RANGE>(
NS_LITERAL_STRING("Origin time"));
return nullptr;
}
RefPtr<DocumentTimeline> timeline = new DocumentTimeline(doc, originTime);
return timeline.forget();
}
Nullable<TimeDuration>
DocumentTimeline::GetCurrentTime() const
{
return ToTimelineTime(GetCurrentTimeStamp());
}
TimeStamp
DocumentTimeline::GetCurrentTimeStamp() const
{
nsRefreshDriver* refreshDriver = GetRefreshDriver();
TimeStamp refreshTime = refreshDriver
? refreshDriver->MostRecentRefresh()
: TimeStamp();
// Always return the same object to benefit from return-value optimization.
TimeStamp result = !refreshTime.IsNull()
? refreshTime
: mLastRefreshDriverTime;
nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
// If we don't have a refresh driver and we've never had one use the
// timeline's zero time.
// In addition, it's possible that our refresh driver's timestamp is behind
// from the navigation start time because the refresh driver timestamp is
// sent through an IPC call whereas the navigation time is set by calling
// TimeStamp::Now() directly. In such cases we also use the timeline's zero
// time.
if (timing &&
(result.IsNull() ||
result < timing->GetNavigationStartTimeStamp())) {
result = timing->GetNavigationStartTimeStamp();
// Also, let this time represent the current refresh time. This way
// we'll save it as the last refresh time and skip looking up
// navigation start time each time.
refreshTime = result;
}
if (!refreshTime.IsNull()) {
mLastRefreshDriverTime = refreshTime;
}
return result;
}
Nullable<TimeDuration>
DocumentTimeline::ToTimelineTime(const TimeStamp& aTimeStamp) const
{
Nullable<TimeDuration> result; // Initializes to null
if (aTimeStamp.IsNull()) {
return result;
}
nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
if (MOZ_UNLIKELY(!timing)) {
return result;
}
result.SetValue(aTimeStamp
- timing->GetNavigationStartTimeStamp()
- mOriginTime);
return result;
}
Bug 1195180 part 6 - Lazily remove animations from timelines; r=heycam Now that DocumentTimeline observes the refresh driver we can use regular ticks to remove unnecessary animations. We do this because in a subsequent patch, in order to provide deterministic enumeration order when ticking animations, we will store animations in an array. Removing an arbitrary element from an nsTArray is O(n) since we have to search for the array index first, or O(log n) if we keep the array sorted. If we destroy a subtree containing n animations, the operation effectively becomes O(n^2), or, if we keep the array sorted, O(n log n). By destroying during a tick when we are already iterating over the array, however, we will be able to do this much more efficiently. Whether an animation is newly associated with a timeline, or is disassociated from a timeline, or if it merely has its timing updated, the behavior implemented in this patch is to simply make sure we are observing the refresh driver and deal with the animation on the next tick. It might seem that we could be a lot more clever about this and, for example, if an animation reports NeedsTicks() == false, not start observing the refresh driver. There are various edge cases however that need to be taken into account. For example, if a CSS animation is finished (IsRelevant() == false so that animation will have been removed from the timeline), and paused (NeedsTicks() == false), and we seek it back to the point where it is relevant again, we actually need to observe the refresh driver so that it can dispatch an animationstart event on the next tick. A test case in a subsequent patch tests this specific situation. We could possibly add logic to detect if we need to fire events on the next tick but the complexity does not seem warranted given that even if we unnecessarily start observing the refresh driver, we will stop watching it on the next tick. This patch removes some rather lengthy comments from AnimationTiming::UpdateTiming. This is, in part, because of the behavior described above that makes these comments no longer relevant. Other parts are removed because the Web Animations specification has been updated such that a timeline becoming inactive now pauses the animation[1] so that the issue regarding detecting timelines becoming active/inactive no longer applies since animations attached to an inactive timeline remain "relevant". [1] https://w3c.github.io/web-animations/#responding-to-a-newly-inactive-timeline
2015-09-28 06:38:41 +03:00
void
DocumentTimeline::NotifyAnimationUpdated(Animation& aAnimation)
{
AnimationTimeline::NotifyAnimationUpdated(aAnimation);
if (!mIsObservingRefreshDriver) {
nsRefreshDriver* refreshDriver = GetRefreshDriver();
if (refreshDriver) {
MOZ_ASSERT(isInList(),
"We should not register with the refresh driver if we are not"
" in the document's list of timelines");
ObserveRefreshDriver(refreshDriver);
Bug 1195180 part 6 - Lazily remove animations from timelines; r=heycam Now that DocumentTimeline observes the refresh driver we can use regular ticks to remove unnecessary animations. We do this because in a subsequent patch, in order to provide deterministic enumeration order when ticking animations, we will store animations in an array. Removing an arbitrary element from an nsTArray is O(n) since we have to search for the array index first, or O(log n) if we keep the array sorted. If we destroy a subtree containing n animations, the operation effectively becomes O(n^2), or, if we keep the array sorted, O(n log n). By destroying during a tick when we are already iterating over the array, however, we will be able to do this much more efficiently. Whether an animation is newly associated with a timeline, or is disassociated from a timeline, or if it merely has its timing updated, the behavior implemented in this patch is to simply make sure we are observing the refresh driver and deal with the animation on the next tick. It might seem that we could be a lot more clever about this and, for example, if an animation reports NeedsTicks() == false, not start observing the refresh driver. There are various edge cases however that need to be taken into account. For example, if a CSS animation is finished (IsRelevant() == false so that animation will have been removed from the timeline), and paused (NeedsTicks() == false), and we seek it back to the point where it is relevant again, we actually need to observe the refresh driver so that it can dispatch an animationstart event on the next tick. A test case in a subsequent patch tests this specific situation. We could possibly add logic to detect if we need to fire events on the next tick but the complexity does not seem warranted given that even if we unnecessarily start observing the refresh driver, we will stop watching it on the next tick. This patch removes some rather lengthy comments from AnimationTiming::UpdateTiming. This is, in part, because of the behavior described above that makes these comments no longer relevant. Other parts are removed because the Web Animations specification has been updated such that a timeline becoming inactive now pauses the animation[1] so that the issue regarding detecting timelines becoming active/inactive no longer applies since animations attached to an inactive timeline remain "relevant". [1] https://w3c.github.io/web-animations/#responding-to-a-newly-inactive-timeline
2015-09-28 06:38:41 +03:00
}
}
}
void
DocumentTimeline::MostRecentRefreshTimeUpdated()
{
MOZ_ASSERT(mIsObservingRefreshDriver);
MOZ_ASSERT(GetRefreshDriver(),
"Should be able to reach refresh driver from within WillRefresh");
bool needsTicks = false;
nsTArray<Animation*> animationsToRemove(mAnimations.Count());
nsAutoAnimationMutationBatch mb(mDocument);
for (Animation* animation = mAnimationOrder.getFirst(); animation;
animation =
static_cast<LinkedListElement<Animation>*>(animation)->getNext()) {
// Skip any animations that are longer need associated with this timeline.
if (animation->GetTimeline() != this) {
// If animation has some other timeline, it better not be also in the
// animation list of this timeline object!
MOZ_ASSERT(!animation->GetTimeline());
animationsToRemove.AppendElement(animation);
Bug 1195180 part 6 - Lazily remove animations from timelines; r=heycam Now that DocumentTimeline observes the refresh driver we can use regular ticks to remove unnecessary animations. We do this because in a subsequent patch, in order to provide deterministic enumeration order when ticking animations, we will store animations in an array. Removing an arbitrary element from an nsTArray is O(n) since we have to search for the array index first, or O(log n) if we keep the array sorted. If we destroy a subtree containing n animations, the operation effectively becomes O(n^2), or, if we keep the array sorted, O(n log n). By destroying during a tick when we are already iterating over the array, however, we will be able to do this much more efficiently. Whether an animation is newly associated with a timeline, or is disassociated from a timeline, or if it merely has its timing updated, the behavior implemented in this patch is to simply make sure we are observing the refresh driver and deal with the animation on the next tick. It might seem that we could be a lot more clever about this and, for example, if an animation reports NeedsTicks() == false, not start observing the refresh driver. There are various edge cases however that need to be taken into account. For example, if a CSS animation is finished (IsRelevant() == false so that animation will have been removed from the timeline), and paused (NeedsTicks() == false), and we seek it back to the point where it is relevant again, we actually need to observe the refresh driver so that it can dispatch an animationstart event on the next tick. A test case in a subsequent patch tests this specific situation. We could possibly add logic to detect if we need to fire events on the next tick but the complexity does not seem warranted given that even if we unnecessarily start observing the refresh driver, we will stop watching it on the next tick. This patch removes some rather lengthy comments from AnimationTiming::UpdateTiming. This is, in part, because of the behavior described above that makes these comments no longer relevant. Other parts are removed because the Web Animations specification has been updated such that a timeline becoming inactive now pauses the animation[1] so that the issue regarding detecting timelines becoming active/inactive no longer applies since animations attached to an inactive timeline remain "relevant". [1] https://w3c.github.io/web-animations/#responding-to-a-newly-inactive-timeline
2015-09-28 06:38:41 +03:00
continue;
}
needsTicks |= animation->NeedsTicks();
// Even if |animation| doesn't need future ticks, we should still
// Tick it this time around since it might just need a one-off tick in
// order to dispatch events.
animation->Tick();
if (!animation->IsRelevant() && !animation->NeedsTicks()) {
animationsToRemove.AppendElement(animation);
}
}
for (Animation* animation : animationsToRemove) {
RemoveAnimation(animation);
}
if (!needsTicks) {
// We already assert that GetRefreshDriver() is non-null at the beginning
// of this function but we check it again here to be sure that ticking
// animations does not have any side effects that cause us to lose the
// connection with the refresh driver, such as triggering the destruction
// of mDocument's PresShell.
MOZ_ASSERT(GetRefreshDriver(),
"Refresh driver should still be valid at end of WillRefresh");
UnregisterFromRefreshDriver();
}
}
void
DocumentTimeline::WillRefresh(mozilla::TimeStamp aTime)
{
// https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events,
// step2.
// Note that this should be done before nsAutoAnimationMutationBatch which is
// inside MostRecentRefreshTimeUpdated(). If PerformMicroTaskCheckpoint was
// called before nsAutoAnimationMutationBatch is destroyed, some mutation
// records might not be delivered in this checkpoint.
nsAutoMicroTask mt;
MostRecentRefreshTimeUpdated();
}
void
DocumentTimeline::NotifyTimerAdjusted(TimeStamp aTime)
{
MostRecentRefreshTimeUpdated();
}
void
DocumentTimeline::ObserveRefreshDriver(nsRefreshDriver* aDriver)
{
MOZ_ASSERT(!mIsObservingRefreshDriver);
// Set the mIsObservingRefreshDriver flag before calling AddRefreshObserver
// since it might end up calling NotifyTimerAdjusted which calls
// MostRecentRefreshTimeUpdated which has an assertion for
// mIsObserveingRefreshDriver check.
mIsObservingRefreshDriver = true;
aDriver->AddRefreshObserver(this, FlushType::Style);
aDriver->AddTimerAdjustmentObserver(this);
}
void
DocumentTimeline::NotifyRefreshDriverCreated(nsRefreshDriver* aDriver)
{
MOZ_ASSERT(!mIsObservingRefreshDriver,
"Timeline should not be observing the refresh driver before"
" it is created");
if (!mAnimationOrder.isEmpty()) {
MOZ_ASSERT(isInList(),
"We should not register with the refresh driver if we are not"
" in the document's list of timelines");
ObserveRefreshDriver(aDriver);
// Although we have started observing the refresh driver, it's possible we
// could perform a paint before the first refresh driver tick happens. To
// ensure we're in a consistent state in that case we run the first tick
// manually.
MostRecentRefreshTimeUpdated();
}
}
void
DocumentTimeline::DisconnectRefreshDriver(nsRefreshDriver* aDriver)
{
MOZ_ASSERT(mIsObservingRefreshDriver);
aDriver->RemoveRefreshObserver(this, FlushType::Style);
aDriver->RemoveTimerAdjustmentObserver(this);
mIsObservingRefreshDriver = false;
}
void
DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver)
{
if (!mIsObservingRefreshDriver) {
return;
}
DisconnectRefreshDriver(aDriver);
}
void
DocumentTimeline::RemoveAnimation(Animation* aAnimation)
{
AnimationTimeline::RemoveAnimation(aAnimation);
if (mIsObservingRefreshDriver && mAnimations.IsEmpty()) {
UnregisterFromRefreshDriver();
}
}
TimeStamp
DocumentTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const
{
TimeStamp result;
nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming();
if (MOZ_UNLIKELY(!timing)) {
return result;
}
result =
timing->GetNavigationStartTimeStamp() + (aTimeDuration + mOriginTime);
return result;
}
nsRefreshDriver*
DocumentTimeline::GetRefreshDriver() const
{
nsPresContext* presContext = mDocument->GetPresContext();
if (MOZ_UNLIKELY(!presContext)) {
return nullptr;
}
return presContext->RefreshDriver();
}
void
DocumentTimeline::UnregisterFromRefreshDriver()
{
if (!mIsObservingRefreshDriver) {
return;
}
nsRefreshDriver* refreshDriver = GetRefreshDriver();
if (!refreshDriver) {
return;
}
DisconnectRefreshDriver(refreshDriver);
}
} // namespace dom
} // namespace mozilla