blob: ca5dd80d8b1dbc3a794659a24db5581de9bf8670 [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 <cmath>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "base/allocator/dispatcher/reentry_guard.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/metrics_hashes.h"
#include "base/notreached.h"
#include "base/numerics/clamped_math.h"
#include "base/profiler/frame.h"
#include "base/profiler/metadata_recorder.h"
#include "base/profiler/module_cache.h"
#include "base/rand_util.h"
#include "base/sampling_heap_profiler/poisson_allocation_sampler.h"
#include "base/sampling_heap_profiler/sampling_heap_profiler.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "components/heap_profiling/in_process/browser_process_snapshot_controller.h"
#include "components/heap_profiling/in_process/heap_profiler_parameters.h"
#include "components/heap_profiling/in_process/switches.h"
#include "components/metrics/call_stacks/call_stack_profile_builder.h"
#include "components/sampling_profiler/process_type.h"
#include "components/services/heap_profiling/public/cpp/merge_samples.h"
#include "components/variations/variations_switches.h"
#include "components/version_info/channel.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
namespace heap_profiling {
namespace {
using ProcessType = sampling_profiler::ProfilerProcessType;
// The heap profiler for this process. HeapProfilerController will set this on
// creation, and reset it to nullptr on destruction, so that it's always unset
// after each unit test that creates a HeapProfilerController.
HeapProfilerController* g_instance = nullptr;
base::TimeDelta RandomInterval(base::TimeDelta mean) {
// Time intervals between profile collections form a Poisson stream with
// given mean interval.
double rnd = base::RandDouble();
if (rnd == 0) {
// log(0) is an error.
rnd = std::numeric_limits<double>::min();
}
return -std::log(rnd) * mean;
}
// Returns true iff `process_type` is handled by ProcessHistogramName.
bool HasProcessHistogramName(ProcessType process_type) {
switch (process_type) {
case ProcessType::kBrowser:
case ProcessType::kRenderer:
case ProcessType::kGpu:
case ProcessType::kUtility:
case ProcessType::kNetworkService:
return true;
case ProcessType::kUnknown:
default:
// Profiler should not be enabled for these process types.
return false;
}
}
// Returns the full name of a histogram to record by appending the
// ProfiledProcess variant name for `process_type` (defined in
// tools/metrics/histograms/metadata/memory/histograms.xml) to `base_name`.
// Returns `base_name` unchanged if `process_type` is nullopt.
std::string ProcessHistogramName(std::string_view base_name,
std::optional<ProcessType> process_type) {
if (!process_type.has_value()) {
return std::string(base_name);
}
switch (process_type.value()) {
case ProcessType::kBrowser:
return base::StrCat({base_name, ".Browser"});
case ProcessType::kRenderer:
return base::StrCat({base_name, ".Renderer"});
case ProcessType::kGpu:
return base::StrCat({base_name, ".GPU"});
case ProcessType::kUtility:
return base::StrCat({base_name, ".Utility"});
case ProcessType::kNetworkService:
return base::StrCat({base_name, ".NetworkService"});
case ProcessType::kUnknown:
default:
// Profiler should not be enabled for these process types.
NOTREACHED();
}
}
double GetChannelProbability(version_info::Channel channel) {
switch (channel) {
case version_info::Channel::STABLE:
case version_info::Channel::UNKNOWN:
// If the channel can't be determined, treat it as `stable` for safety.
// Don't disable heap profiling completely so that developers can still
// enable it with --enable-feature flags.
return kStableProbability.Get();
case version_info::Channel::BETA:
case version_info::Channel::DEV:
case version_info::Channel::CANARY:
return kNonStableProbability.Get();
}
NOTREACHED();
}
// Returns true iff heap profiles should be collected for this process, along
// with a name for a synthetic field trial group based on the decision or
// nullopt if no group applies.
std::pair<bool, std::optional<std::string>> DecideIfCollectionIsEnabled(
version_info::Channel channel,
ProcessType process_type) {
// Check the feature before the process type so that users are assigned to
// groups in the browser process.
if (process_type != ProcessType::kBrowser) {
// The browser process decided whether profiling is enabled and used
// AppendCommandLineSwitchForChildProcess() to pass on the decision.
const bool is_enabled = base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kSubprocessHeapProfiling);
return {is_enabled, std::nullopt};
}
// Never profile during benchmarking.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
variations::switches::kEnableBenchmarking)) {
return {false, std::nullopt};
}
if (!base::FeatureList::IsEnabled(kHeapProfilerReporting)) {
return {false, std::nullopt};
}
// Randomly determine whether profiling is enabled.
const double seed = base::RandDouble();
const double probability = GetChannelProbability(channel);
if (seed < probability) {
return {true, "Enabled"};
}
if (seed < 2 * probability && 2 * probability <= 1.0) {
// Only register a Control group if it can be the same size as Enabled.
return {false, "Control"};
}
return {false, "Default"};
}
// Logs statistics about the sampling profiler.
void LogProfilerStats(std::optional<ProcessType> process_type,
const base::PoissonAllocationSamplerStats& profiler_stats,
size_t num_samples) {
const double hit_rate =
profiler_stats.address_cache_hits
? (static_cast<double>(profiler_stats.address_cache_hits) /
(profiler_stats.address_cache_hits +
profiler_stats.address_cache_misses))
: 0.0;
base::UmaHistogramCounts100000(
ProcessHistogramName("HeapProfiling.InProcess.SamplesPerSnapshot",
process_type),
num_samples);
base::UmaHistogramCounts1M(
ProcessHistogramName(
"HeapProfiling.InProcess.SampledAddressCacheHitCount", process_type),
profiler_stats.address_cache_hits);
base::UmaHistogramCounts10000(
ProcessHistogramName("HeapProfiling.InProcess.SampledAddressCacheHitRate",
process_type),
hit_rate * 10000);
base::UmaHistogramCounts1M(
ProcessHistogramName("HeapProfiling.InProcess.SampledAddressCacheMaxSize",
process_type),
profiler_stats.address_cache_max_size);
base::UmaHistogramPercentage(
ProcessHistogramName(
"HeapProfiling.InProcess.SampledAddressCacheMaxLoadFactor",
process_type),
100 * profiler_stats.address_cache_max_load_factor);
for (size_t bucket_length :
profiler_stats.address_cache_bucket_stats.lengths) {
base::UmaHistogramCounts100(
ProcessHistogramName(
"HeapProfiling.InProcess.SampledAddressCacheBucketLengths",
process_type),
bucket_length);
}
// Expected to cluster around 100% - target range is around 95% to 105%.
base::UmaHistogramCustomCounts(
ProcessHistogramName(
"HeapProfiling.InProcess.SampledAddressCacheUniformity",
process_type),
100 * profiler_stats.address_cache_bucket_stats.chi_squared, 0, 200, 50);
}
// Retrieves a snapshot from the SamplingHeapProfiler and logs metrics about
// profiler performance.
std::vector<base::SamplingHeapProfiler::Sample> RetrieveAndLogSnapshot(
ProcessType process_type) {
auto samples = base::SamplingHeapProfiler::Get()->GetSamples(0);
const base::PoissonAllocationSamplerStats profiler_stats =
base::PoissonAllocationSampler::Get()->GetAndResetStats();
LogProfilerStats(process_type, profiler_stats, samples.size());
// Also summarize over all process types.
LogProfilerStats(std::nullopt, profiler_stats, samples.size());
return samples;
}
} // namespace
HeapProfilerController::SnapshotParams::SnapshotParams(
std::optional<base::TimeDelta> mean_interval,
bool use_random_interval,
scoped_refptr<StoppedFlag> stopped,
ProcessType process_type,
base::TimeTicks profiler_creation_time,
base::OnceClosure on_first_snapshot_callback)
: mean_interval(std::move(mean_interval)),
use_random_interval(use_random_interval),
stopped(std::move(stopped)),
process_type(process_type),
profiler_creation_time(profiler_creation_time),
on_first_snapshot_callback(std::move(on_first_snapshot_callback)) {}
HeapProfilerController::SnapshotParams::SnapshotParams(
scoped_refptr<StoppedFlag> stopped,
ProcessType process_type,
base::TimeTicks profiler_creation_time,
uint32_t process_probability_pct,
size_t process_index,
base::OnceClosure on_first_snapshot_callback)
: stopped(std::move(stopped)),
process_type(process_type),
profiler_creation_time(profiler_creation_time),
process_probability_pct(process_probability_pct),
process_index(process_index),
on_first_snapshot_callback(std::move(on_first_snapshot_callback)) {}
HeapProfilerController::SnapshotParams::~SnapshotParams() = default;
HeapProfilerController::SnapshotParams::SnapshotParams(SnapshotParams&& other) =
default;
HeapProfilerController::SnapshotParams&
HeapProfilerController::SnapshotParams::operator=(SnapshotParams&& other) =
default;
// static
HeapProfilerController* HeapProfilerController::GetInstance() {
return g_instance;
}
HeapProfilerController::HeapProfilerController(version_info::Channel channel,
ProcessType process_type)
: process_type_(process_type),
stopped_(base::MakeRefCounted<StoppedFlag>()),
snapshot_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT})) {
// Only one HeapProfilerController should exist at a time in each
// process.
CHECK(!g_instance);
g_instance = this;
std::tie(profiling_enabled_, synthetic_field_trial_group_) =
DecideIfCollectionIsEnabled(channel, process_type);
// Before starting the profiler, record the ReentryGuard's TLS slot to a crash
// key to debug reentry into the profiler.
// TODO(crbug.com/40062835): Remove this after diagnosing reentry crashes.
base::allocator::dispatcher::ReentryGuard::RecordTLSSlotToCrashKey();
if (profiling_enabled_ && process_type_ == ProcessType::kBrowser) {
browser_process_snapshot_controller_ =
std::make_unique<BrowserProcessSnapshotController>(
snapshot_task_runner_);
}
}
HeapProfilerController::~HeapProfilerController() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
stopped_->data.Set();
CHECK_EQ(g_instance, this);
g_instance = nullptr;
// BrowserProcessSnapshotController must be deleted on the sequence that its
// WeakPtr's are bound to.
if (browser_process_snapshot_controller_) {
snapshot_task_runner_->DeleteSoon(
FROM_HERE, std::move(browser_process_snapshot_controller_));
}
}
bool HeapProfilerController::StartIfEnabled() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Only supported processes are assigned a patterned histogram.
if (HasProcessHistogramName(process_type_)) {
constexpr char kEnabledHistogramName[] = "HeapProfiling.InProcess.Enabled";
base::UmaHistogramBoolean(
ProcessHistogramName(kEnabledHistogramName, process_type_),
profiling_enabled_);
// Also summarize over all supported process types.
base::UmaHistogramBoolean(kEnabledHistogramName, profiling_enabled_);
}
if (!profiling_enabled_) {
return false;
}
const size_t sampling_rate_bytes = GetSamplingRateForProcess(process_type_);
if (sampling_rate_bytes > 0) {
base::SamplingHeapProfiler::Get()->SetSamplingInterval(sampling_rate_bytes);
}
const float hash_set_load_factor =
GetHashSetLoadFactorForProcess(process_type_);
if (hash_set_load_factor > 0) {
base::PoissonAllocationSampler::Get()->SetTargetHashSetLoadFactor(
hash_set_load_factor);
}
base::SamplingHeapProfiler::Get()->Start();
if (process_type_ != ProcessType::kBrowser) {
// ChildProcessSnapshotController will trigger snapshots.
return true;
}
const base::TimeDelta collection_interval = kCollectionInterval.Get();
CHECK(collection_interval.is_positive());
SnapshotParams params(
collection_interval,
/*use_random_interval=*/!suppress_randomness_for_testing_, stopped_,
process_type_, creation_time_, std::move(on_first_snapshot_callback_));
params.trigger_child_process_snapshot_closure = base::BindRepeating(
&BrowserProcessSnapshotController::TakeSnapshotsOnSnapshotSequence,
browser_process_snapshot_controller_->GetWeakPtr());
snapshot_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&HeapProfilerController::ScheduleNextSnapshot,
std::move(params)));
return true;
}
bool HeapProfilerController::GetSyntheticFieldTrial(
std::string& trial_name,
std::string& group_name) const {
CHECK_EQ(process_type_, ProcessType::kBrowser);
if (!synthetic_field_trial_group_.has_value()) {
return false;
}
trial_name = "SyntheticHeapProfilingConfiguration";
group_name = synthetic_field_trial_group_.value();
return true;
}
void HeapProfilerController::SuppressRandomnessForTesting() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
suppress_randomness_for_testing_ = true;
if (browser_process_snapshot_controller_) {
browser_process_snapshot_controller_
->SuppressRandomnessForTesting(); // IN-TEST
}
}
void HeapProfilerController::SetFirstSnapshotCallbackForTesting(
base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
on_first_snapshot_callback_ = std::move(callback);
}
void HeapProfilerController::AppendCommandLineSwitchForChildProcess(
base::CommandLine* command_line,
ProcessType child_process_type,
int child_process_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_EQ(process_type_, ProcessType::kBrowser);
// If profiling is disabled in the browser process, pass a null
// BrowserProcessSnapshotController to disable it in the child process too.
BrowserProcessSnapshotController* snapshot_controller = nullptr;
if (profiling_enabled_) {
CHECK(browser_process_snapshot_controller_);
snapshot_controller = browser_process_snapshot_controller_.get();
}
AppendCommandLineSwitchInternal(command_line, child_process_type,
child_process_id, snapshot_controller);
}
BrowserProcessSnapshotController*
HeapProfilerController::GetBrowserProcessSnapshotController() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return browser_process_snapshot_controller_.get();
}
void HeapProfilerController::TakeSnapshotInChildProcess(
base::PassKey<ChildProcessSnapshotController>,
uint32_t process_probability_pct,
size_t process_index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_NE(process_type_, ProcessType::kBrowser);
snapshot_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&TakeSnapshot,
SnapshotParams(stopped_, process_type_, creation_time_,
process_probability_pct, process_index,
std::move(on_first_snapshot_callback_))));
}
void HeapProfilerController::LogMetricsWithoutSnapshotInChildProcess(
base::PassKey<ChildProcessSnapshotController>) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_NE(process_type_, ProcessType::kBrowser);
snapshot_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
[](ProcessType process_type, scoped_refptr<StoppedFlag> stopped) {
if (!stopped->data.IsSet()) {
// Log metrics about the snapshot, but don't upload it to UMA.
RetrieveAndLogSnapshot(process_type);
}
},
process_type_, stopped_));
}
// static
void HeapProfilerController::AppendCommandLineSwitchForTesting(
base::CommandLine* command_line,
ProcessType child_process_type,
int child_process_id,
BrowserProcessSnapshotController* snapshot_controller) {
AppendCommandLineSwitchInternal(command_line, child_process_type,
child_process_id, snapshot_controller);
}
// static
void HeapProfilerController::AppendCommandLineSwitchInternal(
base::CommandLine* command_line,
ProcessType child_process_type,
int child_process_id,
BrowserProcessSnapshotController* snapshot_controller) {
CHECK_NE(child_process_type, ProcessType::kBrowser);
if (snapshot_controller &&
GetSnapshotProbabilityForProcess(child_process_type) > 0) {
command_line->AppendSwitch(switches::kSubprocessHeapProfiling);
snapshot_controller->BindRemoteForChildProcess(child_process_id,
child_process_type);
}
}
// static
void HeapProfilerController::ScheduleNextSnapshot(SnapshotParams params) {
// Should only be called for repeating snapshots.
CHECK(params.mean_interval.has_value());
base::TimeDelta interval = params.use_random_interval
? RandomInterval(params.mean_interval.value())
: params.mean_interval.value();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&HeapProfilerController::TakeSnapshot, std::move(params)),
interval);
}
// static
void HeapProfilerController::TakeSnapshot(SnapshotParams params) {
if (params.stopped->data.IsSet()) {
return;
}
if (!params.on_first_snapshot_callback.is_null()) {
std::move(params.on_first_snapshot_callback).Run();
}
RetrieveAndSendSnapshot(
params.process_type,
base::TimeTicks::Now() - params.profiler_creation_time,
params.process_probability_pct, params.process_index);
if (params.process_type == ProcessType::kBrowser) {
// Also trigger snapshots in child processes.
params.trigger_child_process_snapshot_closure.Run();
}
if (params.mean_interval.has_value()) {
// Callback should be left as null for next snapshot.
CHECK(params.on_first_snapshot_callback.is_null());
ScheduleNextSnapshot(std::move(params));
}
}
// static
void HeapProfilerController::RetrieveAndSendSnapshot(
ProcessType process_type,
base::TimeDelta time_since_profiler_creation,
uint32_t process_probability_pct,
size_t process_index) {
using Sample = base::SamplingHeapProfiler::Sample;
CHECK_GT(process_probability_pct, 0u);
CHECK_LE(process_probability_pct, 100u);
// Always log the total sampled memory before returning. If `samples` is empty
// this will be logged as 0 MB.
base::ClampedNumeric<uint64_t> total_sampled_bytes;
absl::Cleanup log_total_sampled_memory = [&total_sampled_bytes, process_type,
process_probability_pct] {
// Scale this processes' memory by the inverse of the probability that it
// was chosen to get its estimated contribution to the total memory.
constexpr int kBytesPerMB = 1024 * 1024;
const int scaled_sampled_memory =
total_sampled_bytes / kBytesPerMB * (100.0 / process_probability_pct);
base::UmaHistogramMemoryLargeMB(
ProcessHistogramName("HeapProfiling.InProcess.TotalSampledMemory",
process_type),
scaled_sampled_memory);
// Also summarize over all process types.
base::UmaHistogramMemoryLargeMB(
"HeapProfiling.InProcess.TotalSampledMemory", scaled_sampled_memory);
};
std::vector<Sample> samples = RetrieveAndLogSnapshot(process_type);
base::ModuleCache module_cache;
sampling_profiler::CallStackProfileParams params(
process_type, sampling_profiler::ProfilerThreadType::kUnknown,
sampling_profiler::CallStackProfileParams::Trigger::
kPeriodicHeapCollection,
time_since_profiler_creation);
metrics::CallStackProfileBuilder profile_builder(params);
SampleMap merged_samples = MergeSamples(samples);
for (auto& pair : merged_samples) {
const Sample& sample = pair.first;
const SampleValue& value = pair.second;
const size_t stack_size = sample.stack.size();
std::vector<base::Frame> frames;
frames.reserve(stack_size);
for (const void* frame : sample.stack) {
const uintptr_t address = reinterpret_cast<const uintptr_t>(frame);
const base::ModuleCache::Module* module =
module_cache.GetModuleForAddress(address);
frames.emplace_back(address, module);
}
// Heap "samples" represent allocation stacks aggregated over time so
// do not have a meaningful timestamp.
profile_builder.OnSampleCompleted(std::move(frames), base::TimeTicks(),
value.total, value.count);
total_sampled_bytes += value.total;
}
// Initialize on first call since HashMetricName isn't constexpr.
static const uint64_t kProcessPercentHash =
base::HashMetricName("process_percent"); // 0xd598e4d0a9e55408
static const uint64_t kProcessIndexHash =
base::HashMetricName("process_index"); // 0x28f4372e67b3f8f8
profile_builder.AddProfileMetadata(
base::MetadataRecorder::Item(kProcessPercentHash, std::nullopt,
std::nullopt, process_probability_pct));
profile_builder.AddProfileMetadata(base::MetadataRecorder::Item(
kProcessIndexHash, std::nullopt, std::nullopt, process_index));
profile_builder.OnProfileCompleted(base::TimeDelta(), base::TimeDelta());
}
} // namespace heap_profiling