blob: 21ff858de9588f300cc5d43034536399d17ba2e6 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/heap_profiling/in_process/heap_profiler_controller.h"
#include <atomic>
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/metrics/field_trial_params.h"
#include "base/sampling_heap_profiler/poisson_allocation_sampler.h"
#include "base/sampling_heap_profiler/sampling_heap_profiler.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/heap_profiling/in_process/heap_profiler_parameters.h"
#include "components/metrics/call_stack_profile_builder.h"
#include "components/metrics/call_stack_profile_params.h"
#include "components/metrics/public/mojom/call_stack_profile_collector.mojom.h"
#include "components/version_info/channel.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/execution_context.pb.h"
#include "third_party/metrics_proto/sampled_profile.pb.h"
namespace heap_profiling {
namespace {
using ProcessType = metrics::CallStackProfileParams::Process;
constexpr size_t kSamplingRate = 1024;
constexpr size_t kAllocationSize = 42 * kSamplingRate;
using ProfileCollectorCallback =
base::RepeatingCallback<void(base::TimeTicks, metrics::SampledProfile)>;
// A fake CallStackProfileCollector that deserializes profiles it receives from
// a fake child process, and passes them to the same callback that receives
// profiles from the fake browser process.
class TestCallStackProfileCollector final
: public metrics::mojom::CallStackProfileCollector {
public:
explicit TestCallStackProfileCollector(
ProfileCollectorCallback collector_callback)
: collector_callback_(std::move(collector_callback)) {}
TestCallStackProfileCollector(const TestCallStackProfileCollector&) = delete;
TestCallStackProfileCollector& operator=(
const TestCallStackProfileCollector&) = delete;
~TestCallStackProfileCollector() final = default;
// metrics::mojom::CallStackProfileCollector
void Collect(base::TimeTicks start_timestamp,
metrics::mojom::ProfileType profile_type,
metrics::mojom::SampledProfilePtr profile) final {
metrics::SampledProfile sampled_profile;
ASSERT_TRUE(profile);
ASSERT_TRUE(sampled_profile.ParseFromString(profile->contents));
EXPECT_EQ(profile_type == metrics::mojom::ProfileType::kHeap,
sampled_profile.trigger_event() ==
metrics::SampledProfile::PERIODIC_HEAP_COLLECTION);
collector_callback_.Run(start_timestamp, std::move(sampled_profile));
}
private:
ProfileCollectorCallback collector_callback_;
};
} // namespace
// HeapProfilerControllerTest can't be in an anonymous namespace because it is a
// friend of SamplingHeapProfiler.
class HeapProfilerControllerTest : public ::testing::Test {
public:
// Sets `sample_received_` to true if any sample is received. This will work
// even without stack unwinding since it doesn't check the contents of the
// sample. This must be public so that BindRepeating can access it from
// subclasses.
void RecordSampleReceived(base::TimeTicks,
metrics::SampledProfile sampled_profile) {
EXPECT_EQ(sampled_profile.trigger_event(),
metrics::SampledProfile::PERIODIC_HEAP_COLLECTION);
EXPECT_EQ(sampled_profile.process(), expected_process_);
// The mock clock should not have advanced since the sample was recorded, so
// the collection time can be compared exactly.
const base::TimeDelta expected_time_offset =
task_environment_.NowTicks() - profiler_creation_time_;
EXPECT_EQ(sampled_profile.call_stack_profile().profile_time_offset_ms(),
expected_time_offset.InMilliseconds());
sample_received_ = true;
}
protected:
// The default constructor parameters enable the HeapProfilerReporting feature
// on all channels. Child classes can override the constructor to create test
// suites that test different configurations. Empty `supported_processes` uses
// the default feature config, which should be browser-only.
explicit HeapProfilerControllerTest(bool feature_enabled = true,
const char* supported_processes = "",
double stable_probability = 1.0,
double nonstable_probability = 1.0) {
// ScopedFeatureList must be initialized in the constructor, before any
// threads are started.
if (feature_enabled) {
feature_list_.InitAndEnableFeatureWithParameters(
kHeapProfilerReporting,
{
{"stable-probability", base::NumberToString(stable_probability)},
{"nonstable-probability",
base::NumberToString(nonstable_probability)},
{"sampling-rate", base::NumberToString(kSamplingRate)},
{"supported-processes", supported_processes},
});
} else {
feature_list_.InitAndDisableFeature(kHeapProfilerReporting);
// Set the sampling rate manually since there's no param to read.
base::SamplingHeapProfiler::Get()->SetSamplingInterval(kSamplingRate);
}
// Clear any samples set in the global SamplingHeapProfiler before the
// ScopedMuteHookedSamplesForTesting was created.
base::SamplingHeapProfiler::Get()->ClearSamplesForTesting();
}
~HeapProfilerControllerTest() override {
// Remove any collectors that were set in StartHeapProfiling.
metrics::CallStackProfileBuilder::SetBrowserProcessReceiverCallback(
base::DoNothing());
metrics::CallStackProfileBuilder::
ResetChildCallStackProfileCollectorForTesting();
}
void StartHeapProfiling(version_info::Channel channel,
ProcessType process_type,
ProfileCollectorCallback collector_callback) {
ASSERT_FALSE(controller_) << "StartHeapProfiling called twice";
switch (process_type) {
case ProcessType::kBrowser:
expected_process_ = metrics::Process::BROWSER_PROCESS;
metrics::CallStackProfileBuilder::SetBrowserProcessReceiverCallback(
std::move(collector_callback));
break;
case ProcessType::kUtility:
expected_process_ = metrics::Process::UTILITY_PROCESS;
ConnectRemoteProfileCollector(std::move(collector_callback));
break;
default:
// Connect up the profile collector even though we expect the heap
// profiler not to start, so that the test environment is complete.
expected_process_ = metrics::Process::UNKNOWN_PROCESS;
ConnectRemoteProfileCollector(std::move(collector_callback));
}
ASSERT_EQ(HeapProfilerController::GetProfilingEnabled(),
HeapProfilerController::ProfilingEnabled::kNoController);
profiler_creation_time_ = task_environment_.NowTicks();
controller_ =
std::make_unique<HeapProfilerController>(channel, process_type);
controller_->SuppressRandomnessForTesting();
controller_->StartIfEnabled();
}
void AddOneSampleAndWait() {
auto* sampler = base::PoissonAllocationSampler::Get();
sampler->RecordAlloc(reinterpret_cast<void*>(0x1337), kAllocationSize,
base::PoissonAllocationSampler::kManualForTesting,
nullptr);
// Advance several days to be sure the sample isn't scheduled right on the
// boundary of the fast-forward.
task_environment_.FastForwardBy(base::Days(2));
// Free the allocation so that other tests can re-use the address.
sampler->RecordFree(reinterpret_cast<void*>(0x1337));
}
void ConnectRemoteProfileCollector(
ProfileCollectorCallback collector_callback) {
mojo::PendingRemote<metrics::mojom::CallStackProfileCollector> remote;
child_profile_collector_ = mojo::MakeSelfOwnedReceiver(
std::make_unique<TestCallStackProfileCollector>(
std::move(collector_callback)),
remote.InitWithNewPipeAndPassReceiver());
metrics::CallStackProfileBuilder::SetParentProfileCollectorForChildProcess(
std::move(remote));
}
// Initialize `mute_hooks_` before `task_environment_` so that memory
// allocations aren't sampled while TaskEnvironment creates a thread. The
// sampling is crashing in the hooked FreeFunc on some test bots.
base::PoissonAllocationSampler::ScopedMuteHookedSamplesForTesting mute_hooks_;
base::PoissonAllocationSampler::ScopedSuppressRandomnessForTesting
suppress_randomness_;
// Create `feature_list_` before `task_environment_` and destroy it after to
// avoid a race in destruction.
base::test::ScopedFeatureList feature_list_;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<HeapProfilerController> controller_;
base::HistogramTester histogram_tester_;
mojo::SelfOwnedReceiverRef<metrics::mojom::CallStackProfileCollector>
child_profile_collector_;
// The creation time of the HeapProfilerController, saved so that
// RecordSampleReceived() can test that SampledProfile::ms_after_login() in
// that sample is a delta from the creation time.
base::TimeTicks profiler_creation_time_;
// Expected process type in a sample.
metrics::Process expected_process_ = metrics::Process::UNKNOWN_PROCESS;
// `sample_received_` is read from the main thread and written from a
// background thread, but does not need to be atomic because the write happens
// during a scheduled sample and the read happens well after that.
bool sample_received_ = false;
};
namespace {
TEST_F(HeapProfilerControllerTest, EmptyProfileIsNotEmitted) {
StartHeapProfiling(
version_info::Channel::STABLE, ProcessType::kBrowser,
base::BindRepeating(&HeapProfilerControllerTest::RecordSampleReceived,
base::Unretained(this)));
// Advance several days to be sure the sample isn't scheduled right on the
// boundary of the fast-forward.
task_environment_.FastForwardBy(base::Days(2));
EXPECT_FALSE(sample_received_);
}
// Sampling profiler is not capable of unwinding stack on Android under tests.
#if !BUILDFLAG(IS_ANDROID)
TEST_F(HeapProfilerControllerTest, ProfileCollectionsScheduler) {
constexpr int kSnapshotsToCollect = 3;
std::atomic<int> profile_count{0};
auto check_profile = [&](base::TimeTicks time,
metrics::SampledProfile profile) {
EXPECT_EQ(metrics::SampledProfile::PERIODIC_HEAP_COLLECTION,
profile.trigger_event());
EXPECT_LT(0, profile.call_stack_profile().stack_sample_size());
bool found = false;
for (const metrics::CallStackProfile::StackSample& sample :
profile.call_stack_profile().stack_sample()) {
// Check that the samples being reported are the allocations created
// below. The sampler calculates the average weight of each sample, and
// sometimes rounds up to more than kAllocationSize.
if (sample.has_weight() &&
static_cast<size_t>(sample.weight()) >= kAllocationSize) {
found = true;
break;
}
}
EXPECT_TRUE(found);
++profile_count;
};
StartHeapProfiling(version_info::Channel::STABLE, ProcessType::kBrowser,
base::BindLambdaForTesting(check_profile));
auto* sampler = base::PoissonAllocationSampler::Get();
sampler->RecordAlloc(reinterpret_cast<void*>(0x1337), kAllocationSize,
base::PoissonAllocationSampler::kManualForTesting,
nullptr);
sampler->RecordAlloc(reinterpret_cast<void*>(0x7331), kAllocationSize,
base::PoissonAllocationSampler::kManualForTesting,
nullptr);
// The profiler should continue to collect snapshots as long as this memory is
// allocated. If not the test will time out.
while (profile_count < kSnapshotsToCollect) {
task_environment_.FastForwardBy(base::Days(1));
}
// Free all recorded memory so the address list is empty for the next test.
sampler->RecordFree(reinterpret_cast<void*>(0x1337));
sampler->RecordFree(reinterpret_cast<void*>(0x7331));
}
#endif
// Configurations of the HeapProfilerReporting feature to test.
// The default parameters collect samples from stable and nonstable channels in
// the browser process only.
struct FeatureTestParams {
struct ChannelParams {
double probability = 1.0;
bool expect_browser_sample = true;
bool expect_child_sample = false;
};
bool feature_enabled = true;
const char* supported_processes = "";
ChannelParams stable;
ChannelParams nonstable;
};
std::ostream& operator<<(std::ostream& os, const FeatureTestParams& params) {
os << "{";
os << "enabled:" << params.feature_enabled << ",";
os << "processes:[" << params.supported_processes << "],";
os << "stable:" << params.stable.probability << ",";
os << "nonstable:" << params.nonstable.probability;
os << "}";
return os;
}
class HeapProfilerControllerFeatureTest
: public HeapProfilerControllerTest,
public ::testing::WithParamInterface<FeatureTestParams> {
public:
HeapProfilerControllerFeatureTest()
: HeapProfilerControllerTest(GetParam().feature_enabled,
GetParam().supported_processes,
GetParam().stable.probability,
GetParam().nonstable.probability) {}
};
// Test the feature on various channels.
constexpr FeatureTestParams kChannelConfigs[] = {
// Disabled.
{
.feature_enabled = false,
.stable = {.expect_browser_sample = false},
.nonstable = {.expect_browser_sample = false},
},
// Enabled, but with probability 0 on all channels.
{
.feature_enabled = true,
.stable = {.probability = 0.0, .expect_browser_sample = false},
.nonstable = {.probability = 0.0, .expect_browser_sample = false},
},
// Enabled on all channels.
{
.feature_enabled = true,
.stable = {.probability = 1.0, .expect_browser_sample = true},
.nonstable = {.probability = 1.0, .expect_browser_sample = true},
},
// Enabled on stable channel only.
{
.feature_enabled = true,
.stable = {.probability = 1.0, .expect_browser_sample = true},
.nonstable = {.probability = 0.0, .expect_browser_sample = false},
},
// Enabled on non-stable channels only.
{
.feature_enabled = true,
.stable = {.probability = 0.0, .expect_browser_sample = false},
.nonstable = {.probability = 1.0, .expect_browser_sample = true},
},
};
using HeapProfilerControllerChannelTest = HeapProfilerControllerFeatureTest;
INSTANTIATE_TEST_SUITE_P(All,
HeapProfilerControllerChannelTest,
::testing::ValuesIn(kChannelConfigs));
TEST_P(HeapProfilerControllerChannelTest, StableChannel) {
StartHeapProfiling(
version_info::Channel::STABLE, ProcessType::kBrowser,
base::BindRepeating(&HeapProfilerControllerTest::RecordSampleReceived,
base::Unretained(this)));
EXPECT_EQ(HeapProfilerController::GetProfilingEnabled(),
GetParam().stable.expect_browser_sample
? HeapProfilerController::ProfilingEnabled::kEnabled
: HeapProfilerController::ProfilingEnabled::kDisabled);
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser",
GetParam().stable.expect_browser_sample, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
GetParam().stable.expect_browser_sample,
1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, GetParam().stable.expect_browser_sample);
}
// TODO(crbug.com/1302007): This test hangs on iPad device.
#if BUILDFLAG(IS_IOS)
#define MAYBE_CanaryChannel DISABLED_CanaryChannel
#else
#define MAYBE_CanaryChannel CanaryChannel
#endif
TEST_P(HeapProfilerControllerChannelTest, MAYBE_CanaryChannel) {
StartHeapProfiling(
version_info::Channel::CANARY, ProcessType::kBrowser,
base::BindRepeating(&HeapProfilerControllerTest::RecordSampleReceived,
base::Unretained(this)));
EXPECT_EQ(HeapProfilerController::GetProfilingEnabled(),
GetParam().nonstable.expect_browser_sample
? HeapProfilerController::ProfilingEnabled::kEnabled
: HeapProfilerController::ProfilingEnabled::kDisabled);
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser",
GetParam().nonstable.expect_browser_sample, 1);
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled",
GetParam().nonstable.expect_browser_sample, 1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, GetParam().nonstable.expect_browser_sample);
}
// Test the feature in various processes.
constexpr FeatureTestParams kProcessConfigs[] = {
// Enabled in parent process only.
{
.supported_processes = "browser",
.stable = {.expect_browser_sample = true, .expect_child_sample = false},
.nonstable = {.expect_browser_sample = true,
.expect_child_sample = false},
},
// Enabled in child process only.
{
.supported_processes = "utility",
.stable = {.expect_browser_sample = false, .expect_child_sample = true},
.nonstable = {.expect_browser_sample = false,
.expect_child_sample = true},
},
// Enabled in parent and child processes.
{
.supported_processes = "browser;utility",
.stable = {.expect_browser_sample = true, .expect_child_sample = true},
.nonstable = {.expect_browser_sample = true,
.expect_child_sample = true},
},
};
using HeapProfilerControllerProcessTest = HeapProfilerControllerFeatureTest;
INSTANTIATE_TEST_SUITE_P(All,
HeapProfilerControllerProcessTest,
::testing::ValuesIn(kProcessConfigs));
TEST_P(HeapProfilerControllerProcessTest, BrowserProcess) {
StartHeapProfiling(
version_info::Channel::STABLE, ProcessType::kBrowser,
base::BindRepeating(&HeapProfilerControllerTest::RecordSampleReceived,
base::Unretained(this)));
EXPECT_EQ(HeapProfilerController::GetProfilingEnabled(),
GetParam().stable.expect_browser_sample
? HeapProfilerController::ProfilingEnabled::kEnabled
: HeapProfilerController::ProfilingEnabled::kDisabled);
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser",
GetParam().stable.expect_browser_sample, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
GetParam().stable.expect_browser_sample,
1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, GetParam().stable.expect_browser_sample);
}
TEST_P(HeapProfilerControllerProcessTest, ChildProcess) {
StartHeapProfiling(
version_info::Channel::STABLE, ProcessType::kUtility,
base::BindRepeating(&HeapProfilerControllerTest::RecordSampleReceived,
base::Unretained(this)));
EXPECT_EQ(HeapProfilerController::GetProfilingEnabled(),
GetParam().stable.expect_child_sample
? HeapProfilerController::ProfilingEnabled::kEnabled
: HeapProfilerController::ProfilingEnabled::kDisabled);
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Utility",
GetParam().stable.expect_child_sample, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
GetParam().stable.expect_child_sample,
1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, GetParam().stable.expect_child_sample);
}
class HeapProfilerControllerUnknownProcessTest
: public HeapProfilerControllerTest {
protected:
HeapProfilerControllerUnknownProcessTest()
: HeapProfilerControllerTest(
/*feature_enabled=*/true,
/*supported_processes=*/"browser;unrecognized-process-string") {}
};
TEST_F(HeapProfilerControllerUnknownProcessTest, UnknownParamString) {
// Unrecognized string in `supported_processes` should safely be ignored.
StartHeapProfiling(version_info::Channel::STABLE, ProcessType::kBrowser,
base::DoNothing());
EXPECT_EQ(HeapProfilerController::GetProfilingEnabled(),
HeapProfilerController::ProfilingEnabled::kEnabled);
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser", true, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled", true,
1);
}
TEST_F(HeapProfilerControllerUnknownProcessTest, UnhandledProcess) {
// Starting the heap profiler in an unhandled process type should safely do
// nothing.
StartHeapProfiling(version_info::Channel::STABLE, ProcessType::kUnknown,
base::DoNothing());
EXPECT_EQ(HeapProfilerController::GetProfilingEnabled(),
HeapProfilerController::ProfilingEnabled::kDisabled);
// The Enabled summary histogram should not be logged for unsupported
// processes, because they're not included in the per-process histograms that
// are aggregated into it.
histogram_tester_.ExpectTotalCount("HeapProfiling.InProcess.Enabled", 0);
}
} // namespace
} // namespace heap_profiling