blob: 881eff480904ced13e839179a7622fd59dddab4f [file] [log] [blame]
// Copyright 2020 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/metrics/dropped_frame_counter.h"
#include <vector>
#include "base/synchronization/lock.h"
#include "base/synchronization/waitable_event.h"
#include "base/time/time.h"
#include "cc/animation/animation_host.h"
#include "cc/test/fake_content_layer_client.h"
#include "cc/test/fake_picture_layer.h"
#include "cc/test/layer_tree_test.h"
namespace cc {
namespace {
class DroppedFrameCounterTestBase : public LayerTreeTest {
public:
DroppedFrameCounterTestBase() = default;
~DroppedFrameCounterTestBase() override = default;
virtual void SetUpTestConfigAndExpectations() = 0;
void InitializeSettings(LayerTreeSettings* settings) override {
settings->commit_to_active_tree = false;
}
void SetupTree() override {
LayerTreeTest::SetupTree();
Layer* root_layer = layer_tree_host()->root_layer();
scroll_layer_ = FakePictureLayer::Create(&client_);
// Set up the layer so it always has something to paint.
scroll_layer_->set_always_update_resources(true);
scroll_layer_->SetBounds({3, 3});
client_.set_bounds({3, 3});
root_layer->AddChild(scroll_layer_);
}
void RunTest(CompositorMode mode) override {
SetUpTestConfigAndExpectations();
LayerTreeTest::RunTest(mode);
}
void BeginTest() override {
ASSERT_GT(config_.animation_frames, 0u);
// Start with requesting main-frames.
PostSetNeedsCommitToMainThread();
}
void AfterTest() override {
EXPECT_GE(total_frames_, config_.animation_frames);
// It is possible to drop even more frame than what the test expects (e.g.
// in slower machines, slower builds such as asan/tsan builds, etc.), since
// the test does not strictly control both threads and deadlines. Therefore,
// it is not possible to check for strict equality here.
EXPECT_LE(expect_.min_dropped_main, dropped_main_);
EXPECT_LE(expect_.min_dropped_compositor, dropped_compositor_);
EXPECT_LE(expect_.min_dropped_smoothness, dropped_smoothness_);
}
// Compositor thread function overrides:
void WillBeginImplFrameOnThread(LayerTreeHostImpl* host_impl,
const viz::BeginFrameArgs& args) override {
if (TestEnded())
return;
// Request a re-draw, and set a non-empty damage region (otherwise the
// draw is aborted with 'no damage').
host_impl->SetNeedsRedraw();
host_impl->SetViewportDamage(gfx::Rect(0, 0, 10, 20));
if (skip_main_thread_next_frame_) {
skip_main_thread_next_frame_ = false;
} else {
// Request update from the main-thread too.
host_impl->SetNeedsCommit();
}
}
void DrawLayersOnThread(LayerTreeHostImpl* host_impl) override {
// If the main-thread is blocked, then unblock it once the compositor thread
// has already drawn a frame.
base::WaitableEvent* wait = nullptr;
{
base::AutoLock lock(wait_lock_);
wait = wait_;
}
if (wait) {
// When the main-thread blocks during a frame, skip the main-thread for
// the next frame, so that the main-thread can be in sync with the
// compositor thread again.
skip_main_thread_next_frame_ = true;
wait->Signal();
}
}
void DidReceivePresentationTimeOnThread(
LayerTreeHostImpl* host_impl,
uint32_t frame_token,
const gfx::PresentationFeedback& feedback) override {
++presented_frames_;
if (presented_frames_ < config_.animation_frames)
return;
auto* dropped_frame_counter = host_impl->dropped_frame_counter();
DCHECK(dropped_frame_counter);
total_frames_ = dropped_frame_counter->total_frames();
dropped_main_ = dropped_frame_counter->total_main_dropped();
dropped_compositor_ = dropped_frame_counter->total_compositor_dropped();
dropped_smoothness_ = dropped_frame_counter->total_smoothness_dropped();
EndTest();
}
// Main-thread function overrides:
void BeginMainFrame(const viz::BeginFrameArgs& args) override {
if (TestEnded())
return;
bool should_wait = false;
if (config_.should_drop_main_every > 0) {
should_wait =
args.frame_id.sequence_number % config_.should_drop_main_every == 0;
}
if (should_wait) {
base::WaitableEvent wait{base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED};
{
base::AutoLock lock(wait_lock_);
wait_ = &wait;
}
wait.Wait();
{
base::AutoLock lock(wait_lock_);
wait_ = nullptr;
}
}
// Make some changes so that the main-thread needs to push updates to the
// compositor thread (i.e. force a commit).
auto const bounds = scroll_layer_->bounds();
scroll_layer_->SetBounds({bounds.width(), bounds.height() + 1});
if (config_.should_register_main_thread_animation) {
animation_host()->SetAnimationCounts(1, true, true);
}
}
protected:
// The test configuration options. This is set before the test starts, and
// remains unchanged after that. So it is safe to read these fields from
// either threads.
struct TestConfig {
uint32_t should_drop_main_every = 0;
uint32_t animation_frames = 0;
bool should_register_main_thread_animation = false;
} config_;
// The test expectations. This is set before the test starts, and
// remains unchanged after that. So it is safe to read these fields from
// either threads.
struct TestExpectation {
uint32_t min_dropped_main = 0;
uint32_t min_dropped_compositor = 0;
uint32_t min_dropped_smoothness = 0;
} expect_;
private:
// Set up a dummy picture layer so that every begin-main frame requires a
// commit (without the dummy layer, the main-thread never has to paint, which
// causes an early 'no damage' abort of the main-frame.
FakeContentLayerClient client_;
scoped_refptr<FakePictureLayer> scroll_layer_;
// This field is used only on the compositor thread to track how many frames
// have been processed.
uint32_t presented_frames_ = 0;
// The |wait_| event is used when the test wants to deliberately force the
// main-thread to block while processing begin-main-frames.
base::Lock wait_lock_;
base::WaitableEvent* wait_ = nullptr;
// These fields are populated in the compositor thread when the desired number
// of frames have been processed. These fields are subsequently compared
// against the expectation after the test ends.
uint32_t total_frames_ = 0;
uint32_t dropped_main_ = 0;
uint32_t dropped_compositor_ = 0;
uint32_t dropped_smoothness_ = 0;
bool skip_main_thread_next_frame_ = false;
};
class DroppedFrameCounterNoDropTest : public DroppedFrameCounterTestBase {
public:
~DroppedFrameCounterNoDropTest() override = default;
void SetUpTestConfigAndExpectations() override {
config_.animation_frames = 28;
config_.should_register_main_thread_animation = false;
expect_.min_dropped_main = 0;
expect_.min_dropped_compositor = 0;
expect_.min_dropped_smoothness = 0;
}
};
MULTI_THREAD_TEST_F(DroppedFrameCounterNoDropTest);
class DroppedFrameCounterMainDropsNoSmoothness
: public DroppedFrameCounterTestBase {
public:
~DroppedFrameCounterMainDropsNoSmoothness() override = default;
void SetUpTestConfigAndExpectations() override {
config_.animation_frames = 28;
config_.should_drop_main_every = 5;
config_.should_register_main_thread_animation = false;
expect_.min_dropped_main = 5;
expect_.min_dropped_smoothness = 0;
}
};
// TODO(crbug.com/1115376) Disabled for flakiness.
// MULTI_THREAD_TEST_F(DroppedFrameCounterMainDropsNoSmoothness);
class DroppedFrameCounterMainDropsSmoothnessTest
: public DroppedFrameCounterTestBase {
public:
~DroppedFrameCounterMainDropsSmoothnessTest() override = default;
void SetUpTestConfigAndExpectations() override {
config_.animation_frames = 28;
config_.should_drop_main_every = 5;
config_.should_register_main_thread_animation = true;
expect_.min_dropped_main = 5;
expect_.min_dropped_smoothness = 5;
}
};
// TODO(crbug.com/1115376) Disabled for flakiness.
// MULTI_THREAD_TEST_F(DroppedFrameCounterMainDropsSmoothnessTest);
class DroppedFrameCounterTest : public testing::Test {
public:
DroppedFrameCounterTest() {
dropped_frame_counter_.set_total_counter(&total_frame_counter_);
dropped_frame_counter_.OnFcpReceived();
}
~DroppedFrameCounterTest() override = default;
// For each boolean in frame_states produces a frame
void SimulateFrameSequence(std::vector<bool> frame_states, int repeat) {
for (int i = 0; i < repeat; i++) {
for (auto is_dropped : frame_states) {
viz::BeginFrameArgs args_ = SimulateBeginFrameArgs();
dropped_frame_counter_.OnBeginFrame(args_, /*is_scroll_active=*/false);
dropped_frame_counter_.OnEndFrame(args_, is_dropped);
sequence_number_++;
frame_time_ += interval_;
}
}
}
void AdvancetimeByIntervals(int interval_count) {
frame_time_ += interval_ * interval_count;
}
double MaxPercentDroppedFrame() {
return dropped_frame_counter_.sliding_window_max_percent_dropped();
}
double PercentDroppedFrame95Percentile() {
return dropped_frame_counter_.SlidingWindow95PercentilePercentDropped();
}
double GetTotalFramesInWindow() {
return base::TimeDelta::FromSeconds(1) / interval_;
}
void SetInterval(base::TimeDelta interval) { interval_ = interval; }
base::TimeTicks GetNextFrameTime() const { return frame_time_ + interval_; }
public:
DroppedFrameCounter dropped_frame_counter_;
private:
TotalFrameCounter total_frame_counter_;
uint64_t sequence_number_ = 1;
uint64_t source_id_ = 1;
const base::TickClock* tick_clock_ = base::DefaultTickClock::GetInstance();
base::TimeTicks frame_time_ = tick_clock_->NowTicks();
base::TimeDelta interval_ =
base::TimeDelta::FromMicroseconds(16667); // 16.667 ms
viz::BeginFrameArgs SimulateBeginFrameArgs() {
viz::BeginFrameId current_id_(source_id_, sequence_number_);
viz::BeginFrameArgs args = viz::BeginFrameArgs();
args.frame_id = current_id_;
args.frame_time = frame_time_;
args.interval = interval_;
return args;
}
};
TEST_F(DroppedFrameCounterTest, SimplePattern1) {
// 2 out of every 3 frames are dropped (In total 80 frames out of 120).
SimulateFrameSequence({true, true, true, false, true, false}, 20);
// The max is the following window:
// 16 * <sequence> + {true, true, true, false
// Which means a max of 67 dropped frames.
EXPECT_EQ(std::round(MaxPercentDroppedFrame()), 67);
EXPECT_EQ(PercentDroppedFrame95Percentile(), 67); // all values are in the
// 67th bucket, and as a result 95th percentile is also 67.
}
TEST_F(DroppedFrameCounterTest, SimplePattern2) {
// 1 out of every 5 frames are dropped (In total 24 frames out of 120).
SimulateFrameSequence({false, false, false, false, true}, 24);
double expected_percent_dropped_frame = (12 / GetTotalFramesInWindow()) * 100;
EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame);
EXPECT_EQ(PercentDroppedFrame95Percentile(), 20); // all values are in the
// 20th bucket, and as a result 95th percentile is also 20.
}
TEST_F(DroppedFrameCounterTest, IncompleteWindow) {
// There are only 5 frames submitted and both Max and 95pct should report
// zero.
SimulateFrameSequence({false, false, false, false, true}, 1);
EXPECT_EQ(MaxPercentDroppedFrame(), 0.0);
EXPECT_EQ(PercentDroppedFrame95Percentile(), 0);
}
TEST_F(DroppedFrameCounterTest, MaxPercentDroppedChanges) {
// First 60 frames have 20% dropped.
SimulateFrameSequence({false, false, false, false, true}, 12);
double expected_percent_dropped_frame1 =
(12 / GetTotalFramesInWindow()) * 100;
EXPECT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame1);
EXPECT_FLOAT_EQ(PercentDroppedFrame95Percentile(), 20); // There is only one
// element in the histogram and that is 20.
// 30 new frames are added that have 18 dropped frames.
// and the 30 frame before that had 6 dropped frames.
// So in total in the window has 24 frames dropped out of 60 frames.
SimulateFrameSequence({false, false, true, true, true}, 6);
double expected_percent_dropped_frame2 =
(24 / GetTotalFramesInWindow()) * 100;
EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame2);
// 30 new frames are added that have 24 dropped frames.
// and the 30 frame before that had 18 dropped frames.
// So in total in the window has 42 frames dropped out of 60 frames.
SimulateFrameSequence({false, true, true, true, true}, 6);
double expected_percent_dropped_frame3 =
(42 / GetTotalFramesInWindow()) * 100;
EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame3);
// Percent dropped frame of window increases gradually to 70%.
// 1 value exist when we reach 60 frames and 1 value thereafter for each
// frame added. So there 61 values in histogram. Last value is 70 (2 sampels)
// and then 67 with 1 sample, which would be the 95th percentile.
EXPECT_EQ(PercentDroppedFrame95Percentile(), 67);
}
TEST_F(DroppedFrameCounterTest, MaxPercentDroppedWithIdleFrames) {
// First 20 frames have 4 frames dropped (20%).
SimulateFrameSequence({false, false, false, false, true}, 4);
// Then no frames are added for 20 intervals.
AdvancetimeByIntervals(20);
// Then 20 frames have 16 frames dropped (60%).
SimulateFrameSequence({false, false, true, true, true}, 4);
// So in total, there are 40 frames in the 1 second window with 16 dropped
// frames (40% in total).
double expected_percent_dropped_frame = (16 / GetTotalFramesInWindow()) * 100;
EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame);
}
TEST_F(DroppedFrameCounterTest, NoCrashForIntervalLargerThanWindow) {
SetInterval(base::TimeDelta::FromMilliseconds(1000));
SimulateFrameSequence({false, false}, 1);
}
TEST_F(DroppedFrameCounterTest, Percentile95WithIdleFrames) {
// Test scenario:
// . 4s of 20% dropped frames.
// . 96s of idle time.
// The 96%ile dropped-frame metric should be 0.
// Set an interval that rounds up nicely with 1 second.
constexpr auto kInterval = base::TimeDelta::FromMilliseconds(10);
constexpr size_t kFps = base::TimeDelta::FromSeconds(1) / kInterval;
static_assert(
kFps % 5 == 0,
"kFps must be a multiple of 5 because this test depends on it.");
SetInterval(kInterval);
const auto* histogram = dropped_frame_counter_.GetSlidingWindowHistogram();
// First 4 seconds with 20% dropped frames.
SimulateFrameSequence({false, false, false, false, true}, (kFps / 5) * 4);
EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);
// Then no frames are added for 97s. Note that this 1s more than 96 seconds,
// because the last second remains in the sliding window.
AdvancetimeByIntervals(kFps * 97);
// A single frame to flush the pipeline.
SimulateFrameSequence({false}, 1);
EXPECT_EQ(histogram->total_count(), 100u * kFps);
EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.96), 0u);
EXPECT_GT(histogram->GetPercentDroppedFramePercentile(0.97), 0u);
}
TEST_F(DroppedFrameCounterTest, Percentile95WithIdleFramesWhileHidden) {
// The test scenario is the same as |Percentile95WithIdleFrames| test:
// . 4s of 20% dropped frames.
// . 96s of idle time.
// However, the 96s of idle time happens *after* the page becomes invisible
// (e.g. after a tab-switch). In this case, the idle time *should not*
// contribute to the sliding window.
// Set an interval that rounds up nicely with 1 second.
constexpr auto kInterval = base::TimeDelta::FromMilliseconds(10);
constexpr size_t kFps = base::TimeDelta::FromSeconds(1) / kInterval;
static_assert(
kFps % 5 == 0,
"kFps must be a multiple of 5 because this test depends on it.");
SetInterval(kInterval);
const auto* histogram = dropped_frame_counter_.GetSlidingWindowHistogram();
// First 4 seconds with 20% dropped frames.
SimulateFrameSequence({false, false, false, false, true}, (kFps / 5) * 4);
EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);
// Hide the page (thus resetting the pending frames), then idle for 96s before
// producing a single frame.
dropped_frame_counter_.ResetPendingFrames(GetNextFrameTime());
AdvancetimeByIntervals(kFps * 97);
// A single frame to flush the pipeline.
SimulateFrameSequence({false}, 1);
EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);
}
TEST_F(DroppedFrameCounterTest, Percentile95WithIdleFramesThenHide) {
// The test scenario is the same as |Percentile95WithIdleFramesWhileHidden|:
// . 4s of 20% dropped frames.
// . 96s of idle time.
// However, the 96s of idle time happens *before* the page becomes invisible
// (e.g. after a tab-switch). In this case, the idle time *should*
// contribute to the sliding window.
// Set an interval that rounds up nicely with 1 second.
constexpr auto kInterval = base::TimeDelta::FromMilliseconds(10);
constexpr size_t kFps = base::TimeDelta::FromSeconds(1) / kInterval;
static_assert(
kFps % 5 == 0,
"kFps must be a multiple of 5 because this test depends on it.");
SetInterval(kInterval);
const auto* histogram = dropped_frame_counter_.GetSlidingWindowHistogram();
// First 4 seconds with 20% dropped frames.
SimulateFrameSequence({false, false, false, false, true}, (kFps / 5) * 4);
EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);
// Idle for 96s before hiding the page.
AdvancetimeByIntervals(kFps * 97);
dropped_frame_counter_.ResetPendingFrames(GetNextFrameTime());
AdvancetimeByIntervals(kFps * 97);
// A single frame to flush the pipeline.
SimulateFrameSequence({false}, 1);
EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.96), 0u);
EXPECT_GT(histogram->GetPercentDroppedFramePercentile(0.97), 0u);
}
} // namespace
} // namespace cc