blob: c237e92a469462cb95303b4ebc91748464a0b95a [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 "third_party/blink/renderer/platform/scheduler/main_thread/main_thread_metrics_helper.h"
#include <memory>
#include "base/macros.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/page/launching_process_state.h"
#include "third_party/blink/renderer/platform/scheduler/base/test/sequence_manager_for_test.h"
#include "third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.h"
#include "third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/test/fake_frame_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/test/fake_page_scheduler.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
using base::sequence_manager::TaskQueue;
namespace blink {
namespace scheduler {
namespace {
class MainThreadSchedulerImplForTest : public MainThreadSchedulerImpl {
public:
MainThreadSchedulerImplForTest(
std::unique_ptr<base::sequence_manager::SequenceManager> sequence_manager,
base::Optional<base::Time> initial_virtual_time)
: MainThreadSchedulerImpl(std::move(sequence_manager),
initial_virtual_time){};
using MainThreadSchedulerImpl::SetCurrentUseCaseForTest;
};
} // namespace
using QueueType = MainThreadTaskQueue::QueueType;
using base::Bucket;
using testing::ElementsAre;
using testing::UnorderedElementsAre;
class MainThreadMetricsHelperTest : public testing::Test {
public:
MainThreadMetricsHelperTest()
: task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME,
base::test::ScopedTaskEnvironment::ExecutionMode::QUEUED) {
// Null clock might trigger some assertions.
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(1));
}
~MainThreadMetricsHelperTest() override = default;
void SetUp() override {
histogram_tester_.reset(new base::HistogramTester());
scheduler_ = std::make_unique<MainThreadSchedulerImplForTest>(
base::sequence_manager::SequenceManagerForTest::Create(
nullptr, task_environment_.GetMainThreadTaskRunner(),
task_environment_.GetMockTickClock()),
base::nullopt);
metrics_helper_ = &scheduler_->main_thread_only().metrics_helper;
}
void TearDown() override {
scheduler_->Shutdown();
scheduler_.reset();
}
base::TimeTicks Now() {
return task_environment_.GetMockTickClock()->NowTicks();
}
void FastForwardTo(base::TimeTicks time) {
CHECK_LE(Now(), time);
task_environment_.FastForwardBy(time - Now());
}
void RunTask(MainThreadTaskQueue::QueueType queue_type,
base::TimeTicks start,
base::TimeDelta duration) {
DCHECK_LE(Now(), start);
FastForwardTo(start + duration);
scoped_refptr<MainThreadTaskQueueForTest> queue;
if (queue_type != MainThreadTaskQueue::QueueType::kDetached) {
queue = scoped_refptr<MainThreadTaskQueueForTest>(
new MainThreadTaskQueueForTest(queue_type));
}
// Pass an empty task for recording.
TaskQueue::PostedTask posted_task(base::OnceClosure(), FROM_HERE);
TaskQueue::Task task(std::move(posted_task), base::TimeTicks());
metrics_helper_->RecordTaskMetrics(queue.get(), task, start,
start + duration, base::nullopt);
}
void RunTask(FrameSchedulerImpl* scheduler,
base::TimeTicks start,
base::TimeDelta duration) {
DCHECK_LE(Now(), start);
FastForwardTo(start + duration);
scoped_refptr<MainThreadTaskQueueForTest> queue(
new MainThreadTaskQueueForTest(QueueType::kDefault));
queue->SetFrameSchedulerForTest(scheduler);
// Pass an empty task for recording.
TaskQueue::PostedTask posted_task(base::OnceClosure(), FROM_HERE);
TaskQueue::Task task(std::move(posted_task), base::TimeTicks());
metrics_helper_->RecordTaskMetrics(queue.get(), task, start,
start + duration, base::nullopt);
}
void RunTask(UseCase use_case,
base::TimeTicks start,
base::TimeDelta duration) {
DCHECK_LE(Now(), start);
FastForwardTo(start + duration);
scoped_refptr<MainThreadTaskQueueForTest> queue(
new MainThreadTaskQueueForTest(QueueType::kDefault));
scheduler_->SetCurrentUseCaseForTest(use_case);
// Pass an empty task for recording.
TaskQueue::PostedTask posted_task(base::OnceClosure(), FROM_HERE);
TaskQueue::Task task(std::move(posted_task), base::TimeTicks());
metrics_helper_->RecordTaskMetrics(queue.get(), task, start,
start + duration, base::nullopt);
}
base::TimeTicks Milliseconds(int milliseconds) {
return base::TimeTicks() + base::TimeDelta::FromMilliseconds(milliseconds);
}
base::TimeTicks Seconds(int seconds) {
return base::TimeTicks() + base::TimeDelta::FromSeconds(seconds);
}
void ForceUpdatePolicy() { scheduler_->ForceUpdatePolicy(); }
std::unique_ptr<FakeFrameScheduler> CreateFakeFrameSchedulerWithType(
FrameStatus frame_status) {
FakeFrameScheduler::Builder builder;
switch (frame_status) {
case FrameStatus::kNone:
case FrameStatus::kDetached:
return nullptr;
case FrameStatus::kMainFrameVisible:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame)
.SetIsPageVisible(true)
.SetIsFrameVisible(true);
break;
case FrameStatus::kMainFrameVisibleService:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame)
.SetPageScheduler(playing_view_.get())
.SetIsFrameVisible(true);
break;
case FrameStatus::kMainFrameHidden:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame)
.SetIsPageVisible(true);
break;
case FrameStatus::kMainFrameHiddenService:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame)
.SetPageScheduler(playing_view_.get());
break;
case FrameStatus::kMainFrameBackground:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame);
break;
case FrameStatus::kMainFrameBackgroundExemptSelf:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame)
.SetIsExemptFromThrottling(true);
break;
case FrameStatus::kMainFrameBackgroundExemptOther:
builder.SetFrameType(FrameScheduler::FrameType::kMainFrame)
.SetPageScheduler(throtting_exempt_view_.get());
break;
case FrameStatus::kSameOriginVisible:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsPageVisible(true)
.SetIsFrameVisible(true);
break;
case FrameStatus::kSameOriginVisibleService:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetPageScheduler(playing_view_.get())
.SetIsFrameVisible(true);
break;
case FrameStatus::kSameOriginHidden:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsPageVisible(true);
break;
case FrameStatus::kSameOriginHiddenService:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetPageScheduler(playing_view_.get());
break;
case FrameStatus::kSameOriginBackground:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe);
break;
case FrameStatus::kSameOriginBackgroundExemptSelf:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsExemptFromThrottling(true);
break;
case FrameStatus::kSameOriginBackgroundExemptOther:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetPageScheduler(throtting_exempt_view_.get());
break;
case FrameStatus::kCrossOriginVisible:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true)
.SetIsPageVisible(true)
.SetIsFrameVisible(true);
break;
case FrameStatus::kCrossOriginVisibleService:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true)
.SetPageScheduler(playing_view_.get())
.SetIsFrameVisible(true);
break;
case FrameStatus::kCrossOriginHidden:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true)
.SetIsPageVisible(true);
break;
case FrameStatus::kCrossOriginHiddenService:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true)
.SetPageScheduler(playing_view_.get());
break;
case FrameStatus::kCrossOriginBackground:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true);
break;
case FrameStatus::kCrossOriginBackgroundExemptSelf:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true)
.SetIsExemptFromThrottling(true);
break;
case FrameStatus::kCrossOriginBackgroundExemptOther:
builder.SetFrameType(FrameScheduler::FrameType::kSubframe)
.SetIsCrossOrigin(true)
.SetPageScheduler(throtting_exempt_view_.get());
break;
case FrameStatus::kCount:
NOTREACHED();
return nullptr;
}
return builder.Build();
}
base::test::ScopedTaskEnvironment task_environment_;
std::unique_ptr<MainThreadSchedulerImplForTest> scheduler_;
MainThreadMetricsHelper* metrics_helper_; // NOT OWNED
std::unique_ptr<base::HistogramTester> histogram_tester_;
std::unique_ptr<FakePageScheduler> playing_view_ =
FakePageScheduler::Builder().SetIsAudioPlaying(true).Build();
std::unique_ptr<FakePageScheduler> throtting_exempt_view_ =
FakePageScheduler::Builder().SetIsThrottlingExempt(true).Build();
DISALLOW_COPY_AND_ASSIGN(MainThreadMetricsHelperTest);
};
TEST_F(MainThreadMetricsHelperTest, Metrics_PerQueueType) {
// QueueType::kDefault is checking sub-millisecond task aggregation,
// FRAME_* tasks are checking normal task aggregation and other
// queue types have a single task.
// Make sure that it starts in a foregrounded state.
if (kLaunchingProcessIsBackgrounded)
scheduler_->SetRendererBackgrounded(false);
RunTask(QueueType::kDefault, Seconds(1),
base::TimeDelta::FromMilliseconds(700));
RunTask(QueueType::kDefault, Seconds(2),
base::TimeDelta::FromMilliseconds(700));
RunTask(QueueType::kDefault, Seconds(3),
base::TimeDelta::FromMilliseconds(700));
RunTask(QueueType::kControl, Seconds(4), base::TimeDelta::FromSeconds(3));
RunTask(QueueType::kFrameLoading, Seconds(8),
base::TimeDelta::FromSeconds(6));
RunTask(QueueType::kFramePausable, Seconds(16),
base::TimeDelta::FromSeconds(2));
RunTask(QueueType::kCompositor, Seconds(19), base::TimeDelta::FromSeconds(2));
RunTask(QueueType::kTest, Seconds(22), base::TimeDelta::FromSeconds(4));
scheduler_->SetRendererBackgrounded(true);
// Wait for internally triggered tasks to run.
constexpr int kCoolingOfTimeSeconds = 10;
RunTask(QueueType::kControl, Seconds(26 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(2));
RunTask(QueueType::kFrameThrottleable, Seconds(28 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(8));
RunTask(QueueType::kUnthrottled, Seconds(38 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(5));
RunTask(QueueType::kFrameLoading, Seconds(45 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(10));
RunTask(QueueType::kFrameThrottleable, Seconds(60 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(5));
RunTask(QueueType::kCompositor, Seconds(70 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(20));
RunTask(QueueType::kIdle, Seconds(90 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(5));
RunTask(QueueType::kFrameLoadingControl, Seconds(100 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(5));
RunTask(QueueType::kControl, Seconds(106 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(6));
RunTask(QueueType::kFrameThrottleable, Seconds(114 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(6));
RunTask(QueueType::kFramePausable, Seconds(120 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(17));
RunTask(QueueType::kIdle, Seconds(140 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(15));
RunTask(QueueType::kDetached, Seconds(156 + kCoolingOfTimeSeconds),
base::TimeDelta::FromSeconds(2));
std::vector<base::Bucket> expected_samples = {
{static_cast<int>(QueueType::kControl), 11},
{static_cast<int>(QueueType::kDefault), 2},
{static_cast<int>(QueueType::kUnthrottled), 5},
{static_cast<int>(QueueType::kFrameLoading), 16},
{static_cast<int>(QueueType::kCompositor), 22},
{static_cast<int>(QueueType::kIdle), 20},
{static_cast<int>(QueueType::kTest), 4},
{static_cast<int>(QueueType::kFrameLoadingControl), 5},
{static_cast<int>(QueueType::kFrameThrottleable), 19},
{static_cast<int>(QueueType::kFramePausable), 19},
{static_cast<int>(QueueType::kDetached), 2},
};
EXPECT_THAT(histogram_tester_->GetAllSamples(
"RendererScheduler.TaskDurationPerQueueType3"),
testing::ContainerEq(expected_samples));
EXPECT_THAT(histogram_tester_->GetAllSamples(
"RendererScheduler.TaskDurationPerQueueType3.Foreground"),
UnorderedElementsAre(
Bucket(static_cast<int>(QueueType::kControl), 3),
Bucket(static_cast<int>(QueueType::kDefault), 2),
Bucket(static_cast<int>(QueueType::kFrameLoading), 6),
Bucket(static_cast<int>(QueueType::kCompositor), 2),
Bucket(static_cast<int>(QueueType::kTest), 4),
Bucket(static_cast<int>(QueueType::kFramePausable), 2)));
EXPECT_THAT(histogram_tester_->GetAllSamples(
"RendererScheduler.TaskDurationPerQueueType3.Background"),
UnorderedElementsAre(
Bucket(static_cast<int>(QueueType::kControl), 8),
Bucket(static_cast<int>(QueueType::kUnthrottled), 5),
Bucket(static_cast<int>(QueueType::kFrameLoading), 10),
Bucket(static_cast<int>(QueueType::kFrameThrottleable), 19),
Bucket(static_cast<int>(QueueType::kFramePausable), 17),
Bucket(static_cast<int>(QueueType::kCompositor), 20),
Bucket(static_cast<int>(QueueType::kIdle), 20),
Bucket(static_cast<int>(QueueType::kFrameLoadingControl), 5),
Bucket(static_cast<int>(QueueType::kDetached), 2)));
}
TEST_F(MainThreadMetricsHelperTest, Metrics_PerUseCase) {
RunTask(UseCase::kNone, Milliseconds(500),
base::TimeDelta::FromMilliseconds(400));
RunTask(UseCase::kTouchstart, Seconds(1), base::TimeDelta::FromSeconds(2));
RunTask(UseCase::kTouchstart, Seconds(3),
base::TimeDelta::FromMilliseconds(300));
RunTask(UseCase::kTouchstart, Seconds(4),
base::TimeDelta::FromMilliseconds(300));
RunTask(UseCase::kCompositorGesture, Seconds(5),
base::TimeDelta::FromSeconds(5));
RunTask(UseCase::kCompositorGesture, Seconds(10),
base::TimeDelta::FromSeconds(3));
RunTask(UseCase::kMainThreadCustomInputHandling, Seconds(14),
base::TimeDelta::FromSeconds(2));
RunTask(UseCase::kSynchronizedGesture, Seconds(17),
base::TimeDelta::FromSeconds(2));
RunTask(UseCase::kMainThreadCustomInputHandling, Seconds(19),
base::TimeDelta::FromSeconds(5));
RunTask(UseCase::kLoading, Seconds(25), base::TimeDelta::FromSeconds(6));
RunTask(UseCase::kMainThreadGesture, Seconds(31),
base::TimeDelta::FromSeconds(6));
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskDurationPerUseCase2"),
UnorderedElementsAre(
Bucket(static_cast<int>(UseCase::kTouchstart), 3),
Bucket(static_cast<int>(UseCase::kCompositorGesture), 8),
Bucket(static_cast<int>(UseCase::kMainThreadCustomInputHandling), 7),
Bucket(static_cast<int>(UseCase::kSynchronizedGesture), 2),
Bucket(static_cast<int>(UseCase::kLoading), 6),
Bucket(static_cast<int>(UseCase::kMainThreadGesture), 6)));
}
TEST_F(MainThreadMetricsHelperTest, GetFrameStatusTest) {
DCHECK_EQ(GetFrameStatus(nullptr), FrameStatus::kNone);
FrameStatus frame_statuses_tested[] = {
FrameStatus::kMainFrameVisible,
FrameStatus::kSameOriginHidden,
FrameStatus::kCrossOriginHidden,
FrameStatus::kSameOriginBackground,
FrameStatus::kMainFrameBackgroundExemptSelf,
FrameStatus::kSameOriginVisibleService,
FrameStatus::kCrossOriginHiddenService,
FrameStatus::kMainFrameBackgroundExemptOther};
for (FrameStatus frame_status : frame_statuses_tested) {
std::unique_ptr<FakeFrameScheduler> frame =
CreateFakeFrameSchedulerWithType(frame_status);
EXPECT_EQ(GetFrameStatus(frame.get()), frame_status);
}
}
TEST_F(MainThreadMetricsHelperTest, TaskCountPerFrameStatus) {
int task_count = 0;
struct CountPerFrameStatus {
FrameStatus frame_status;
int count;
};
CountPerFrameStatus test_data[] = {
{FrameStatus::kNone, 4},
{FrameStatus::kMainFrameVisible, 8},
{FrameStatus::kMainFrameBackgroundExemptSelf, 5},
{FrameStatus::kCrossOriginHidden, 3},
{FrameStatus::kCrossOriginHiddenService, 7},
{FrameStatus::kCrossOriginVisible, 1},
{FrameStatus::kMainFrameBackgroundExemptOther, 2},
{FrameStatus::kSameOriginVisible, 10},
{FrameStatus::kSameOriginBackground, 9},
{FrameStatus::kSameOriginVisibleService, 6}};
for (const auto& data : test_data) {
std::unique_ptr<FakeFrameScheduler> frame =
CreateFakeFrameSchedulerWithType(data.frame_status);
for (int i = 0; i < data.count; ++i) {
RunTask(frame.get(), Milliseconds(++task_count),
base::TimeDelta::FromMicroseconds(100));
}
}
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kNone), 4),
Bucket(static_cast<int>(FrameStatus::kMainFrameVisible), 8),
Bucket(static_cast<int>(FrameStatus::kMainFrameBackgroundExemptSelf),
5),
Bucket(static_cast<int>(FrameStatus::kMainFrameBackgroundExemptOther),
2),
Bucket(static_cast<int>(FrameStatus::kSameOriginVisible), 10),
Bucket(static_cast<int>(FrameStatus::kSameOriginVisibleService), 6),
Bucket(static_cast<int>(FrameStatus::kSameOriginBackground), 9),
Bucket(static_cast<int>(FrameStatus::kCrossOriginVisible), 1),
Bucket(static_cast<int>(FrameStatus::kCrossOriginHidden), 3),
Bucket(static_cast<int>(FrameStatus::kCrossOriginHiddenService), 7)));
}
TEST_F(MainThreadMetricsHelperTest, TaskCountPerFrameTypeLongerThan) {
int total_duration = 0;
struct TasksPerFrameStatus {
FrameStatus frame_status;
std::vector<int> durations;
};
TasksPerFrameStatus test_data[] = {
{FrameStatus::kSameOriginHidden,
{2, 15, 16, 20, 25, 30, 49, 50, 73, 99, 100, 110, 140, 150, 800, 1000,
1200}},
{FrameStatus::kCrossOriginVisibleService,
{5, 10, 18, 19, 20, 55, 75, 220}},
{FrameStatus::kMainFrameBackground,
{21, 31, 41, 51, 61, 71, 81, 91, 101, 1001}},
};
for (const auto& data : test_data) {
std::unique_ptr<FakeFrameScheduler> frame =
CreateFakeFrameSchedulerWithType(data.frame_status);
for (size_t i = 0; i < data.durations.size(); ++i) {
RunTask(frame.get(), Milliseconds(++total_duration),
base::TimeDelta::FromMilliseconds(data.durations[i]));
total_duration += data.durations[i];
}
}
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kMainFrameBackground), 10),
Bucket(static_cast<int>(FrameStatus::kSameOriginHidden), 17),
Bucket(static_cast<int>(FrameStatus::kCrossOriginVisibleService),
8)));
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType."
"LongerThan16ms"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kMainFrameBackground), 10),
Bucket(static_cast<int>(FrameStatus::kSameOriginHidden), 15),
Bucket(static_cast<int>(FrameStatus::kCrossOriginVisibleService),
6)));
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType."
"LongerThan50ms"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kMainFrameBackground), 7),
Bucket(static_cast<int>(FrameStatus::kSameOriginHidden), 10),
Bucket(static_cast<int>(FrameStatus::kCrossOriginVisibleService),
3)));
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType."
"LongerThan100ms"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kMainFrameBackground), 2),
Bucket(static_cast<int>(FrameStatus::kSameOriginHidden), 7),
Bucket(static_cast<int>(FrameStatus::kCrossOriginVisibleService),
1)));
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType."
"LongerThan150ms"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kMainFrameBackground), 1),
Bucket(static_cast<int>(FrameStatus::kSameOriginHidden), 4),
Bucket(static_cast<int>(FrameStatus::kCrossOriginVisibleService),
1)));
EXPECT_THAT(
histogram_tester_->GetAllSamples(
"RendererScheduler.TaskCountPerFrameType.LongerThan1s"),
UnorderedElementsAre(
Bucket(static_cast<int>(FrameStatus::kMainFrameBackground), 1),
Bucket(static_cast<int>(FrameStatus::kSameOriginHidden), 2)));
}
// TODO(crbug.com/754656): Add tests for NthMinute and
// AfterNthMinute histograms.
// TODO(crbug.com/754656): Add tests for
// TaskDuration.Hidden/Visible histograms.
// TODO(crbug.com/754656): Add tests for non-TaskDuration
// histograms.
} // namespace scheduler
} // namespace blink