// 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/base_switches.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/lock_free_address_hash_set.h"
#include "base/sampling_heap_profiler/lock_free_bloom_filter.h"
#include "base/sampling_heap_profiler/poisson_allocation_sampler.h"
#include "base/sampling_heap_profiler/sampling_heap_churn_profiler.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/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(
          ::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,
                      size_t num_samples,
                      base::ByteSize expected_sampling_interval) {
  base::UmaHistogramCounts100000(
      ProcessHistogramName("HeapProfiling.InProcess.SamplesPerSnapshot",
                           process_type),
      num_samples);

  CHECK(expected_sampling_interval.is_positive());
  const size_t actual_interval =
      base::PoissonAllocationSampler::Get()->SamplingInterval();
  const int ratio_pct = std::round(actual_interval * 100.0 /
                                   expected_sampling_interval.InBytesF());
  base::UmaHistogramCounts10000(
      ProcessHistogramName("HeapProfiling.InProcess.SamplingIntervalVariance",
                           process_type),
      ratio_pct);
}

// Retrieves a snapshot from the SamplingHeapProfiler and logs metrics about
// profiler performance.
std::vector<base::SamplingHeapProfiler::Sample> RetrieveAndLogSnapshot(
    ProcessType process_type,
    base::ByteSize expected_sampling_interval) {
  auto samples = base::SamplingHeapProfiler::Get()->GetSamples(std::nullopt);
  LogProfilerStats(process_type, samples.size(), expected_sampling_interval);
  // Also summarize over all process types.
  LogProfilerStats(std::nullopt, samples.size(), expected_sampling_interval);
  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::ByteSize expected_sampling_interval,
    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),
      expected_sampling_interval(expected_sampling_interval),
      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::ByteSize expected_sampling_interval,
    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),
      expected_sampling_interval(expected_sampling_interval),
      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;

  if (profiling_session_) {
    base::SamplingHeapProfiler::Get()->Stop(*profiling_session_);
    if (base::FeatureList::IsEnabled(kHeapProfilerChurnReporting)) {
      base::SamplingHeapProfiler::Get()->churn_profiler().Stop();
    }
  }

  // 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) {
    return false;
  }
  expected_sampling_rate_ = base::ByteSize(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);
  }
  profiling_session_ = base::SamplingHeapProfiler::Get()->Start(
      expected_sampling_rate_,
      base::SamplingHeapProfiler::Priority::kBackground);
  if (base::FeatureList::IsEnabled(kHeapProfilerChurnReporting)) {
    auto& churn_profiler = base::SamplingHeapProfiler::Get()->churn_profiler();
    churn_profiler.SetSubsamplingChance(kHeapChurnSubsamplingChance.Get());
    churn_profiler.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_, expected_sampling_rate_,
      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);
  SnapshotParams params(stopped_, process_type_, creation_time_,
                        process_probability_pct, process_index,
                        expected_sampling_rate_,
                        std::move(on_first_snapshot_callback_));
  snapshot_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&TakeSnapshot, std::move(params)));
}

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,
             base::ByteSize expected_sampling_interval) {
            if (!stopped->data.IsSet()) {
              // Log metrics about the snapshot, but don't upload it to UMA.
              RetrieveAndLogSnapshot(process_type, expected_sampling_interval);
              std::ignore = base::SamplingHeapProfiler::Get()
                                ->churn_profiler()
                                .TakeSamples();
            }
          },
          process_type_, stopped_, expected_sampling_rate_));
}

// 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,
      params.expected_sampling_interval);
  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,
    base::ByteSize expected_sampling_interval) {
  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, expected_sampling_interval);

  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);
    }

    std::optional<size_t> merged_resident_bytes;
    if (value.total > 0) {
      merged_resident_bytes = value.resident_total;
    }

    // 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,
                                      merged_resident_bytes);

    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
  static const uint64_t kSamplingIntervalHash =
      base::HashMetricName("sampling_interval");

  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.AddProfileMetadata(base::MetadataRecorder::Item(
      kSamplingIntervalHash, std::nullopt, std::nullopt,
      expected_sampling_interval.InBytes()));

  profile_builder.OnProfileCompleted(base::TimeDelta(), base::TimeDelta());

  std::vector<Sample> churn_samples =
      base::SamplingHeapProfiler::Get()->churn_profiler().TakeSamples();
  if (!churn_samples.empty()) {
    sampling_profiler::CallStackProfileParams churn_params(
        process_type, sampling_profiler::ProfilerThreadType::kUnknown,
        sampling_profiler::CallStackProfileParams::Trigger::
            kPeriodicHeapChurnCollection,
        time_since_profiler_creation);
    metrics::CallStackProfileBuilder churn_profile_builder(churn_params);

    SampleMap merged_churn_samples = MergeSamples(churn_samples);
    for (auto& pair : merged_churn_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);
      }
      churn_profile_builder.OnSampleCompleted(
          std::move(frames), base::TimeTicks(), value.total, value.count);
    }
    churn_profile_builder.AddProfileMetadata(
        base::MetadataRecorder::Item(kProcessPercentHash, std::nullopt,
                                     std::nullopt, process_probability_pct));
    churn_profile_builder.AddProfileMetadata(base::MetadataRecorder::Item(
        kProcessIndexHash, std::nullopt, std::nullopt, process_index));
    churn_profile_builder.AddProfileMetadata(base::MetadataRecorder::Item(
        kSamplingIntervalHash, std::nullopt, std::nullopt,
        expected_sampling_interval.InBytes()));
    churn_profile_builder.OnProfileCompleted(base::TimeDelta(),
                                             base::TimeDelta());
  }
}

}  // namespace heap_profiling
