blob: 75b5d8490e1876c163ca487f2233c9a89aca1e3c [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/sessions/session_restore_stats_collector.h"
#include <stddef.h>
#include <memory>
#include <utility>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using TabLoaderStats = SessionRestoreStatsCollector::TabLoaderStats;
using StatsReportingDelegate =
SessionRestoreStatsCollector::StatsReportingDelegate;
// A mock StatsReportingDelegate. This is used by the unittests to validate the
// reporting and lifetime behaviour of the SessionRestoreStatsCollector under
// test.
class MockStatsReportingDelegate : public StatsReportingDelegate {
public:
MockStatsReportingDelegate()
: report_tab_loader_stats_call_count_(0u),
report_stats_collector_death_call_count_(0u) {}
MockStatsReportingDelegate(const MockStatsReportingDelegate&) = delete;
MockStatsReportingDelegate& operator=(const MockStatsReportingDelegate&) =
delete;
~MockStatsReportingDelegate() override = default;
void ReportTabLoaderStats(const TabLoaderStats& stats) override {
report_tab_loader_stats_call_count_++;
tab_loader_stats_ = stats;
}
// This is not part of the StatsReportingDelegate, but an added function that
// is invoked by the PassthroughStatsReportingDelegate when it dies. This
// allows the tests to be notified the moment the underlying stats collector
// terminates itself.
void ReportStatsCollectorDeath() {
report_stats_collector_death_call_count_++;
}
void ExpectReportTabLoaderStatsCalled(
size_t tab_count,
int foreground_tab_first_paint_ms,
SessionRestoreStatsCollector::SessionRestorePaintFinishReasonUma
finish_reason) {
EXPECT_EQ(tab_count, tab_loader_stats_.tab_count);
EXPECT_EQ(base::Milliseconds(foreground_tab_first_paint_ms),
tab_loader_stats_.foreground_tab_first_paint);
EXPECT_EQ(tab_loader_stats_.tab_first_paint_reason, finish_reason);
}
void EnsureNoUnexpectedCalls() {
EXPECT_EQ(0u, report_tab_loader_stats_call_count_);
EXPECT_EQ(0u, report_stats_collector_death_call_count_);
report_tab_loader_stats_call_count_ = 0u;
report_stats_collector_death_call_count_ = 0u;
tab_loader_stats_ = TabLoaderStats();
}
void ExpectEnded() {
EXPECT_EQ(1u, report_tab_loader_stats_call_count_);
EXPECT_EQ(1u, report_stats_collector_death_call_count_);
}
private:
size_t report_tab_loader_stats_call_count_;
size_t report_stats_collector_death_call_count_;
TabLoaderStats tab_loader_stats_;
};
// A pass-through stats reporting delegate. This is used to decouple the
// lifetime of the mock reporting delegate from the SessionRestoreStatsCollector
// under test. The SessionRestoreStatsCollector has ownership of this delegate,
// which will notify the mock delegate upon its death.
class PassthroughStatsReportingDelegate : public StatsReportingDelegate {
public:
PassthroughStatsReportingDelegate() : reporting_delegate_(nullptr) {}
PassthroughStatsReportingDelegate(const PassthroughStatsReportingDelegate&) =
delete;
PassthroughStatsReportingDelegate& operator=(
const PassthroughStatsReportingDelegate&) = delete;
~PassthroughStatsReportingDelegate() override {
reporting_delegate_->ReportStatsCollectorDeath();
}
void set_reporting_delegate(MockStatsReportingDelegate* reporting_delegate) {
reporting_delegate_ = reporting_delegate;
}
void ReportTabLoaderStats(const TabLoaderStats& tab_loader_stats) override {
reporting_delegate_->ReportTabLoaderStats(tab_loader_stats);
}
private:
raw_ptr<MockStatsReportingDelegate> reporting_delegate_;
};
} // namespace
class SessionRestoreStatsCollectorTest : public testing::Test {
public:
using RestoredTab = SessionRestoreDelegate::RestoredTab;
SessionRestoreStatsCollectorTest()
: task_environment_{base::test::TaskEnvironment::TimeSource::MOCK_TIME} {}
SessionRestoreStatsCollectorTest(const SessionRestoreStatsCollectorTest&) =
delete;
SessionRestoreStatsCollectorTest& operator=(
const SessionRestoreStatsCollectorTest&) = delete;
void SetUp() override {
test_web_contents_factory_ =
std::make_unique<content::TestWebContentsFactory>();
// Ownership of the reporting delegate is passed to the
// SessionRestoreStatsCollector, but a raw pointer is kept to it so it can
// be queried by the test.
passthrough_reporting_delegate_ = new PassthroughStatsReportingDelegate();
// Force creation of the stats collector.
stats_collector_ = SessionRestoreStatsCollector::GetOrCreateInstance(
base::TimeTicks::Now(), std::unique_ptr<StatsReportingDelegate>(
passthrough_reporting_delegate_));
}
void TearDown() override {
passthrough_reporting_delegate_ = nullptr;
// Clean up any tabs that were generated by the unittest.
restored_tabs_.clear();
test_web_contents_factory_.reset();
}
// Advances the test clock by 1ms.
void Tick() { task_environment_.FastForwardBy(base::Milliseconds(1)); }
void Show(size_t tab_index) {
restored_tabs_[tab_index].contents()->WasShown();
GenerateRenderWidgetVisiblityChanged(tab_index, /*visible=*/true);
}
void Hide(size_t tab_index) {
restored_tabs_[tab_index].contents()->WasHidden();
GenerateRenderWidgetVisiblityChanged(tab_index, /*visible=*/false);
}
// Creates a restored tab backed by dummy WebContents/NavigationController/
// RenderWidgetHost/RenderWidgetHostView.
void CreateRestoredTab(bool is_active) {
content::WebContents* contents =
test_web_contents_factory_->CreateWebContents(&testing_profile_);
std::vector<std::unique_ptr<content::NavigationEntry>> entries;
entries.push_back(content::NavigationEntry::Create());
contents->GetController().Restore(0, content::RestoreType::kRestored,
&entries);
// Create a last active time in the past.
content::WebContentsTester::For(contents)->SetLastActiveTime(
base::TimeTicks::Now() - base::Minutes(1));
restored_tabs_.push_back(
RestoredTab(contents, is_active, false, false, std::nullopt));
if (is_active)
Show(restored_tabs_.size() - 1);
}
// Generates a render widget host destroyed notification for the given tab.
void GenerateRenderWidgetHostDestroyed(size_t tab_index) {
content::WebContents* contents = restored_tabs_[tab_index].contents();
stats_collector_->RenderWidgetHostDestroyed(
contents->GetRenderWidgetHostView()->GetRenderWidgetHost());
}
// Generates a paint notification for the given tab.
void GenerateRenderWidgetHostDidUpdateBackingStore(size_t tab_index) {
content::WebContents* contents = restored_tabs_[tab_index].contents();
content::RenderWidgetHost* host =
contents->GetRenderWidgetHostView()->GetRenderWidgetHost();
stats_collector_->RenderWidgetHostDidUpdateVisualProperties(host);
}
void GenerateRenderWidgetVisiblityChanged(size_t tab_index, bool visible) {
content::WebContents* contents = restored_tabs_[tab_index].contents();
content::RenderWidgetHost* host =
contents->GetRenderWidgetHostView()->GetRenderWidgetHost();
stats_collector_->RenderWidgetHostVisibilityChanged(host, visible);
}
content::BrowserTaskEnvironment task_environment_;
// |task_environment_| needs to still be alive when
// |testing_profile_| is destroyed.
TestingProfile testing_profile_;
// Inputs to the stats collector. Reset prior to each test.
std::vector<RestoredTab> restored_tabs_;
// A new web contents factory is generated per test. This automatically cleans
// up any tabs created by previous tests.
std::unique_ptr<content::TestWebContentsFactory> test_web_contents_factory_;
// These are recreated for each test. The reporting delegate allows the test
// to observe the behaviour of the SessionRestoreStatsCollector under test.
raw_ptr<PassthroughStatsReportingDelegate, DanglingUntriaged>
passthrough_reporting_delegate_;
raw_ptr<SessionRestoreStatsCollector, DanglingUntriaged> stats_collector_;
};
TEST_F(SessionRestoreStatsCollectorTest, MultipleTabsLoadSerially) {
MockStatsReportingDelegate mock_reporting_delegate;
passthrough_reporting_delegate_->set_reporting_delegate(
&mock_reporting_delegate);
CreateRestoredTab(true);
CreateRestoredTab(false);
CreateRestoredTab(false);
stats_collector_->TrackTabs(restored_tabs_);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
// Foreground tab paints then finishes loading.
Tick(); // 1ms.
GenerateRenderWidgetHostDidUpdateBackingStore(0);
mock_reporting_delegate.ExpectReportTabLoaderStatsCalled(
3, 1, SessionRestoreStatsCollector::PAINT_FINISHED_UMA_DONE);
mock_reporting_delegate.ExpectEnded();
}
// Test that if a single restored foreground tab is occluded at any point
// before first paint, the time to first paint is not recorded, and the
// FinishReason is PAINT_FINISHED_UMA_NO_COMPLETELY_VISIBLE_TABS.
TEST_F(SessionRestoreStatsCollectorTest, ForegroundTabOccluded) {
MockStatsReportingDelegate mock_reporting_delegate;
passthrough_reporting_delegate_->set_reporting_delegate(
&mock_reporting_delegate);
CreateRestoredTab(/*is_active=*/true);
stats_collector_->TrackTabs(restored_tabs_);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
Tick(); // 1ms.
// Mark the tab as hidden/occluded.
GenerateRenderWidgetVisiblityChanged(0, /*visible=*/false);
Tick(); // 2ms.
// Mark the tab as visible.
GenerateRenderWidgetVisiblityChanged(0, /*visible=*/true);
// Mark the tab as having painted. This should cause the
// stats to be emitted, but with an empty foreground paint value because it
// was not visible at one point during restore.
GenerateRenderWidgetHostDidUpdateBackingStore(0);
// Destroy the tab.
GenerateRenderWidgetHostDestroyed(0);
mock_reporting_delegate.ExpectReportTabLoaderStatsCalled(
1, 0,
SessionRestoreStatsCollector::
PAINT_FINISHED_UMA_NO_COMPLETELY_VISIBLE_TABS);
mock_reporting_delegate.ExpectEnded();
}
// Test that if the first tab painted was hidden/occluded before first paint,
// the first paint time of the second tab to be painted is recorded.
TEST_F(SessionRestoreStatsCollectorTest, FirstOfTwoTabsOccluded) {
MockStatsReportingDelegate mock_reporting_delegate;
passthrough_reporting_delegate_->set_reporting_delegate(
&mock_reporting_delegate);
CreateRestoredTab(/*is_active=*/true);
CreateRestoredTab(/*is_active=*/true);
stats_collector_->TrackTabs(restored_tabs_);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
Tick(); // 1ms.
// Mark the tab as hidden/occluded.
GenerateRenderWidgetVisiblityChanged(0, /*visible=*/false);
Tick(); // 2ms.
// Mark the tab as visible.
GenerateRenderWidgetVisiblityChanged(0, /*visible=*/true);
// Mark the tab as having painted. Because it was occluded,
// no stat should be emitted.
GenerateRenderWidgetHostDidUpdateBackingStore(0);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
Tick(); // 3ms.
mock_reporting_delegate.EnsureNoUnexpectedCalls();
Tick(); // 4ms.
Tick(); // 5ms.
// Mark the second tab as painted.
GenerateRenderWidgetHostDidUpdateBackingStore(1);
mock_reporting_delegate.ExpectReportTabLoaderStatsCalled(
2, 5, SessionRestoreStatsCollector::PAINT_FINISHED_UMA_DONE);
mock_reporting_delegate.ExpectEnded();
}
TEST_F(SessionRestoreStatsCollectorTest, LoadingTabDestroyedBeforePaint) {
MockStatsReportingDelegate mock_reporting_delegate;
passthrough_reporting_delegate_->set_reporting_delegate(
&mock_reporting_delegate);
CreateRestoredTab(true);
stats_collector_->TrackTabs(restored_tabs_);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
// Destroy the tab. Expect all timings to be zero.
GenerateRenderWidgetHostDestroyed(0);
mock_reporting_delegate.ExpectReportTabLoaderStatsCalled(
1, 0, SessionRestoreStatsCollector::PAINT_FINISHED_UMA_NO_PAINT);
mock_reporting_delegate.ExpectEnded();
}
TEST_F(SessionRestoreStatsCollectorTest, FocusSwitchNoForegroundPaintOrLoad) {
MockStatsReportingDelegate mock_reporting_delegate;
passthrough_reporting_delegate_->set_reporting_delegate(
&mock_reporting_delegate);
CreateRestoredTab(true);
stats_collector_->TrackTabs(restored_tabs_);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
// Create another tab and make it the foreground tab. This tab is not actually
// being tracked by the SessionRestoreStatsCollector, but its paint events
// will be observed.
CreateRestoredTab(false);
Hide(0);
Show(1);
// Load and paint the restored tab (now the background tab). Don't expect
// any calls to the mock as a visible tab paint has not yet been observed.
Tick(); // 1ms.
GenerateRenderWidgetHostDidUpdateBackingStore(0);
mock_reporting_delegate.EnsureNoUnexpectedCalls();
Tick(); // 2ms.
mock_reporting_delegate.EnsureNoUnexpectedCalls();
// Mark the new foreground tab as having painted. This should cause the
// stats to be emitted, but with empty foreground paint and load values.
Tick(); // 3ms.
GenerateRenderWidgetHostDidUpdateBackingStore(1);
mock_reporting_delegate.ExpectReportTabLoaderStatsCalled(
1, 0,
SessionRestoreStatsCollector::
PAINT_FINISHED_NON_RESTORED_TAB_PAINTED_FIRST);
mock_reporting_delegate.ExpectEnded();
}