blob: 62900fb0a7cadf13b7613921110fea36b8b4d925 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cc/trees/image_animation_controller.h"
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "cc/paint/image_animation_count.h"
namespace cc {
namespace {
// The maximum number of time an animation can be delayed before it is reset to
// start from the beginning, instead of fast-forwarding to catch up to the
// desired frame.
const base::TimeDelta kAnimationResyncCutoff = base::TimeDelta::FromMinutes(5);
} // namespace
ImageAnimationController::ImageAnimationController(
base::SingleThreadTaskRunner* task_runner,
base::RepeatingClosure invalidation_callback,
bool enable_image_animation_resync)
: notifier_(task_runner, invalidation_callback),
enable_image_animation_resync_(enable_image_animation_resync) {}
ImageAnimationController::~ImageAnimationController() = default;
void ImageAnimationController::UpdateAnimatedImage(
const DiscardableImageMap::AnimatedImageMetadata& data) {
AnimationState& animation_state = animation_state_map_[data.paint_image_id];
animation_state.UpdateMetadata(data);
}
void ImageAnimationController::RegisterAnimationDriver(
PaintImage::Id paint_image_id,
AnimationDriver* driver) {
auto it = animation_state_map_.find(paint_image_id);
DCHECK(it != animation_state_map_.end());
it->second.AddDriver(driver);
registered_animations_.insert(paint_image_id);
}
void ImageAnimationController::UnregisterAnimationDriver(
PaintImage::Id paint_image_id,
AnimationDriver* driver) {
auto it = animation_state_map_.find(paint_image_id);
DCHECK(it != animation_state_map_.end());
it->second.RemoveDriver(driver);
if (!it->second.has_drivers())
registered_animations_.erase(paint_image_id);
}
const PaintImageIdFlatSet& ImageAnimationController::AnimateForSyncTree(
base::TimeTicks now) {
TRACE_EVENT0("cc", "ImageAnimationController::AnimateImagesForSyncTree");
DCHECK(images_animated_on_sync_tree_.empty());
notifier_.WillAnimate();
base::Optional<base::TimeTicks> next_invalidation_time;
for (auto id : registered_animations_) {
auto it = animation_state_map_.find(id);
DCHECK(it != animation_state_map_.end());
AnimationState& state = it->second;
// Is anyone still interested in animating this image?
state.UpdateStateFromDrivers();
if (!state.ShouldAnimate())
continue;
// If we were able to advance this animation, invalidate it on the sync
// tree.
if (state.AdvanceFrame(now, enable_image_animation_resync_))
images_animated_on_sync_tree_.insert(id);
// Update the next invalidation time to the earliest time at which we need
// a frame to animate an image.
// Note its important to check ShouldAnimate() here again since advancing to
// a new frame on the sync tree means we might not need to animate this
// image any longer.
if (!state.ShouldAnimate())
continue;
DCHECK_GT(state.next_desired_frame_time(), now);
if (!next_invalidation_time.has_value()) {
next_invalidation_time.emplace(state.next_desired_frame_time());
} else {
next_invalidation_time = std::min(state.next_desired_frame_time(),
next_invalidation_time.value());
}
}
if (next_invalidation_time.has_value())
notifier_.Schedule(now, next_invalidation_time.value());
else
notifier_.Cancel();
return images_animated_on_sync_tree_;
}
void ImageAnimationController::UpdateStateFromDrivers(base::TimeTicks now) {
TRACE_EVENT0("cc", "UpdateStateFromAnimationDrivers");
base::Optional<base::TimeTicks> next_invalidation_time;
for (auto image_id : registered_animations_) {
auto it = animation_state_map_.find(image_id);
DCHECK(it != animation_state_map_.end());
AnimationState& state = it->second;
state.UpdateStateFromDrivers();
// Note that by not updating the |next_invalidation_time| from this image
// here, we will cancel any pending invalidation scheduled for this image
// when updating the |notifier_| at the end of this loop.
if (!state.ShouldAnimate())
continue;
if (!next_invalidation_time.has_value()) {
next_invalidation_time.emplace(state.next_desired_frame_time());
} else {
next_invalidation_time = std::min(next_invalidation_time.value(),
state.next_desired_frame_time());
}
}
if (next_invalidation_time.has_value())
notifier_.Schedule(now, next_invalidation_time.value());
else
notifier_.Cancel();
}
void ImageAnimationController::DidActivate() {
TRACE_EVENT0("cc", "ImageAnimationController::WillActivate");
for (auto id : images_animated_on_sync_tree_) {
auto it = animation_state_map_.find(id);
DCHECK(it != animation_state_map_.end());
it->second.PushPendingToActive();
}
images_animated_on_sync_tree_.clear();
// We would retain state for images with no drivers (no recordings) to allow
// resuming of animations. However, since the animation will be re-started
// from the beginning after navigation, we can avoid maintaining the state.
if (did_navigate_) {
for (auto it = animation_state_map_.begin();
it != animation_state_map_.end();) {
if (it->second.has_drivers())
it++;
else
it = animation_state_map_.erase(it);
}
did_navigate_ = false;
}
}
size_t ImageAnimationController::GetFrameIndexForImage(
PaintImage::Id paint_image_id,
WhichTree tree) const {
const auto& it = animation_state_map_.find(paint_image_id);
DCHECK(it != animation_state_map_.end());
return tree == WhichTree::PENDING_TREE ? it->second.pending_index()
: it->second.active_index();
}
const base::flat_set<ImageAnimationController::AnimationDriver*>&
ImageAnimationController::GetDriversForTesting(
PaintImage::Id paint_image_id) const {
const auto& it = animation_state_map_.find(paint_image_id);
DCHECK(it != animation_state_map_.end());
return it->second.drivers_for_testing();
}
size_t ImageAnimationController::GetLastNumOfFramesSkippedForTesting(
PaintImage::Id paint_image_id) const {
const auto& it = animation_state_map_.find(paint_image_id);
DCHECK(it != animation_state_map_.end());
return it->second.last_num_frames_skipped_for_testing();
}
ImageAnimationController::AnimationState::AnimationState() = default;
ImageAnimationController::AnimationState::AnimationState(
AnimationState&& other) = default;
ImageAnimationController::AnimationState&
ImageAnimationController::AnimationState::operator=(AnimationState&& other) =
default;
ImageAnimationController::AnimationState::~AnimationState() {
DCHECK(drivers_.empty());
}
bool ImageAnimationController::AnimationState::ShouldAnimate() const {
DCHECK(repetitions_completed_ == 0 || is_complete());
// If we have no drivers for this image, no need to animate it.
if (!should_animate_from_drivers_)
return false;
switch (requested_repetitions_) {
case kAnimationLoopOnce:
if (repetitions_completed_ >= 1)
return false;
break;
case kAnimationNone:
NOTREACHED() << "We shouldn't be tracking kAnimationNone images";
break;
case kAnimationLoopInfinite:
break;
default:
if (requested_repetitions_ <= repetitions_completed_)
return false;
}
// If we have not yet received all data for this image, we can not advance to
// an incomplete frame.
if (!frames_[NextFrameIndex()].complete)
return false;
// If we don't have all data for this image, we can not trust the frame count
// and loop back to the first frame.
size_t last_frame_index = frames_.size() - 1;
if (completion_state_ != PaintImage::CompletionState::DONE &&
pending_index_ == last_frame_index)
return false;
return true;
}
bool ImageAnimationController::AnimationState::AdvanceFrame(
base::TimeTicks now,
bool enable_image_animation_resync) {
DCHECK(ShouldAnimate());
// Start the animation from the first frame, if not yet started. We don't need
// an invalidation here if the pending and active tree are both displaying the
// first frame. Its possible for the 2 to be different if the animation was
// reset, in which case we are starting again from the first frame on the
// pending tree.
if (!animation_started_) {
DCHECK_EQ(pending_index_, 0u);
next_desired_frame_time_ = now + frames_[0].duration;
animation_started_ = true;
return pending_index_ != active_index_;
}
// Don't advance the animation if its not time yet to move to the next frame.
if (now < next_desired_frame_time_)
return false;
// If the animation is more than 5 min out of date, we don't bother catching
// up and start again from the current frame.
// Note that we don't need to invalidate this image since the active tree
// is already displaying the current frame.
if (enable_image_animation_resync &&
now - next_desired_frame_time_ > kAnimationResyncCutoff) {
DCHECK_EQ(pending_index_, active_index_);
next_desired_frame_time_ = now + frames_[pending_index_].duration;
return false;
}
// Keep catching up the animation until we reach the frame we should be
// displaying now.
// TODO(khushalsagar): Avoid unnecessary iterations for skipping whole loops
// in the animations.
size_t last_frame_index = frames_.size() - 1;
size_t num_of_frames_advanced = 0u;
while (next_desired_frame_time_ <= now && ShouldAnimate()) {
num_of_frames_advanced++;
size_t next_frame_index = NextFrameIndex();
base::TimeTicks next_desired_frame_time =
next_desired_frame_time_ + frames_[next_frame_index].duration;
// The image may load more slowly than it's supposed to animate, so that by
// the time we reach the end of the first repetition, we're well behind.
// Start the animation from the first frame in this case, so that we don't
// skip frames (or whole iterations) trying to "catch up". This is a
// tradeoff: It guarantees users see the whole animation the second time
// through and don't miss any repetitions, and is closer to what other
// browsers do; on the other hand, it makes animations "less accurate" for
// pages that try to sync an image and some other resource (e.g. audio),
// especially if users switch tabs (and thus stop drawing the animation,
// which will pause it) during that initial loop, then switch back later.
if (enable_image_animation_resync && next_frame_index == 0u &&
repetitions_completed_ == 1 && next_desired_frame_time <= now) {
pending_index_ = 0u;
next_desired_frame_time_ = now + frames_[0].duration;
repetitions_completed_ = 0;
break;
}
pending_index_ = next_frame_index;
next_desired_frame_time_ = next_desired_frame_time;
// If we are advancing to the last frame and the image has been completely
// loaded (which means that the frame count is known to be accurate), we
// just finished a loop in the animation.
if (pending_index_ == last_frame_index && is_complete())
repetitions_completed_++;
}
// We should have advanced a single frame, anything more than that are frames
// skipped trying to catch up.
DCHECK_GT(num_of_frames_advanced, 0u);
last_num_frames_skipped_ = num_of_frames_advanced - 1u;
UMA_HISTOGRAM_COUNTS_100000("AnimatedImage.NumOfFramesSkipped.Compositor",
last_num_frames_skipped_);
return pending_index_ != active_index_;
}
void ImageAnimationController::AnimationState::UpdateMetadata(
const DiscardableImageMap::AnimatedImageMetadata& data) {
paint_image_id_ = data.paint_image_id;
DCHECK_NE(data.repetition_count, kAnimationNone);
requested_repetitions_ = data.repetition_count;
DCHECK(frames_.size() <= data.frames.size())
<< "Updated recordings can only append frames";
frames_ = data.frames;
DCHECK_GT(frames_.size(), 1u);
DCHECK(completion_state_ != PaintImage::CompletionState::DONE ||
data.completion_state == PaintImage::CompletionState::DONE)
<< "If the image was marked complete before, it can not be incomplete in "
"a new update";
completion_state_ = data.completion_state;
// Update the repetition count in case we have displayed the last frame and
// we now know the frame count to be accurate.
size_t last_frame_index = frames_.size() - 1;
if (pending_index_ == last_frame_index && is_complete() &&
repetitions_completed_ == 0)
repetitions_completed_++;
// Reset the animation if the sequence id received in this recording was
// incremented.
if (reset_animation_sequence_id_ < data.reset_animation_sequence_id) {
reset_animation_sequence_id_ = data.reset_animation_sequence_id;
ResetAnimation();
}
}
void ImageAnimationController::AnimationState::PushPendingToActive() {
active_index_ = pending_index_;
}
void ImageAnimationController::AnimationState::AddDriver(
AnimationDriver* driver) {
drivers_.insert(driver);
}
void ImageAnimationController::AnimationState::RemoveDriver(
AnimationDriver* driver) {
drivers_.erase(driver);
}
void ImageAnimationController::AnimationState::UpdateStateFromDrivers() {
should_animate_from_drivers_ = false;
for (auto* driver : drivers_) {
if (driver->ShouldAnimate(paint_image_id_)) {
should_animate_from_drivers_ = true;
break;
}
}
}
void ImageAnimationController::AnimationState::ResetAnimation() {
animation_started_ = false;
next_desired_frame_time_ = base::TimeTicks();
repetitions_completed_ = 0;
pending_index_ = 0u;
// Don't reset the |active_index_|, tiles on the active tree still need it.
}
size_t ImageAnimationController::AnimationState::NextFrameIndex() const {
if (!animation_started_)
return 0u;
return (pending_index_ + 1) % frames_.size();
}
ImageAnimationController::DelayedNotifier::DelayedNotifier(
base::SingleThreadTaskRunner* task_runner,
base::RepeatingClosure closure)
: task_runner_(task_runner),
closure_(std::move(closure)),
weak_factory_(this) {
DCHECK(task_runner_->BelongsToCurrentThread());
}
ImageAnimationController::DelayedNotifier::~DelayedNotifier() {
DCHECK(task_runner_->BelongsToCurrentThread());
}
void ImageAnimationController::DelayedNotifier::Schedule(
base::TimeTicks now,
base::TimeTicks notification_time) {
// If an animation is already pending, don't schedule another invalidation.
// We will schedule the next invalidation based on the latest animation state
// during AnimateForSyncTree.
if (animation_pending_)
return;
// The requested notification time can be in the past. For instance, if an
// animation was paused because the image became invisible.
if (notification_time < now)
notification_time = now;
// If we already have a notification scheduled to run at this time, no need to
// Cancel it.
if (pending_notification_time_.has_value() &&
notification_time == pending_notification_time_.value())
return;
// Cancel the pending notification since we the requested notification time
// has changed.
Cancel();
TRACE_EVENT2("cc", "ScheduleInvalidationForImageAnimation",
"notification_time", notification_time, "now", now);
pending_notification_time_.emplace(notification_time);
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DelayedNotifier::Notify, weak_factory_.GetWeakPtr()),
notification_time - now);
}
void ImageAnimationController::DelayedNotifier::Cancel() {
pending_notification_time_.reset();
weak_factory_.InvalidateWeakPtrs();
}
void ImageAnimationController::DelayedNotifier::Notify() {
pending_notification_time_.reset();
animation_pending_ = true;
closure_.Run();
}
void ImageAnimationController::DelayedNotifier::WillAnimate() {
animation_pending_ = false;
}
} // namespace cc