| // 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 <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/allocator/dispatcher/reentry_guard.h" |
| #include "base/check.h" |
| #include "base/debug/stack_trace.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/numerics/clamped_math.h" |
| #include "base/profiler/module_cache.h" |
| #include "base/rand_util.h" |
| #include "base/sampling_heap_profiler/sampling_heap_profiler.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_piece.h" |
| #include "base/task/thread_pool.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/services/heap_profiling/public/cpp/merge_samples.h" |
| #include "components/version_info/channel.h" |
| #include "third_party/abseil-cpp/absl/cleanup/cleanup.h" |
| |
| namespace heap_profiling { |
| |
| namespace { |
| |
| using ProfilingEnabled = HeapProfilerController::ProfilingEnabled; |
| |
| // Records whether heap profiling is enabled for this process. |
| // HeapProfilerController will set this on creation, and reset it to |
| // kNoController on destruction, so that it's always reset to the default |
| // state after each unit test that creates a HeapProfilerController. |
| ProfilingEnabled g_profiling_enabled = ProfilingEnabled::kNoController; |
| |
| using ProcessType = metrics::CallStackProfileParams::Process; |
| |
| 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`. |
| std::string ProcessHistogramName(base::StringPiece base_name, |
| ProcessType process_type) { |
| switch (process_type) { |
| 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(); |
| return std::string(); |
| } |
| } |
| |
| ProfilingEnabled DecideIfCollectionIsEnabled(version_info::Channel channel, |
| ProcessType process_type) { |
| HeapProfilerParameters params = |
| GetHeapProfilerParametersForProcess(process_type); |
| if (!params.is_supported) |
| return ProfilingEnabled::kDisabled; |
| const double probability = (channel == version_info::Channel::STABLE) |
| ? params.stable_probability |
| : params.nonstable_probability; |
| if (base::RandDouble() >= probability) |
| return ProfilingEnabled::kDisabled; |
| return ProfilingEnabled::kEnabled; |
| } |
| |
| // Records a time histogram for the `interval` between snapshots, using the |
| // appropriate histogram buckets for the platform (desktop or mobile). |
| // `recording_time` must be one of the {RecordingTime} token variants in the |
| // definition of HeapProfiling.InProcess.SnapshotInterval.{Platform}. |
| // {RecordingTime} in tools/metrics/histograms/metadata/memory/histograms.xml. |
| void RecordUmaSnapshotInterval(base::TimeDelta interval, |
| base::StringPiece recording_time, |
| ProcessType process_type) { |
| #if BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID) |
| // On mobile, the interval is distributed around a mean of 30 minutes. |
| constexpr base::TimeDelta kMinHistogramTime = base::Seconds(30); |
| constexpr base::TimeDelta kMaxHistogramTime = base::Hours(3); |
| constexpr const char* const kPlatform = "Mobile"; |
| #else |
| // On desktop, the interval is distributed around a mean of 1 day. |
| constexpr base::TimeDelta kMinHistogramTime = base::Minutes(30); |
| constexpr base::TimeDelta kMaxHistogramTime = base::Days(6); |
| constexpr const char* const kPlatform = "Desktop"; |
| #endif |
| |
| const auto base_name = |
| base::StrCat({"HeapProfiling.InProcess.SnapshotInterval.", kPlatform, ".", |
| recording_time}); |
| base::UmaHistogramCustomTimes(ProcessHistogramName(base_name, process_type), |
| interval, kMinHistogramTime, kMaxHistogramTime, |
| 50); |
| // Also summarize over all process types. |
| base::UmaHistogramCustomTimes(base_name, interval, kMinHistogramTime, |
| kMaxHistogramTime, 50); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Records metrics about the quality of each stack that is sampled. |
| class StackQualityMetricsRecorder { |
| public: |
| StackQualityMetricsRecorder(ProcessType process_type, |
| base::ModuleCache& module_cache) |
| : process_type_(process_type), |
| chrome_module_(GetCurrentModule(module_cache)) {} |
| |
| // Records that a new stack is being processed. |
| void NewStack(size_t stack_size) { |
| stack_size_ = stack_size; |
| num_non_chrome_frames_ = 0; |
| } |
| |
| // Records that a frame was found in `module` in the ModuleCache. |
| void AddFrameInModule(const base::ModuleCache::Module* module) { |
| // If the chrome module couldn't be found, record all frames as non-chrome. |
| if (!chrome_module_ || !module || |
| module->GetBaseAddress() != chrome_module_->GetBaseAddress()) { |
| num_non_chrome_frames_ += 1; |
| } |
| } |
| |
| // Records summary metrics through UMA. |
| void RecordUmaMetrics() { |
| // From inspecting reports received on Android, most reports with only 1 to |
| // 3 frames are clearly truncated, suggesting a problem with the unwinder, |
| // or contain a JNI base call that directly allocates. (These are not broken |
| // but don't have anything actionable in them.) Reports with 4 frames are |
| // more likely to be useful but still have a large proportion of truncated |
| // or non-actionable stacks. With 5 or more frames the stacks are more |
| // likely than not to be actionable. |
| constexpr size_t kMinFramesForGoodQuality = 5; |
| |
| const bool has_few_frames = stack_size_ < kMinFramesForGoodQuality; |
| base::UmaHistogramBoolean("HeapProfiling.InProcess.AndroidShortStacks", |
| has_few_frames); |
| base::UmaHistogramBoolean( |
| ProcessHistogramName("HeapProfiling.InProcess.AndroidShortStacks", |
| process_type_), |
| has_few_frames); |
| |
| if (stack_size_ > 0) { |
| const double non_chrome_frame_percent = |
| 100.0 * num_non_chrome_frames_ / stack_size_; |
| base::UmaHistogramPercentage( |
| "HeapProfiling.InProcess.AndroidNonChromeFrames", |
| non_chrome_frame_percent); |
| base::UmaHistogramPercentage( |
| ProcessHistogramName("HeapProfiling.InProcess.AndroidNonChromeFrames", |
| process_type_), |
| non_chrome_frame_percent); |
| } |
| } |
| |
| private: |
| static const base::ModuleCache::Module* GetCurrentModule( |
| base::ModuleCache& module_cache) { |
| // Get the address of the current function. |
| const uintptr_t address = reinterpret_cast<const uintptr_t>( |
| &StackQualityMetricsRecorder::GetCurrentModule); |
| return module_cache.GetModuleForAddress(address); |
| } |
| |
| ProcessType process_type_; |
| raw_ptr<const base::ModuleCache::Module> chrome_module_; |
| size_t stack_size_ = 0; |
| size_t num_non_chrome_frames_ = 0; |
| }; |
| #else |
| // No-op implementation of StackQualityMetricsRecorder. |
| class StackQualityMetricsRecorder { |
| public: |
| StackQualityMetricsRecorder(ProcessType, base::ModuleCache&) {} |
| void NewStack(size_t) {} |
| void AddFrameInModule(const base::ModuleCache::Module*) {} |
| void RecordUmaMetrics() {} |
| }; |
| #endif |
| |
| } // namespace |
| |
| HeapProfilerController::SnapshotParams::SnapshotParams( |
| base::TimeDelta mean_interval, |
| bool use_random_interval, |
| scoped_refptr<StoppedFlag> stopped, |
| ProcessType process_type, |
| base::TimeTicks profiler_creation_time) |
| : mean_interval(mean_interval), |
| use_random_interval(use_random_interval), |
| stopped(std::move(stopped)), |
| process_type(process_type), |
| profiler_creation_time(profiler_creation_time) {} |
| |
| HeapProfilerController::SnapshotParams::~SnapshotParams() = default; |
| |
| HeapProfilerController::SnapshotParams::SnapshotParams(SnapshotParams&& other) = |
| default; |
| |
| HeapProfilerController::SnapshotParams& |
| HeapProfilerController::SnapshotParams::operator=(SnapshotParams&& other) = |
| default; |
| |
| // static |
| ProfilingEnabled HeapProfilerController::GetProfilingEnabled() { |
| return g_profiling_enabled; |
| } |
| |
| HeapProfilerController::HeapProfilerController(version_info::Channel channel, |
| ProcessType process_type) |
| : process_type_(process_type), |
| stopped_(base::MakeRefCounted<StoppedFlag>()) { |
| // Only one HeapProfilerController should exist at a time in each |
| // process. The class is not a singleton so it can be created and |
| // destroyed in tests. |
| DCHECK_EQ(g_profiling_enabled, ProfilingEnabled::kNoController); |
| g_profiling_enabled = 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/1411454): Remove this after diagnosing reentry crashes. |
| base::allocator::dispatcher::ReentryGuard::RecordTLSSlotToCrashKey(); |
| } |
| |
| HeapProfilerController::~HeapProfilerController() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| stopped_->data.Set(); |
| g_profiling_enabled = ProfilingEnabled::kNoController; |
| } |
| |
| bool HeapProfilerController::StartIfEnabled() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| const bool profiling_enabled = |
| g_profiling_enabled == ProfilingEnabled::kEnabled; |
| // 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; |
| HeapProfilerParameters profiler_params = |
| GetHeapProfilerParametersForProcess(process_type_); |
| // DecideIfCollectionIsEnabled() should return false if not supported. |
| DCHECK(profiler_params.is_supported); |
| if (profiler_params.sampling_rate_bytes > 0) { |
| base::SamplingHeapProfiler::Get()->SetSamplingInterval( |
| profiler_params.sampling_rate_bytes); |
| } |
| base::SamplingHeapProfiler::Get()->Start(); |
| DCHECK(profiler_params.collection_interval.is_positive()); |
| SnapshotParams params( |
| profiler_params.collection_interval, |
| /*use_random_interval=*/!suppress_randomness_for_testing_, stopped_, |
| process_type_, creation_time_); |
| ScheduleNextSnapshot(std::move(params)); |
| return true; |
| } |
| |
| void HeapProfilerController::SuppressRandomnessForTesting() { |
| suppress_randomness_for_testing_ = true; |
| } |
| |
| // static |
| void HeapProfilerController::ScheduleNextSnapshot(SnapshotParams params) { |
| base::TimeDelta interval = params.use_random_interval |
| ? RandomInterval(params.mean_interval) |
| : params.mean_interval; |
| RecordUmaSnapshotInterval(interval, "Scheduled", params.process_type); |
| base::ThreadPool::PostDelayedTask( |
| FROM_HERE, {base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(&HeapProfilerController::TakeSnapshot, std::move(params), |
| /*previous_interval=*/interval), |
| interval); |
| } |
| |
| // static |
| void HeapProfilerController::TakeSnapshot(SnapshotParams params, |
| base::TimeDelta previous_interval) { |
| if (params.stopped->data.IsSet()) |
| return; |
| RecordUmaSnapshotInterval(previous_interval, "Taken", params.process_type); |
| RetrieveAndSendSnapshot( |
| params.process_type, |
| base::TimeTicks::Now() - params.profiler_creation_time); |
| ScheduleNextSnapshot(std::move(params)); |
| } |
| |
| // static |
| void HeapProfilerController::RetrieveAndSendSnapshot( |
| ProcessType process_type, |
| base::TimeDelta time_since_profiler_creation) { |
| using Sample = base::SamplingHeapProfiler::Sample; |
| |
| // 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] { |
| constexpr int kBytesPerMB = 1024 * 1024; |
| base::UmaHistogramMemoryLargeMB( |
| ProcessHistogramName("HeapProfiling.InProcess.TotalSampledMemory", |
| process_type), |
| base::ClampDiv(total_sampled_bytes, kBytesPerMB)); |
| }; |
| |
| std::vector<Sample> samples = |
| base::SamplingHeapProfiler::Get()->GetSamples(0); |
| base::UmaHistogramCounts100000( |
| ProcessHistogramName("HeapProfiling.InProcess.SamplesPerSnapshot", |
| process_type), |
| samples.size()); |
| // Also summarize over all process types. |
| base::UmaHistogramCounts100000("HeapProfiling.InProcess.SamplesPerSnapshot", |
| samples.size()); |
| if (samples.empty()) |
| return; |
| |
| base::ModuleCache module_cache; |
| metrics::CallStackProfileParams params( |
| process_type, metrics::CallStackProfileParams::Thread::kUnknown, |
| metrics::CallStackProfileParams::Trigger::kPeriodicHeapCollection, |
| time_since_profiler_creation); |
| metrics::CallStackProfileBuilder profile_builder(params); |
| |
| SampleMap merged_samples = MergeSamples(samples); |
| |
| StackQualityMetricsRecorder quality_recorder(process_type, module_cache); |
| 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); |
| |
| quality_recorder.NewStack(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); |
| quality_recorder.AddFrameInModule(module); |
| frames.emplace_back(address, module); |
| } |
| quality_recorder.RecordUmaMetrics(); |
| |
| // 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; |
| } |
| |
| profile_builder.OnProfileCompleted(base::TimeDelta(), base::TimeDelta()); |
| } |
| |
| } // namespace heap_profiling |