blob: f5c94641c69bb65ceb85008b84101cca9d1ce63b [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 <array>
#include <atomic>
#include <iomanip>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "base/allocator/dispatcher/notification_data.h"
#include "base/allocator/dispatcher/subsystem.h"
#include "base/auto_reset.h"
#include "base/barrier_closure.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/metrics_hashes.h"
#include "base/process/launch.h"
#include "base/process/process.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/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/multiprocess_test.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "base/types/optional_util.h"
#include "build/build_config.h"
#include "components/heap_profiling/in_process/browser_process_snapshot_controller.h"
#include "components/heap_profiling/in_process/child_process_snapshot_controller.h"
#include "components/heap_profiling/in_process/heap_profiler_parameters.h"
#include "components/heap_profiling/in_process/mojom/snapshot_controller.mojom.h"
#include "components/heap_profiling/in_process/mojom/test_connector.mojom.h"
#include "components/heap_profiling/in_process/switches.h"
#include "components/metrics/call_stacks/call_stack_profile_builder.h"
#include "components/metrics/public/mojom/call_stack_profile_collector.mojom.h"
#include "components/sampling_profiler/process_type.h"
#include "components/version_info/channel.h"
#include "mojo/core/embedder/scoped_ipc_support.h"
#include "mojo/public/cpp/base/proto_wrapper.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "mojo/public/cpp/bindings/unique_receiver_set.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "mojo/public/cpp/system/invitation.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/metrics_proto/call_stack_profile.pb.h"
#include "third_party/metrics_proto/execution_context.pb.h"
#include "third_party/metrics_proto/sampled_profile.pb.h"
// Test printers. These need to be in the same namespace as the types to print
// so GTest can find them.
namespace metrics {
void PrintTo(const SampledProfile& profile, std::ostream* os) {
*os << "process:" << profile.process() << ",samples";
if (profile.call_stack_profile().stack_sample_size() == 0) {
*os << ":none";
} else {
for (const auto& sample : profile.call_stack_profile().stack_sample()) {
*os << ":" << sample.count() << "/" << sample.weight();
}
}
*os << ",metadata";
if (profile.call_stack_profile().profile_metadata_size() == 0) {
*os << ":none";
} else {
const auto& name_hashes = profile.call_stack_profile().metadata_name_hash();
for (const auto& metadata :
profile.call_stack_profile().profile_metadata()) {
if (metadata.name_hash_index() < name_hashes.size()) {
*os << ":" << std::hex << "0x"
<< name_hashes.at(metadata.name_hash_index()) << std::dec;
} else {
*os << ":unknown";
}
*os << "=" << metadata.value();
}
}
}
} // namespace metrics
namespace sampling_profiler {
void PrintTo(const ProfilerProcessType& process_type, std::ostream* os) {
switch (process_type) {
case ProfilerProcessType::kUnknown:
*os << "unknown";
return;
case ProfilerProcessType::kBrowser:
*os << "browser";
return;
case ProfilerProcessType::kGpu:
*os << "gpu";
return;
case ProfilerProcessType::kNetworkService:
*os << "network";
return;
case ProfilerProcessType::kRenderer:
*os << "renderer";
return;
case ProfilerProcessType::kUtility:
*os << "utility";
return;
default:
*os << "unsupported (" << static_cast<int>(process_type) << ")";
return;
}
}
} // namespace sampling_profiler
namespace heap_profiling {
namespace {
#if BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)
#define ENABLE_MULTIPROCESS_TESTS 0
#else
#define ENABLE_MULTIPROCESS_TESTS 1
#endif
using base::allocator::dispatcher::AllocationNotificationData;
using base::allocator::dispatcher::AllocationSubsystem;
using base::allocator::dispatcher::FreeNotificationData;
using base::test::FeatureRef;
using base::test::FeatureRefAndParams;
using sampling_profiler::ProfilerProcessType;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Combine;
using ::testing::Conditional;
using ::testing::ElementsAre;
using ::testing::Ge;
using ::testing::IsEmpty;
using ::testing::Lt;
using ::testing::Optional;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::UnorderedElementsAre;
using ::testing::Values;
using ::testing::ValuesIn;
using ProfileCollectorCallback =
base::RepeatingCallback<void(base::TimeTicks, metrics::SampledProfile)>;
using ScopedMuteHookedSamplesForTesting =
base::PoissonAllocationSampler::ScopedMuteHookedSamplesForTesting;
using ScopedSuppressRandomnessForTesting =
base::PoissonAllocationSampler::ScopedSuppressRandomnessForTesting;
constexpr size_t kSamplingRate = 1024;
constexpr size_t kAllocationSize = 42 * kSamplingRate;
// 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(base::OptionalUnwrapTo(
profile->contents.As<metrics::SampledProfile>(), sampled_profile));
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_;
};
// A scoped holder for callbacks to pass to
// HeapProfilerControllerTest::StartHeapProfiling(), and a BarrierClosure that
// will quit a run loop. The ScopedCallbacks object must stay in scope until the
// run loop finishes.
class ScopedCallbacks {
public:
// Creates a BarrierClosure that will invoke the given `quit_closure` after
// the expected callbacks are invoked:
//
// If `expect_take_snapshot` is true, HeapProfilerController::TakeSnapshot()
// should be called, so first_snapshot_callback() returns a callback that's
// expected to be invoked. If not, first_snapshot_callback() returns a
// callback that's expected not to run.
//
// If `expected_sampled_profiles` > 0, HeapProfilerController::TakeSnapshot()
// should find a sample to pass to CallStackProfileBuilder, so
// collector_callback() returns a callback that's expected to be invoked. It
// will also run the given `profile_collector_callback`. If not,
// collector_callback() returns a callback that's expected not to run, and the
// given `profile_collector_callback` is ignored.
//
// If `use_other_process_callback` is true, the test will also fake a snapshot
// in another process, so other_process_callback() will return a callback to
// invoke for this.
ScopedCallbacks(bool expect_take_snapshot,
size_t expected_sampled_profiles,
bool use_other_process_callback,
ProfileCollectorCallback profile_collector_callback,
base::OnceClosure quit_closure) {
size_t num_callbacks = 0;
if (expect_take_snapshot) {
num_callbacks += 1;
}
num_callbacks += expected_sampled_profiles;
if (use_other_process_callback) {
// The test should invoke other_process_snapshot_callback() to simulate a
// snapshot in another process.
num_callbacks += 1;
}
barrier_closure_ =
base::BarrierClosure(num_callbacks, std::move(quit_closure));
// Each callback should invoke `barrier_closure_` once. If they're called
// too often, log a test failure on the first extra call only to avoid log
// spam. These lambdas need to take a copy of the method arguments since
// they outlive the method` scope.
first_snapshot_callback_ =
base::BindLambdaForTesting([this, expect_take_snapshot] {
if (!expect_take_snapshot) {
FAIL() << "TakeSnapshot called unexpectedly.";
}
first_snapshot_count_++;
if (first_snapshot_count_ == 1) {
barrier_closure_.Run();
return;
}
if (first_snapshot_count_ == 2) {
FAIL() << "TakeSnapshot callback invoked too many times.";
}
});
collector_callback_ = base::BindLambdaForTesting(
[this, expected_sampled_profiles,
callback = std::move(profile_collector_callback)](
base::TimeTicks time_ticks, metrics::SampledProfile profile) {
if (expected_sampled_profiles == 0) {
FAIL() << "ProfileCollectorCallback called unexpectedly.";
}
collector_count_++;
if (collector_count_ <= expected_sampled_profiles) {
std::move(callback).Run(time_ticks, profile);
barrier_closure_.Run();
return;
}
if (collector_count_ == expected_sampled_profiles + 1) {
FAIL() << "ProfileCollectorCallback invoked too many times.";
}
});
other_process_callback_ =
base::BindLambdaForTesting([this, use_other_process_callback] {
if (!use_other_process_callback) {
FAIL() << "Other process callback invoked unexpectedly.";
}
other_process_count_++;
if (other_process_count_ == 1) {
barrier_closure_.Run();
return;
}
if (other_process_count_ == 2) {
FAIL() << "Other process callback invoked too many times.";
}
});
}
~ScopedCallbacks() = default;
// Move-only.
ScopedCallbacks(const ScopedCallbacks&) = delete;
ScopedCallbacks& operator=(const ScopedCallbacks&) = delete;
ScopedCallbacks(ScopedCallbacks&&) = default;
ScopedCallbacks& operator=(ScopedCallbacks&&) = default;
base::OnceClosure first_snapshot_callback() {
return std::move(first_snapshot_callback_);
}
ProfileCollectorCallback collector_callback() {
// Return by copy since this is a RepeatingCallback.
return collector_callback_;
}
base::OnceClosure other_process_callback() {
return std::move(other_process_callback_);
}
private:
base::RepeatingClosure barrier_closure_;
base::OnceClosure first_snapshot_callback_;
ProfileCollectorCallback collector_callback_;
base::OnceClosure other_process_callback_;
size_t first_snapshot_count_ = 0;
size_t collector_count_ = 0;
size_t other_process_count_ = 0;
};
class ProfilerSetUpMixin {
public:
ProfilerSetUpMixin(const std::vector<FeatureRefAndParams>& enabled_features,
const std::vector<FeatureRef>& disabled_features) {
// ScopedFeatureList must be initialized in the constructor, before any
// threads are started.
feature_list_.InitWithFeaturesAndParameters(enabled_features,
disabled_features);
if (!base::FeatureList::IsEnabled(kHeapProfilerReporting)) {
// Set the sampling rate manually since there's no feature param to read.
base::SamplingHeapProfiler::Get()->SetSamplingInterval(kSamplingRate);
}
}
~ProfilerSetUpMixin() = default;
base::test::TaskEnvironment& task_env() { return task_environment_; }
private:
// 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.
ScopedMuteHookedSamplesForTesting mute_hooks_ =
base::SamplingHeapProfiler::Get()->MuteHookedSamplesForTesting();
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,
base::test::TaskEnvironment::MainThreadType::IO};
};
#if ENABLE_MULTIPROCESS_TESTS
constexpr char kTestChildTypeSwitch[] = "heap-profiler-test-child-type";
constexpr char kTestNumAllocationsSwitch[] =
"heap-profiler-test-num-allocations";
constexpr char kTestExpectChildProfileSwitch[] =
"heap-profiler-expect-child-profile";
// Runs the heap profiler in a multiprocess test child. This is used instead of
// HeapProfilerControllerTest::CreateHeapProfiler() in tests that create real
// child processes. (Most tests run only in the test main process and pretend
// that it's the Chrome browser process or a Chrome child process.)
class MultiprocessTestChild final : public mojom::TestConnector,
public ProfilerSetUpMixin {
public:
MultiprocessTestChild(
const std::vector<FeatureRefAndParams>& enabled_features,
const std::vector<FeatureRef>& disabled_features)
: ProfilerSetUpMixin(enabled_features, disabled_features),
quit_closure_(task_env().QuitClosure()) {}
~MultiprocessTestChild() final = default;
MultiprocessTestChild(const MultiprocessTestChild&) = delete;
MultiprocessTestChild& operator=(const MultiprocessTestChild&) = delete;
void RunTestInChild() {
base::HistogramTester histogram_tester;
// Get the process type and number of allocations to simulate.
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
ASSERT_TRUE(command_line);
int process_type = 0;
ASSERT_TRUE(base::StringToInt(
command_line->GetSwitchValueASCII(kTestChildTypeSwitch),
&process_type));
const ProfilerProcessType profiler_process_type =
static_cast<ProfilerProcessType>(process_type);
int num_allocations = 0;
ASSERT_TRUE(base::StringToInt(
command_line->GetSwitchValueASCII(kTestNumAllocationsSwitch),
&num_allocations));
// Set up mojo support and attach to the parent's pipe.
mojo::core::ScopedIPCSupport enable_mojo(
base::SingleThreadTaskRunner::GetCurrentDefault(),
mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);
mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept(
mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine(
*command_line));
// Handle the TestConnector::Connect() message that will connect the
// SnapshotController and CallStackProfileCollector interfaces.
mojo::PendingReceiver<mojom::TestConnector> pending_receiver(
invitation.ExtractMessagePipe(0));
mojo::Receiver<mojom::TestConnector> receiver(this,
std::move(pending_receiver));
// Start the heap profiler and wait for TakeSnapshot() messages from the
// parent.
HeapProfilerController controller(version_info::Channel::STABLE,
profiler_process_type);
controller.SuppressRandomnessForTesting();
ASSERT_TRUE(controller.IsEnabled());
controller.StartIfEnabled();
// Make a fixed number of allocations at different addresses to include in
// snapshots. No need to free since the process will exit after the test.
for (int i = 0; i < num_allocations; ++i) {
base::PoissonAllocationSampler::Get()->OnAllocation(
AllocationNotificationData(reinterpret_cast<void*>(0x1337 + i),
kAllocationSize, nullptr,
AllocationSubsystem::kManualForTesting));
}
// Loop until the TestConnector::Disconnect() message.
task_env().RunUntilQuit();
// Profiler stats should be logged whether or not a snapshot was taken, as
// long as the child is profiled at all.
size_t expected_histogram_count =
command_line->HasSwitch(kTestExpectChildProfileSwitch) ? 1 : 0;
switch (profiler_process_type) {
case ProfilerProcessType::kGpu:
histogram_tester.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot.GPU",
expected_histogram_count);
break;
case ProfilerProcessType::kNetworkService:
histogram_tester.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot.Network",
expected_histogram_count);
break;
case ProfilerProcessType::kRenderer:
histogram_tester.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot.Renderer",
expected_histogram_count);
break;
case ProfilerProcessType::kUtility:
histogram_tester.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot.Utility",
expected_histogram_count);
break;
default:
FAIL() << "Unexpected process type " << process_type;
}
histogram_tester.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot", expected_histogram_count);
}
// mojom::TestConnector:
void ConnectSnapshotController(
mojo::PendingReceiver<mojom::SnapshotController> controller,
base::OnceClosure done_callback) final {
ChildProcessSnapshotController::CreateSelfOwnedReceiver(
std::move(controller));
std::move(done_callback).Run();
}
void ConnectProfileCollector(
mojo::PendingRemote<metrics::mojom::CallStackProfileCollector> collector,
base::OnceClosure done_callback) final {
metrics::CallStackProfileBuilder::SetParentProfileCollectorForChildProcess(
std::move(collector));
std::move(done_callback).Run();
}
void Disconnect() final { std::move(quit_closure_).Run(); }
private:
base::OnceClosure quit_closure_;
};
// Manages a set of multiprocess test children and mojo connections to them.
// Created in test cases in the parent process.
class MultiprocessTestParent {
public:
MultiprocessTestParent() = default;
MultiprocessTestParent(const MultiprocessTestParent&) = delete;
MultiprocessTestParent& operator=(const MultiprocessTestParent&) = delete;
~MultiprocessTestParent() {
// Tell all children to stop profiling and wait for them to exit.
for (auto& connector : test_connectors_) {
connector->Disconnect();
}
for (const auto& process : child_processes_) {
int exit_code = 0;
EXPECT_TRUE(base::WaitForMultiprocessTestChildExit(
process, TestTimeouts::action_timeout(), &exit_code));
EXPECT_EQ(exit_code, 0);
}
}
// Waits until `num_children` are connected, then starts profiling the parent
// process with `controller`.
void StartHeapProfilingWhenChildrenConnected(
size_t num_children,
HeapProfilerController* controller) {
// StartIfEnabled() needs to run on the current sequence no matter what
// thread mojo calls `on_child_connected_closure_` from.
on_child_connected_closure_ = base::BarrierClosure(
num_children, base::BindPostTaskToCurrentDefault(
base::BindLambdaForTesting([this, controller] {
// Make sure all children connected successfully.
ASSERT_EQ(test_connectors_.size(),
child_processes_.size());
EXPECT_TRUE(controller->StartIfEnabled());
})));
}
// Called from HeapProfilerController::AppendCommandLineSwitchForChildProcess
// with `connector_id` and `receiver`, plus a `remote` added by the test.
// `connector_id` is the id of a mojo TestConnector interface for the process.
// In production this parameter is the child process id.
void BindTestConnector(
int connector_id,
mojo::PendingReceiver<mojom::SnapshotController> receiver,
mojo::PendingRemote<metrics::mojom::CallStackProfileCollector> remote) {
mojom::TestConnector* connector = test_connectors_.Get(
mojo::RemoteSetElementId::FromUnsafeValue(connector_id));
ASSERT_TRUE(connector);
// BrowserProcessSnapshotController holds the remote end of the
// mojom::SnapshotController. Pass the other end to the test child if it
// should be profiled, otherwise drop it. This verifies that an unbound
// SnapshotController is handled correctly.
base::OnceClosure bind_receiver_closure;
if (should_profile_next_launch_) {
// Unretained is safe since `test_connectors_` won't be destroyed until
// the reply callback runs.
bind_receiver_closure =
base::BindOnce(&mojom::TestConnector::ConnectSnapshotController,
base::Unretained(connector), std::move(receiver),
on_child_connected_closure_);
} else {
bind_receiver_closure = on_child_connected_closure_;
}
// The test fixture holds the receiver end of the CallStackProfileCollector.
// Pass the other end to the test child. The response will eventually
// trigger `on_child_connected_closure_`.
connector->ConnectProfileCollector(std::move(remote),
std::move(bind_receiver_closure));
}
// Launches a multiprocess test child and registers it with `controller`.
// The child will simulate a process of type `process_type` and make
// `num_allocations` memory allocations to report in heap snapshots. If
// `should_profile` is false, simulate the embedder refusing to profile the
// child process.
void LaunchTestChild(HeapProfilerController* controller,
ProfilerProcessType process_type,
int num_allocations,
bool should_profile) {
// `should_profile` will apply during next call to BindTestConnector().
base::AutoReset should_profile_next_launch(&should_profile_next_launch_,
should_profile);
base::LaunchOptions launch_options;
base::CommandLine child_command_line =
base::GetMultiProcessTestChildBaseCommandLine();
child_command_line.AppendSwitchASCII(
kTestChildTypeSwitch,
base::NumberToString(static_cast<int>(process_type)));
child_command_line.AppendSwitchASCII(kTestNumAllocationsSwitch,
base::NumberToString(num_allocations));
if (should_profile) {
child_command_line.AppendSwitch(kTestExpectChildProfileSwitch);
}
// Attach a mojo channel to the child.
mojo::PlatformChannel channel;
channel.PrepareToPassRemoteEndpoint(&launch_options, &child_command_line);
mojo::OutgoingInvitation invitation;
mojo::PendingRemote<mojom::TestConnector> pending_connector(
invitation.AttachMessagePipe(0), 0);
mojo::RemoteSetElementId connector_id =
test_connectors_.Add(std::move(pending_connector));
// In production this only connects the parent end of the SnapshotController
// since content::ChildProcessHost brokers the interface with the child. For
// the test, smuggle the id of a TestConnector to broker the interface by
// pretending it's the child process id.
controller->AppendCommandLineSwitchForChildProcess(
&child_command_line, process_type, connector_id.GetUnsafeValue());
base::Process child_process = base::SpawnMultiProcessTestChild(
"HeapProfilerControllerChildMain", child_command_line, launch_options);
ASSERT_TRUE(child_process.IsValid());
// Finish connecting the mojo channel. This passes the other end of the
// TestConnector message pipe to the child.
channel.RemoteProcessLaunchAttempted();
mojo::OutgoingInvitation::Send(std::move(invitation),
child_process.Handle(),
channel.TakeLocalEndpoint());
child_processes_.push_back(std::move(child_process));
}
private:
// All child processes started by the test. If a child dies the process will
// become invalid but remain in this list.
std::vector<base::Process> child_processes_;
// Test interface for controlling each child process. If a child dies the
// interface will be disconnected and removed from this set.
mojo::RemoteSet<mojom::TestConnector> test_connectors_;
// Closure to call whenever a child process is finished connecting.
base::RepeatingClosure on_child_connected_closure_;
// While this is false, calls to BindTestConnector should skip binding the
// mojom::SnapshotController, to verify that BrowserProcessSnapshotController
// can handle an embedder that doesn't bind the controller to some processes.
bool should_profile_next_launch_ = true;
};
#endif // ENABLE_MULTIPROCESS_TESTS
class MockSnapshotController : public mojom::SnapshotController {
public:
MOCK_METHOD(void, TakeSnapshot, (uint32_t, uint32_t), (override));
MOCK_METHOD(void, LogMetricsWithoutSnapshot, (), (override));
};
// Configurations of the HeapProfiler* features 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;
};
// Whether HeapProfilerReporting is enabled.
bool feature_enabled = true;
// Parameters for different channels. Test suites that only cover one channel
// will use the `stable` params.
ChannelParams stable;
ChannelParams nonstable;
// Probabilities for snapshotting child processes.
int gpu_snapshot_prob = 100;
int network_snapshot_prob = 100;
int renderer_snapshot_prob = 100;
int utility_snapshot_prob = 100;
base::FieldTrialParams ToFieldTrialParams() const;
std::vector<FeatureRefAndParams> GetEnabledFeatures() const;
std::vector<FeatureRef> GetDisabledFeatures() const;
};
// Converts the test params to field trial parameters for the
// HeapProfilerReporting feature.
base::FieldTrialParams FeatureTestParams::ToFieldTrialParams() const {
base::FieldTrialParams field_trial_params;
// Global parameters.
field_trial_params["stable-probability"] =
base::NumberToString(stable.probability);
field_trial_params["nonstable-probability"] =
base::NumberToString(nonstable.probability);
// Per-process parameters.
field_trial_params["browser-sampling-rate-bytes"] =
base::NumberToString(kSamplingRate);
field_trial_params["gpu-sampling-rate-bytes"] =
base::NumberToString(kSamplingRate);
field_trial_params["gpu-prob-pct"] = base::NumberToString(gpu_snapshot_prob);
field_trial_params["network-sampling-rate-bytes"] =
base::NumberToString(kSamplingRate);
field_trial_params["network-prob-pct"] =
base::NumberToString(network_snapshot_prob);
field_trial_params["renderer-sampling-rate-bytes"] =
base::NumberToString(kSamplingRate);
field_trial_params["renderer-prob-pct"] =
base::NumberToString(renderer_snapshot_prob);
field_trial_params["utility-sampling-rate-bytes"] =
base::NumberToString(kSamplingRate);
field_trial_params["utility-prob-pct"] =
base::NumberToString(utility_snapshot_prob);
return field_trial_params;
}
std::vector<FeatureRefAndParams> FeatureTestParams::GetEnabledFeatures() const {
std::vector<FeatureRefAndParams> enabled_features;
if (feature_enabled) {
enabled_features.push_back(
FeatureRefAndParams(kHeapProfilerReporting, ToFieldTrialParams()));
}
return enabled_features;
}
std::vector<FeatureRef> FeatureTestParams::GetDisabledFeatures() const {
std::vector<FeatureRef> disabled_features;
if (!feature_enabled) {
disabled_features.push_back(FeatureRef(kHeapProfilerReporting));
}
return disabled_features;
}
// Formats the test params for error messages.
std::ostream& operator<<(std::ostream& os, const FeatureTestParams& params) {
os << "enabled: " << params.feature_enabled << std::endl;
os << "field_trial_params: {" << std::endl;
for (const auto& field_trial_param : params.ToFieldTrialParams()) {
os << field_trial_param.first << ": " << field_trial_param.second
<< std::endl;
}
os << "}" << std::endl;
os << "expect_samples: {" << std::endl;
os << "stable/browser: " << params.stable.expect_browser_sample << std::endl;
os << "stable/child: " << params.stable.expect_child_sample << std::endl;
os << "nonstable/browser: " << params.stable.expect_browser_sample
<< std::endl;
os << "nonstable/child: " << params.stable.expect_child_sample << std::endl;
os << "}";
return os;
}
// Generic parameterized test suite. Subsets of the tests will alias this to
// create test suites with different parameter lists. The ProfilerProcessType
// parameter is a type of child process to launch, or kUnknown for tests that
// don't launch child processes.
class HeapProfilerControllerTest
: public ::testing::TestWithParam<
std::tuple<FeatureTestParams, ProfilerProcessType>>,
public ProfilerSetUpMixin {
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_env().NowTicks() - profiler_creation_time_;
EXPECT_EQ(sampled_profile.call_stack_profile().profile_time_offset_ms(),
expected_time_offset.InMilliseconds());
sample_received_ = true;
}
protected:
HeapProfilerControllerTest()
: ProfilerSetUpMixin(feature_params().GetEnabledFeatures(),
feature_params().GetDisabledFeatures()) {}
~HeapProfilerControllerTest() override {
// Remove any collectors that were set in StartHeapProfiling.
metrics::CallStackProfileBuilder::SetBrowserProcessReceiverCallback(
base::DoNothing());
metrics::CallStackProfileBuilder::
ResetChildCallStackProfileCollectorForTesting();
}
const FeatureTestParams& feature_params() const {
return std::get<0>(GetParam());
}
const ProfilerProcessType& child_process_type() const {
return std::get<1>(GetParam());
}
// Creates a HeapProfilerController to mock profiling a process of type
// `process_type` on `channel`. The test should pass `expect_enabled` as true
// if heap profiling should be enabled in this test setup.
//
// `first_snapshot_callback` will be invoked the first time
// HeapProfilerController::TakeSnapshot() is called, even if it doesn't
// collect a profile. `collector_callback` will be invoked whenever
// TakeSnapshot() passes a profile to CallStackProfileBuilder.
//
// The test must call StartIfEnabled() after this to start profiling.
void CreateHeapProfiler(
version_info::Channel channel,
ProfilerProcessType process_type,
bool expect_enabled,
base::OnceClosure first_snapshot_callback = base::DoNothing(),
ProfileCollectorCallback collector_callback = base::DoNothing()) {
ASSERT_FALSE(controller_) << "CreateHeapProfiler called twice";
if (process_type == ProfilerProcessType::kBrowser) {
expected_process_ = metrics::Process::BROWSER_PROCESS;
metrics::CallStackProfileBuilder::SetBrowserProcessReceiverCallback(
std::move(collector_callback));
} else {
switch (process_type) {
case ProfilerProcessType::kGpu:
expected_process_ = metrics::Process::GPU_PROCESS;
break;
case ProfilerProcessType::kNetworkService:
expected_process_ = metrics::Process::NETWORK_SERVICE_PROCESS;
break;
case ProfilerProcessType::kRenderer:
expected_process_ = metrics::Process::RENDERER_PROCESS;
break;
case ProfilerProcessType::kUtility:
expected_process_ = metrics::Process::UTILITY_PROCESS;
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;
break;
}
metrics::CallStackProfileBuilder::
SetParentProfileCollectorForChildProcess(
AddTestProfileCollector(std::move(collector_callback)));
}
ASSERT_FALSE(HeapProfilerController::GetInstance());
profiler_creation_time_ = task_env().NowTicks();
controller_ =
std::make_unique<HeapProfilerController>(channel, process_type);
controller_->SuppressRandomnessForTesting();
controller_->SetFirstSnapshotCallbackForTesting(
std::move(first_snapshot_callback));
EXPECT_EQ(HeapProfilerController::GetInstance(), controller_.get());
EXPECT_EQ(controller_->IsEnabled(), expect_enabled);
}
// Creates a HeapProfilerController with CreateHeapProfiler() and starts
// profiling.
void StartHeapProfiling(
version_info::Channel channel,
ProfilerProcessType process_type,
bool expect_enabled,
base::OnceClosure first_snapshot_callback = base::DoNothing(),
ProfileCollectorCallback collector_callback = base::DoNothing()) {
CreateHeapProfiler(channel, process_type, expect_enabled,
std::move(first_snapshot_callback),
std::move(collector_callback));
EXPECT_EQ(controller_->StartIfEnabled(), expect_enabled);
}
void AddOneSampleAndWait() {
// Do nothing if the test has already failed, to avoid timeouts.
ASSERT_FALSE(HasFailure());
auto* sampler = base::PoissonAllocationSampler::Get();
sampler->OnAllocation(AllocationNotificationData(
reinterpret_cast<void*>(0x1337), kAllocationSize, nullptr,
AllocationSubsystem::kManualForTesting));
task_env().RunUntilQuit();
// Free the allocation so that other tests can re-use the address.
sampler->OnFree(
FreeNotificationData(reinterpret_cast<void*>(0x1337),
AllocationSubsystem::kManualForTesting));
}
// Creates a TestCallStackProfileCollector that accepts callstacks from the
// and passes them to `collector_callback`. Returns a remote for the profiler
// to pass the callstacks to.
mojo::PendingRemote<metrics::mojom::CallStackProfileCollector>
AddTestProfileCollector(ProfileCollectorCallback collector_callback) {
mojo::PendingRemote<metrics::mojom::CallStackProfileCollector> remote;
profile_collector_receivers_.Add(
std::make_unique<TestCallStackProfileCollector>(
std::move(collector_callback)),
remote.InitWithNewPipeAndPassReceiver());
return remote;
}
ScopedCallbacks CreateScopedCallbacks(
bool expect_take_snapshot,
bool expect_sampled_profile,
bool use_other_process_callback = false) {
return ScopedCallbacks(
expect_take_snapshot, expect_sampled_profile ? 1 : 0,
use_other_process_callback,
base::BindRepeating(&HeapProfilerControllerTest::RecordSampleReceived,
base::Unretained(this)),
task_env().QuitClosure());
}
std::unique_ptr<HeapProfilerController> controller_;
base::HistogramTester histogram_tester_;
// 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;
// Receivers for callstack profiles. Each element of the set is a
// TestCallStackProfileCollecter and associated mojo::Receiver.
mojo::UniqueReceiverSet<metrics::mojom::CallStackProfileCollector>
profile_collector_receivers_;
};
// Basic tests only use the default feature params.
INSTANTIATE_TEST_SUITE_P(All,
HeapProfilerControllerTest,
Combine(Values(FeatureTestParams{}),
// No child process.
Values(ProfilerProcessType::kUnknown)));
// Sampling profiler is not capable of unwinding stack on Android under tests.
#if !BUILDFLAG(IS_ANDROID)
TEST_P(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,
ProfilerProcessType::kBrowser,
/*expect_enabled=*/true,
/*first_snapshot_callback=*/base::DoNothing(),
base::BindLambdaForTesting(check_profile));
auto* sampler = base::PoissonAllocationSampler::Get();
sampler->OnAllocation(AllocationNotificationData(
reinterpret_cast<void*>(0x1337), kAllocationSize, nullptr,
AllocationSubsystem::kManualForTesting));
sampler->OnAllocation(AllocationNotificationData(
reinterpret_cast<void*>(0x7331), kAllocationSize, nullptr,
AllocationSubsystem::kManualForTesting));
// 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_env().FastForwardBy(base::Days(1));
}
// Free all recorded memory so the address list is empty for the next test.
sampler->OnFree(FreeNotificationData(reinterpret_cast<void*>(0x1337),
AllocationSubsystem::kManualForTesting));
sampler->OnFree(FreeNotificationData(reinterpret_cast<void*>(0x7331),
AllocationSubsystem::kManualForTesting));
}
#endif
TEST_P(HeapProfilerControllerTest, UnhandledProcess) {
// Starting the heap profiler in an unhandled process type should safely do
// nothing.
StartHeapProfiling(version_info::Channel::STABLE,
ProfilerProcessType::kUnknown,
/*expect_enabled=*/false);
// 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);
}
TEST_P(HeapProfilerControllerTest, EmptyProfile) {
// Should save an empty profile even though no memory is allocated.
ScopedCallbacks callbacks = CreateScopedCallbacks(
/*expect_take_snapshot=*/true, /*expect_sampled_profile=*/true);
StartHeapProfiling(
version_info::Channel::STABLE, ProfilerProcessType::kBrowser,
/*expect_enabled=*/true, callbacks.first_snapshot_callback(),
callbacks.collector_callback());
task_env().RunUntilQuit();
EXPECT_TRUE(sample_received_);
}
// Test the feature on various channels in the browser process.
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 = HeapProfilerControllerTest;
INSTANTIATE_TEST_SUITE_P(All,
HeapProfilerControllerChannelTest,
Combine(ValuesIn(kChannelConfigs),
// No child process.
Values(ProfilerProcessType::kUnknown)));
TEST_P(HeapProfilerControllerChannelTest, StableChannel) {
const bool profiling_enabled = feature_params().feature_enabled &&
feature_params().stable.probability > 0.0;
ScopedCallbacks callbacks = CreateScopedCallbacks(
/*expect_take_snapshot=*/profiling_enabled,
feature_params().stable.expect_browser_sample);
StartHeapProfiling(version_info::Channel::STABLE,
ProfilerProcessType::kBrowser, profiling_enabled,
callbacks.first_snapshot_callback(),
callbacks.collector_callback());
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser", profiling_enabled, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
profiling_enabled, 1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, feature_params().stable.expect_browser_sample);
}
TEST_P(HeapProfilerControllerChannelTest, CanaryChannel) {
const bool profiling_enabled = feature_params().feature_enabled &&
feature_params().nonstable.probability > 0.0;
ScopedCallbacks callbacks = CreateScopedCallbacks(
/*expect_take_snapshot=*/profiling_enabled,
feature_params().nonstable.expect_browser_sample);
StartHeapProfiling(version_info::Channel::CANARY,
ProfilerProcessType::kBrowser, profiling_enabled,
callbacks.first_snapshot_callback(),
callbacks.collector_callback());
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser", profiling_enabled, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
profiling_enabled, 1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, feature_params().nonstable.expect_browser_sample);
}
TEST_P(HeapProfilerControllerChannelTest, UnknownChannel) {
// An unknown channel should be treated like stable, in case a large
// population doesn't have the channel set.
const bool profiling_enabled = feature_params().feature_enabled &&
feature_params().stable.probability > 0.0;
ScopedCallbacks callbacks = CreateScopedCallbacks(
/*expect_take_snapshot=*/profiling_enabled,
feature_params().stable.expect_browser_sample);
StartHeapProfiling(version_info::Channel::UNKNOWN,
ProfilerProcessType::kBrowser, profiling_enabled,
callbacks.first_snapshot_callback(),
callbacks.collector_callback());
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser", profiling_enabled, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
profiling_enabled, 1);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, feature_params().stable.expect_browser_sample);
}
// Test the feature in various processes on the stable channel.
constexpr FeatureTestParams kProcessConfigs[] = {
// Enabled in parent process only.
{
.stable = {.probability = 1.0,
.expect_browser_sample = true,
.expect_child_sample = false},
.gpu_snapshot_prob = 0,
.network_snapshot_prob = 0,
.renderer_snapshot_prob = 0,
.utility_snapshot_prob = 0,
},
// Enabled in child process only.
// Central control only samples child processes when the browser process is
// sampled, so no samples are expected even though sampling is supported in
// the child process.
{
.stable = {.probability = 0.0,
.expect_browser_sample = false,
.expect_child_sample = false},
.gpu_snapshot_prob = 100,
.network_snapshot_prob = 100,
.renderer_snapshot_prob = 100,
.utility_snapshot_prob = 100,
},
// Enabled in parent and child processes.
{
.stable = {.probability = 1.0,
.expect_browser_sample = true,
.expect_child_sample = true},
.gpu_snapshot_prob = 100,
.network_snapshot_prob = 100,
.renderer_snapshot_prob = 100,
.utility_snapshot_prob = 100,
},
};
using HeapProfilerControllerProcessTest = HeapProfilerControllerTest;
INSTANTIATE_TEST_SUITE_P(All,
HeapProfilerControllerProcessTest,
Combine(ValuesIn(kProcessConfigs),
Values(ProfilerProcessType::kGpu,
ProfilerProcessType::kNetworkService,
ProfilerProcessType::kRenderer,
ProfilerProcessType::kUtility,
// Include unsupported process types.
ProfilerProcessType::kUnknown)));
TEST_P(HeapProfilerControllerProcessTest, BrowserProcess) {
const bool profiling_enabled = feature_params().stable.expect_browser_sample;
ScopedCallbacks callbacks = CreateScopedCallbacks(
/*expect_take_snapshot=*/profiling_enabled,
/*expect_sampled_profile=*/profiling_enabled,
/*use_other_process_callback=*/
feature_params().stable.expect_child_sample &&
child_process_type() != ProfilerProcessType::kUnknown);
// Mock the child end of the SnapshotController mojo pipe.
MockSnapshotController mock_child_snapshot_controller;
mojo::Receiver<mojom::SnapshotController> mock_receiver(
&mock_child_snapshot_controller);
StartHeapProfiling(version_info::Channel::STABLE,
ProfilerProcessType::kBrowser, profiling_enabled,
callbacks.first_snapshot_callback(),
callbacks.collector_callback());
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Browser", profiling_enabled, 1);
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
profiling_enabled, 1);
constexpr int kTestChildProcessId = 1;
if (profiling_enabled) {
ASSERT_TRUE(controller_->GetBrowserProcessSnapshotController());
// This callback should be invoked from
// AppendCommandLineSwitchForChildProcess to bind the child end of the mojo
// pipe.
controller_->GetBrowserProcessSnapshotController()
->SetBindRemoteForChildProcessCallback(base::BindLambdaForTesting(
[&](int child_process_id,
mojo::PendingReceiver<mojom::SnapshotController> receiver) {
// Should not be called if profiling is unsupported in the child.
ASSERT_TRUE(feature_params().stable.expect_child_sample);
ASSERT_NE(child_process_type(), ProfilerProcessType::kUnknown);
EXPECT_EQ(child_process_id, kTestChildProcessId);
mock_receiver.Bind(std::move(receiver));
}));
} else {
EXPECT_FALSE(controller_->GetBrowserProcessSnapshotController());
}
// Simulate a child process launch. If profiling is enabled in both browser
// and child processes, this will bind the browser end of the mojo pipe to the
// BrowserProcessSnapshotController and use the above callback to bind the
// child end to `mock_child_snapshot_controller`.
base::CommandLine child_command_line(base::CommandLine::NO_PROGRAM);
controller_->AppendCommandLineSwitchForChildProcess(
&child_command_line, child_process_type(), kTestChildProcessId);
if (feature_params().stable.expect_child_sample &&
child_process_type() != ProfilerProcessType::kUnknown) {
EXPECT_CALL(mock_child_snapshot_controller, TakeSnapshot(100, 0))
.WillOnce([&] {
// Record that BrowserProcessSnapshotController triggered a fake
// snapshot in the child process.
callbacks.other_process_callback().Run();
});
} else {
EXPECT_CALL(mock_child_snapshot_controller, TakeSnapshot(_, _)).Times(0);
}
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, feature_params().stable.expect_browser_sample);
}
TEST_P(HeapProfilerControllerProcessTest, ChildProcess) {
// TakeSnapshot() is only called in the child process when the browser process
// triggers it. Nothing to test if sampling in the browser process is
// disabled.
if (!feature_params().stable.expect_browser_sample) {
return;
}
const bool profiling_enabled =
feature_params().stable.expect_child_sample &&
child_process_type() != ProfilerProcessType::kUnknown;
ScopedCallbacks callbacks = CreateScopedCallbacks(
/*expect_take_snapshot=*/profiling_enabled,
/*expect_sampled_profile=*/profiling_enabled,
/*use_other_process_callback=*/true);
// Simulate the browser side of child process launching.
constexpr int kTestChildProcessId = 1;
// Create a snapshot controller to hold the browser end of the mojo pipe.
auto snapshot_task_runner = base::SequencedTaskRunner::GetCurrentDefault();
auto fake_browser_snapshot_controller =
std::make_unique<BrowserProcessSnapshotController>(snapshot_task_runner);
// This callback should be invoked from AppendCommandLineSwitchForTesting to
// bind the child end of the mojo pipe.
fake_browser_snapshot_controller->SetBindRemoteForChildProcessCallback(
base::BindLambdaForTesting(
[&](int child_process_id,
mojo::PendingReceiver<mojom::SnapshotController> receiver) {
// Should not be called if profiling is unsupported in the child.
ASSERT_TRUE(feature_params().stable.expect_child_sample);
ASSERT_NE(child_process_type(), ProfilerProcessType::kUnknown);
EXPECT_EQ(child_process_id, kTestChildProcessId);
ChildProcessSnapshotController::CreateSelfOwnedReceiver(
std::move(receiver));
}));
base::test::ScopedCommandLine scoped_command_line;
HeapProfilerController::AppendCommandLineSwitchForTesting(
scoped_command_line.GetProcessCommandLine(), child_process_type(),
kTestChildProcessId, fake_browser_snapshot_controller.get());
// Simulate the browser process taking a sample after a delay.
snapshot_task_runner->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&BrowserProcessSnapshotController::TakeSnapshotsOnSnapshotSequence,
std::move(fake_browser_snapshot_controller))
.Then(callbacks.other_process_callback()),
TestTimeouts::action_timeout());
StartHeapProfiling(version_info::Channel::STABLE, child_process_type(),
profiling_enabled, callbacks.first_snapshot_callback(),
callbacks.collector_callback());
// If the child process is unknown, no histograms are logged. If it's known
// but disabled, 0 is logged.
if (child_process_type() != ProfilerProcessType::kUnknown) {
switch (child_process_type()) {
case ProfilerProcessType::kGpu:
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.GPU", profiling_enabled, 1);
break;
case ProfilerProcessType::kNetworkService:
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.NetworkService", profiling_enabled,
1);
break;
case ProfilerProcessType::kRenderer:
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Renderer", profiling_enabled, 1);
break;
case ProfilerProcessType::kUtility:
histogram_tester_.ExpectUniqueSample(
"HeapProfiling.InProcess.Enabled.Utility", profiling_enabled, 1);
break;
default:
FAIL() << "Unexpected processs type "
<< static_cast<int>(child_process_type());
}
histogram_tester_.ExpectUniqueSample("HeapProfiling.InProcess.Enabled",
profiling_enabled, 1);
}
// The child process HeapProfilerController should never have a
// BrowserProcessSnapshotController. (`fake_browser_snapshot_controller`
// simulates the browser side of the connection.)
EXPECT_EQ(controller_->GetBrowserProcessSnapshotController(), nullptr);
AddOneSampleAndWait();
EXPECT_EQ(sample_received_, profiling_enabled);
}
#if ENABLE_MULTIPROCESS_TESTS
// Returns a lambda that can be called from a GMock matcher. It will return the
// value of a MetadataItem named `name` in a given CallStackProfile, or nullopt
// if there's no metadata with that name.
auto GetProfileMetadataFunc(std::string_view name) {
auto get_metadata =
[name_hash = base::HashMetricName(name)](
const metrics::CallStackProfile& profile) -> std::optional<int64_t> {
for (int32_t i = 0; i < profile.metadata_name_hash_size(); ++i) {
if (profile.metadata_name_hash(i) == name_hash) {
// Found index of `name_hash`.
for (const auto& metadata_item : profile.profile_metadata()) {
if (metadata_item.name_hash_index() == i) {
return metadata_item.value();
}
}
}
}
// No metadata matched `name_hash`.
return std::nullopt;
};
return get_metadata;
}
// End-to-end test with multiple child processes.
constexpr const auto kMultipleChildConfigs = std::to_array<FeatureTestParams>({
{
.gpu_snapshot_prob = 100,
.network_snapshot_prob = 100,
.renderer_snapshot_prob = 66,
.utility_snapshot_prob = 50,
},
});
using HeapProfilerControllerMultipleChildTest = HeapProfilerControllerTest;
INSTANTIATE_TEST_SUITE_P(All,
HeapProfilerControllerMultipleChildTest,
Combine(ValuesIn(kMultipleChildConfigs),
// Children are manually specified.
Values(ProfilerProcessType::kUnknown)));
MULTIPROCESS_TEST_MAIN(HeapProfilerControllerChildMain) {
MultiprocessTestChild child(kMultipleChildConfigs[0].GetEnabledFeatures(),
kMultipleChildConfigs[0].GetDisabledFeatures());
child.RunTestInChild();
return ::testing::Test::HasFailure();
}
TEST_P(HeapProfilerControllerMultipleChildTest, EndToEnd) {
// Initialize mojo IPC support.
mojo::core::ScopedIPCSupport enable_mojo(
base::SingleThreadTaskRunner::GetCurrentDefault(),
mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);
// Process types to test. Each will make a different
// number of memory allocations so their reports are all different.
const std::vector<std::pair<ProfilerProcessType, size_t>> kProcessesToTest{
{ProfilerProcessType::kBrowser, 0},
{ProfilerProcessType::kGpu, 1},
// 2 utility processes.
{ProfilerProcessType::kUtility, 2},
{ProfilerProcessType::kUtility, 3},
// 5 renderer processes including one with no samples. The first one will
// be ignored to simulate the embedder refusing to profile it.
{ProfilerProcessType::kRenderer, 10},
{ProfilerProcessType::kRenderer, 0},
{ProfilerProcessType::kRenderer, 4},
{ProfilerProcessType::kRenderer, 5},
{ProfilerProcessType::kRenderer, 6},
};
// Expect only 1 utility process and 3 renderer processes to be sampled due
// to the "renderer-prob" and "utility-prob" params.
constexpr size_t kExpectedSampledProfiles =
/*browser*/ 1 + /*gpu*/ 1 + /*utility*/ 1 + /*renderer*/ 3;
// Create callbacks that store profiles from all processes in a vector.
std::vector<metrics::SampledProfile> received_profiles;
auto collector_callback = base::BindLambdaForTesting(
[&](base::TimeTicks, metrics::SampledProfile profile) {
received_profiles.push_back(std::move(profile));
});
ScopedCallbacks callbacks(
/*expect_take_snapshot=*/true, kExpectedSampledProfiles,
/*use_other_process_callback=*/false, std::move(collector_callback),
task_env().QuitClosure());
// Snapshots from the children take real time to be passed back to the parent.
// The mock clock will advance to the next snapshot time while waiting, so
// stop profiling after the first snapshot by deleting the controller.
auto stop_after_first_snapshot_callback =
callbacks.first_snapshot_callback().Then(base::BindLambdaForTesting(
[this, task_runner = base::SequencedTaskRunner::GetCurrentDefault()] {
task_runner->DeleteSoon(FROM_HERE, controller_.release());
}));
CreateHeapProfiler(
version_info::Channel::STABLE, ProfilerProcessType::kBrowser,
/*expect_enabled=*/true, std::move(stop_after_first_snapshot_callback),
callbacks.collector_callback());
ASSERT_TRUE(controller_);
// Start all processes in `kProcessesToTest` except the browser.
MultiprocessTestParent test_parent;
test_parent.StartHeapProfilingWhenChildrenConnected(
kProcessesToTest.size() - 1, controller_.get());
// On every process launch, create a TestCallStackProfileCollector to collect
// profiles from the child. BrowserProcessSnapshotController will create a
// SnapshotController to trigger snapshots in the child.
auto* browser_snapshot_controller =
controller_->GetBrowserProcessSnapshotController();
ASSERT_TRUE(browser_snapshot_controller);
auto binder_callback = base::BindLambdaForTesting(
[&](int id, mojo::PendingReceiver<mojom::SnapshotController> receiver) {
mojo::PendingRemote<metrics::mojom::CallStackProfileCollector> remote =
AddTestProfileCollector(callbacks.collector_callback());
test_parent.BindTestConnector(id, std::move(receiver),
std::move(remote));
});
browser_snapshot_controller->SetBindRemoteForChildProcessCallback(
std::move(binder_callback));
bool renderer_was_skipped = false;
for (const auto [process_type, num_allocations] : kProcessesToTest) {
if (process_type != ProfilerProcessType::kBrowser) {
// Skip the first renderer.
bool should_profile = true;
if (process_type == ProfilerProcessType::kRenderer &&
!renderer_was_skipped) {
should_profile = false;
renderer_was_skipped = true;
}
test_parent.LaunchTestChild(controller_.get(), process_type,
num_allocations, should_profile);
}
}
// Loop until all children are connected and all processes send snapshots.
task_env().RunUntilQuit();
// GMock matcher that tests that the given CallStackProfile contains `count`
// stack samples with metadata containing `process_percent` and
// "process_index" < `sampled_processes`.
auto call_stack_profile_matches = [](size_t count, int64_t process_percent,
int64_t sampled_processes) {
using StackSample = metrics::CallStackProfile::StackSample;
return AllOf(
Property(
"stack_sample", &metrics::CallStackProfile::stack_sample,
Conditional(
count > 0,
// The test makes allocations at addresses without symbols, so
// they're all counted in the same stack frame.
ElementsAre(AllOf(Property("count", &StackSample::count, count),
Property("weight", &StackSample::weight,
count * kAllocationSize))),
// No allocations means no stack frames.
IsEmpty())),
ResultOf("process_percent metadata",
GetProfileMetadataFunc("process_percent"),
Optional(process_percent)),
ResultOf("process_index metadata",
GetProfileMetadataFunc("process_index"),
// Processes can be sampled in any order, so just check the
// range of "process_index".
Optional(AllOf(Ge(0), Lt(sampled_processes)))));
};
// GMock matcher that tests that the given SampledProfile is a heap snapshot
// for the given `process_type` containing `count` stack samples with metadata
// containing `process_percent` and "process_index" < `sampled_processes`.
auto sampled_profile_matches = [&](metrics::Process process_type,
size_t count, int64_t process_percent,
int64_t sampled_processes) {
return AllOf(
Property("process", &metrics::SampledProfile::process, process_type),
Property("call_stack_profile",
&metrics::SampledProfile::call_stack_profile,
call_stack_profile_matches(count, process_percent,
sampled_processes)));
};
// Only the first 1/2 of utility processes and 2/3 of renderers should be
// included due to sampling. Renderers are rounded up to 3 of the 4 that can
// be profiled - the 5th is invisible to the profiler.
EXPECT_THAT(
received_profiles,
UnorderedElementsAre(
sampled_profile_matches(metrics::Process::BROWSER_PROCESS, 0, 100, 1),
sampled_profile_matches(metrics::Process::GPU_PROCESS, 1, 100, 1),
sampled_profile_matches(metrics::Process::UTILITY_PROCESS, 2, 50, 1),
// The first renderer should be skipped.
sampled_profile_matches(metrics::Process::RENDERER_PROCESS, 0, 66, 3),
sampled_profile_matches(metrics::Process::RENDERER_PROCESS, 4, 66, 3),
sampled_profile_matches(metrics::Process::RENDERER_PROCESS, 5, 66,
3)));
// Make sure both per-process and aggregate profiler stats are logged.
// Subprocess metrics aren't hooked up in this test, so `histogram_tester_`
// only sees the browser process histograms.
histogram_tester_.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot.Browser", 1);
histogram_tester_.ExpectTotalCount(
"HeapProfiling.InProcess.SamplesPerSnapshot", 1);
}
#endif // ENABLE_MULTIPROCESS_TESTS
} // namespace
} // namespace heap_profiling