blob: f31f76e4cf4d5cf402251aa14f837c1e223120ba [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/scheduler/browser_ui_thread_scheduler.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/allocator/partition_alloc_support.h"
#include "base/allocator/scheduler_loop_quarantine_config.h"
#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "content/common/features.h"
#include "content/public/browser/browser_thread.h"
#include "partition_alloc/bucket_lookup.h"
#include "partition_alloc/extended_api.h"
#include "partition_alloc/partition_alloc_for_testing.h"
#include "partition_alloc/scheduler_loop_quarantine_runtime_stats.h"
#include "partition_alloc/scheduler_loop_quarantine_support.h"
#include "testing/gtest/include/gtest/gtest.h"
#if !defined(MEMORY_TOOL_REPLACES_ALLOCATOR) && \
PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) && \
PA_CONFIG(THREAD_CACHE_SUPPORTED) && !PA_BUILDFLAG(IS_WIN)
namespace partition_alloc::internal {
// We need to redeclare the storage to use this variable.
const PA_COMPONENT_EXPORT(PARTITION_ALLOC) size_t
SchedulerLoopQuarantineRuntimeStats::kMaxTimesToTrack;
} // namespace partition_alloc::internal
#endif
namespace content {
namespace {
using ::base::Bucket;
using ::base::BucketsAre;
using ::testing::Eq;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
using RuntimeStats =
::partition_alloc::internal::SchedulerLoopQuarantineRuntimeStats;
using StrictMockTask =
testing::StrictMock<base::MockCallback<base::RepeatingCallback<void()>>>;
base::OnceClosure RunOnDestruction(base::OnceClosure task) {
return base::BindOnce(
[](std::unique_ptr<base::ScopedClosureRunner>) {},
std::make_unique<base::ScopedClosureRunner>(std::move(task)));
}
base::OnceClosure PostOnDestruction(
scoped_refptr<base::SingleThreadTaskRunner> task_queue,
base::OnceClosure task) {
return RunOnDestruction(base::BindOnce(
[](base::OnceClosure task,
scoped_refptr<base::SingleThreadTaskRunner> task_queue) {
task_queue->PostTask(FROM_HERE, std::move(task));
},
std::move(task), task_queue));
}
TEST(BrowserUIThreadSchedulerTest, DestructorPostChainDuringShutdown) {
auto browser_ui_thread_scheduler_ =
std::make_unique<BrowserUIThreadScheduler>();
browser_ui_thread_scheduler_->GetHandle()->OnStartupComplete();
auto task_queue =
browser_ui_thread_scheduler_->GetHandle()->GetBrowserTaskRunner(
BrowserUIThreadScheduler::QueueType::kDefault);
bool run = false;
task_queue->PostTask(
FROM_HERE,
PostOnDestruction(
task_queue,
PostOnDestruction(task_queue,
RunOnDestruction(base::BindOnce(
[](bool* run) { *run = true; }, &run)))));
EXPECT_FALSE(run);
browser_ui_thread_scheduler_.reset();
EXPECT_TRUE(run);
}
class BrowserUIThreadSchedulerLoopQuarantineTest : public testing::Test {
private:
base::test::ScopedFeatureList scoped_feature_list_{
features::
kPartitionAllocSchedulerLoopQuarantineTaskObserverForBrowserUIThread};
};
TEST_F(BrowserUIThreadSchedulerLoopQuarantineTest,
TestAllocationGetPurgedFromQuarantineAfterTaskCompletion) {
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
GTEST_SKIP() << "This test does not work with memory tools.";
#elif !PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) || \
!PA_CONFIG(THREAD_CACHE_SUPPORTED)
GTEST_SKIP() << "This test requires PA-E and ThreadCache.";
#else
std::unique_ptr<BrowserUIThreadScheduler> browser_ui_thread_scheduler =
BrowserUIThreadScheduler::CreateForTesting();
browser_ui_thread_scheduler->GetHandle()->OnStartupComplete();
// Pick up a queue.
auto task_queue =
browser_ui_thread_scheduler->GetHandle()->GetBrowserTaskRunner(
BrowserUIThreadScheduler::QueueType::kUserBlocking);
// Prepare PA root for testing.
partition_alloc::PartitionOptions opts;
opts.scheduler_loop_quarantine_thread_local_config.enable_quarantine = true;
opts.scheduler_loop_quarantine_thread_local_config.branch_capacity_in_bytes =
4096;
partition_alloc::PartitionAllocatorForTesting allocator(opts);
partition_alloc::PartitionRoot& root = *allocator.root();
// Disables ThreadCache for the default allocator and enables it for the
// testing allocator.
partition_alloc::internal::ThreadCacheProcessScopeForTesting tcache_scope(
&root);
partition_alloc::internal::
ScopedSchedulerLoopQuarantineBranchAccessorForTesting branch_accessor(
&root);
void* ptr = root.Alloc(16);
base::test::TestFuture<void> future;
task_queue->PostTaskAndReply(
FROM_HERE, base::BindLambdaForTesting([&]() {
EXPECT_FALSE(branch_accessor.IsQuarantined(ptr));
root.Free<
partition_alloc::internal::FreeFlags::kSchedulerLoopQuarantine>(
ptr);
EXPECT_TRUE(branch_accessor.IsQuarantined(ptr));
}),
future.GetCallback());
EXPECT_TRUE(future.Wait());
// `ptr` must not be in the quarantine as the scheduler finished its loop.
EXPECT_FALSE(branch_accessor.IsQuarantined(ptr));
#endif
}
TEST_F(BrowserUIThreadSchedulerLoopQuarantineTest,
SchedulerLoopQuarantineRuntimeStatsAreReported) {
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
GTEST_SKIP() << "This test does not work with memory tools.";
#elif !PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) || \
!PA_CONFIG(THREAD_CACHE_SUPPORTED)
GTEST_SKIP() << "This test requires PA-E and ThreadCache.";
#else
std::unique_ptr<BrowserUIThreadScheduler> browser_ui_thread_scheduler =
BrowserUIThreadScheduler::CreateForTesting();
browser_ui_thread_scheduler->GetHandle()->OnStartupComplete();
// Pick up a queue.
auto task_queue =
browser_ui_thread_scheduler->GetHandle()->GetBrowserTaskRunner(
BrowserUIThreadScheduler::QueueType::kUserBlocking);
// Prepare PA root for testing.
partition_alloc::PartitionOptions opts;
opts.scheduler_loop_quarantine_thread_local_config.enable_quarantine = true;
opts.scheduler_loop_quarantine_thread_local_config.enable_zapping = true;
opts.scheduler_loop_quarantine_thread_local_config.branch_capacity_in_bytes =
4096;
opts.scheduler_loop_quarantine_thread_local_config
.enable_quarantine_runtime_stats = true;
partition_alloc::PartitionAllocatorForTesting allocator(opts);
partition_alloc::PartitionRoot& root = *allocator.root();
// Disables ThreadCache for the default allocator and enables it for the
// testing allocator.
partition_alloc::internal::ThreadCacheProcessScopeForTesting tcache_scope(
&root);
partition_alloc::internal::
ScopedSchedulerLoopQuarantineBranchAccessorForTesting branch_accessor(
&root);
std::vector<void*> ptrs;
task_queue->PostTask(
FROM_HERE, base::BindLambdaForTesting([&]() {
for (int i = 0; i < RuntimeStats::kMaxTimesToTrack + 2; ++i) {
ptrs.push_back(root.Alloc(70));
EXPECT_FALSE(branch_accessor.IsQuarantined(ptrs.back()));
}
}));
base::test::TestFuture<void> free_future;
absl::flat_hash_map<std::string, std::vector<base::Bucket>> samples;
task_queue->PostTaskAndReply(
FROM_HERE, base::BindLambdaForTesting([&]() {
// This resets any allocations that were seen, so that the histograms
// will be just our allocations.
root.ReconfigureSchedulerLoopQuarantineForCurrentThread(
opts.scheduler_loop_quarantine_thread_local_config);
for (void* ptr : ptrs) {
EXPECT_FALSE(branch_accessor.IsQuarantined(ptr));
root.Free<
partition_alloc::internal::FreeFlags::kSchedulerLoopQuarantine>(
ptr);
EXPECT_TRUE(branch_accessor.IsQuarantined(ptr));
}
// Report the first round of metrics, by wrapping it around we avoid
// other allocations and UMA metrics. The interval should be long
// enough that we don't see any other metrics reported. Choose the
// timeout duration.
base::HistogramTester current_samples;
base::allocator::StartSchedulerLoopPeriodicStatsReporting(
base::Seconds(300), &root);
samples = current_samples.GetAllSamplesForPrefix(
"Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine.Stats.");
}),
free_future.GetCallback());
EXPECT_TRUE(free_future.Wait());
size_t bucket_index =
partition_alloc::BucketIndexLookup::GetIndexForDenserBuckets((70));
auto& stats = root.GetStatsForSchedulerLoopQuarantineForCurrentThread();
for (size_t i = 0; i < stats.purge_buckets().size(); ++i) {
if (i != bucket_index + 1) {
EXPECT_FALSE(stats.purge_buckets()[i].valid())
<< i << " vs " << bucket_index + 1;
} else {
EXPECT_TRUE(stats.purge_buckets()[i].valid())
<< i << " vs " << bucket_index + 1;
}
}
// The task should be reposted.
EXPECT_THAT(
samples,
::testing::IsSupersetOf(
{Pair("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine.Stats."
"CycleCount.0B.512B",
BucketsAre(Bucket(1, /*count=*/1))),
Pair("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine.Stats."
"PausedCount.0B.512B",
BucketsAre(Bucket(0, /*count=*/1)))}));
std::vector<std::string> actual_metrics = {};
for (auto& [name, buckets] : samples) {
actual_metrics.push_back(name);
size_t num_samples = 0;
for (auto& bucket : buckets) {
num_samples += bucket.count;
}
if (name.find("CycleCount") != std::string::npos ||
name.find("PausedCount") != std::string::npos) {
EXPECT_EQ(num_samples, 1u);
} else {
// We don't know the actual performance numbers just ensure we got the
// right number of reports.
EXPECT_EQ(num_samples, RuntimeStats::kMaxTimesToTrack);
}
}
EXPECT_THAT(actual_metrics,
UnorderedElementsAre(
Eq("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine."
"Stats.TotalTime.0B.512B"),
Eq("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine."
"Stats.PurgeTime.0B.512B"),
Eq("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine."
"Stats.ZapTime.0B.512B"),
Eq("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine."
"Stats.CycleCount.0B.512B"),
Eq("Memory.Browser.PartitionAlloc.SchedulerLoopQuarantine."
"Stats.PausedCount.0B.512B")));
#endif
}
} // namespace
} // namespace content