// Copyright 2016 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/metrics/content/subprocess_metrics_provider.h"

#include <utility>

#include "base/debug/leak_annotations.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/persistent_histogram_allocator.h"
#include "base/metrics/persistent_memory_allocator.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/metrics/metrics_features.h"
#include "content/public/browser/browser_child_process_host.h"
#include "content/public/browser/browser_child_process_host_iterator.h"
#include "content/public/browser/child_process_data.h"

namespace metrics {
namespace {

SubprocessMetricsProvider* g_subprocess_metrics_provider = nullptr;

scoped_refptr<base::TaskRunner> CreateTaskRunner() {
  // This task runner must block shutdown to ensure metrics are not lost.
  return base::ThreadPool::CreateTaskRunner(
      {base::TaskPriority::BEST_EFFORT,
       base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
}

}  // namespace

// static
bool SubprocessMetricsProvider::CreateInstance() {
  if (g_subprocess_metrics_provider) {
    return false;
  }
  g_subprocess_metrics_provider = new SubprocessMetricsProvider();
  ANNOTATE_LEAKING_OBJECT_PTR(g_subprocess_metrics_provider);
  return true;
}

// static
SubprocessMetricsProvider* SubprocessMetricsProvider::GetInstance() {
  return g_subprocess_metrics_provider;
}

// static
void SubprocessMetricsProvider::MergeHistogramDeltasForTesting(
    bool async,
    base::OnceClosure done_callback) {
  GetInstance()->MergeHistogramDeltas(async, std::move(done_callback));
}

SubprocessMetricsProvider::RefCountedAllocator::RefCountedAllocator(
    std::unique_ptr<base::PersistentHistogramAllocator> allocator)
    : allocator_(std::move(allocator)) {
  CHECK(allocator_);
}

SubprocessMetricsProvider::RefCountedAllocator::~RefCountedAllocator() =
    default;

SubprocessMetricsProvider::SubprocessMetricsProvider()
    : task_runner_(CreateTaskRunner()) {
  base::StatisticsRecorder::RegisterHistogramProvider(
      weak_ptr_factory_.GetWeakPtr());
  content::BrowserChildProcessObserver::Add(this);
  g_subprocess_metrics_provider = this;

  // Ensure no child processes currently exist so that we do not miss any.
  CHECK(content::RenderProcessHost::AllHostsIterator().IsAtEnd());
  CHECK(content::BrowserChildProcessHostIterator().Done());
}

SubprocessMetricsProvider::~SubprocessMetricsProvider() {
  // This object should never be deleted since it is leaky.
  NOTREACHED();
}

void SubprocessMetricsProvider::RegisterSubprocessAllocator(
    int id,
    std::unique_ptr<base::PersistentHistogramAllocator> allocator) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  CHECK(allocator);

  // Pass a custom RangesManager so that we do not register the BucketRanges
  // with the global StatisticsRecorder when creating histogram objects using
  // the allocator's underlying data. This avoids unnecessary contention on the
  // global StatisticsRecorder lock.
  // Note: Since |allocator| may be merged from different threads concurrently,
  // for example on the UI thread and on a background thread, we must use
  // ThreadSafeRangesManager.
  allocator->SetRangesManager(new base::ThreadSafeRangesManager());
  // Insert the allocator into the internal map and verify that there was no
  // allocator with the same ID already.
  auto result = allocators_by_id_.emplace(
      id, base::MakeRefCounted<RefCountedAllocator>(std::move(allocator)));
  CHECK(result.second);
}

void SubprocessMetricsProvider::DeregisterSubprocessAllocator(int id) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  auto it = allocators_by_id_.find(id);
  if (it == allocators_by_id_.end()) {
    return;
  }

  // Extract the matching allocator from the list of active ones. It is possible
  // that a background task is currently holding a reference to it. Removing it
  // from the internal map is fine though, as it is refcounted.
  scoped_refptr<RefCountedAllocator> allocator = std::move(it->second);
  allocators_by_id_.erase(it);
  CHECK(allocator);

  // Merge the last deltas from the allocator before releasing the ref (and
  // deleting if the last one).
  auto* allocator_ptr = allocator.get();
  task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(
          &SubprocessMetricsProvider::MergeHistogramDeltasFromAllocator, id,
          // Unretained is needed to pass a refcounted class as a raw pointer.
          // It is safe because it is kept alive by the reply task.
          base::Unretained(allocator_ptr)),
      base::BindOnce(
          &SubprocessMetricsProvider::OnMergeHistogramDeltasFromAllocator,
          std::move(allocator)));
}

void SubprocessMetricsProvider::MergeHistogramDeltas(
    bool async,
    base::OnceClosure done_callback) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  if (async) {
    // Make a copy of the internal allocators map (with its own refptrs) to pass
    // to the background task.
    auto allocators = std::make_unique<AllocatorByIdMap>(allocators_by_id_);
    auto* allocators_ptr = allocators.get();

    // This is intentionally not posted to |task_runner_| because not running
    // this task does not imply data loss, so no point in blocking shutdown
    // (hence CONTINUE_ON_SHUTDOWN). However, there might be some contention on
    // the StatisticsRecorder between this task and those posted to
    // |task_runner_|.
    base::ThreadPool::PostTaskAndReply(
        FROM_HERE,
        {base::TaskPriority::BEST_EFFORT,
         base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
        base::BindOnce(&MergeHistogramDeltasFromAllocators, allocators_ptr),
        base::BindOnce(
            &SubprocessMetricsProvider::OnMergeHistogramDeltasFromAllocators,
            std::move(allocators), std::move(done_callback)));
  } else {
    MergeHistogramDeltasFromAllocators(&allocators_by_id_);
    std::move(done_callback).Run();
  }
}

void SubprocessMetricsProvider::BrowserChildProcessLaunchedAndConnected(
    const content::ChildProcessData& data) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // See if the new process has a memory allocator and take control of it if so.
  // This call can only be made on the browser's IO thread.
  content::BrowserChildProcessHost* host =
      content::BrowserChildProcessHost::FromID(data.id);
  // |host| should not be null, but such cases have been observed in the wild so
  // gracefully handle this scenario.
  if (!host) {
    return;
  }

  std::unique_ptr<base::PersistentMemoryAllocator> allocator =
      host->TakeMetricsAllocator();
  // The allocator can be null in tests.
  if (!allocator)
    return;

  RegisterSubprocessAllocator(
      data.id, std::make_unique<base::PersistentHistogramAllocator>(
                   std::move(allocator)));
}

void SubprocessMetricsProvider::BrowserChildProcessHostDisconnected(
    const content::ChildProcessData& data) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DeregisterSubprocessAllocator(data.id);
}

void SubprocessMetricsProvider::BrowserChildProcessCrashed(
    const content::ChildProcessData& data,
    const content::ChildProcessTerminationInfo& info) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DeregisterSubprocessAllocator(data.id);
}

void SubprocessMetricsProvider::BrowserChildProcessKilled(
    const content::ChildProcessData& data,
    const content::ChildProcessTerminationInfo& info) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DeregisterSubprocessAllocator(data.id);
}

void SubprocessMetricsProvider::OnRenderProcessHostCreated(
    content::RenderProcessHost* host) {
  // Sometimes, the same host will cause multiple notifications in tests so
  // could possibly do the same in a release build.
  if (!scoped_observations_.IsObservingSource(host))
    scoped_observations_.AddObservation(host);
}

void SubprocessMetricsProvider::RenderProcessReady(
    content::RenderProcessHost* host) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // If the render-process-host passed a persistent-memory-allocator to the
  // renderer process, extract it and register it here.
  std::unique_ptr<base::PersistentMemoryAllocator> allocator =
      host->TakeMetricsAllocator();
  if (allocator) {
    RegisterSubprocessAllocator(
        host->GetID(), std::make_unique<base::PersistentHistogramAllocator>(
                           std::move(allocator)));
  }
}

void SubprocessMetricsProvider::RenderProcessExited(
    content::RenderProcessHost* host,
    const content::ChildProcessTerminationInfo& info) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  DeregisterSubprocessAllocator(host->GetID());
}

void SubprocessMetricsProvider::RenderProcessHostDestroyed(
    content::RenderProcessHost* host) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // It's possible for a Renderer to terminate without RenderProcessExited
  // (above) being called so it's necessary to de-register also upon the
  // destruction of the host. If both get called, no harm is done.

  DeregisterSubprocessAllocator(host->GetID());
  scoped_observations_.RemoveObservation(host);
}

void SubprocessMetricsProvider::RecreateTaskRunnerForTesting() {
  task_runner_ = CreateTaskRunner();
}

// static
void SubprocessMetricsProvider::MergeHistogramDeltasFromAllocator(
    int id,
    RefCountedAllocator* allocator) {
  DCHECK(allocator);

  int histogram_count = 0;
  base::PersistentHistogramAllocator* allocator_ptr = allocator->allocator();
  base::PersistentHistogramAllocator::Iterator hist_iter(allocator_ptr);
  while (true) {
    std::unique_ptr<base::HistogramBase> histogram = hist_iter.GetNext();
    if (!histogram) {
      break;
    }
    allocator_ptr->MergeHistogramDeltaToStatisticsRecorder(histogram.get());
    ++histogram_count;
  }

  DVLOG(1) << "Reported " << histogram_count << " histograms from subprocess #"
           << id;
}

// static
void SubprocessMetricsProvider::MergeHistogramDeltasFromAllocators(
    AllocatorByIdMap* allocators) {
  for (const auto& iter : *allocators) {
    MergeHistogramDeltasFromAllocator(iter.first, iter.second.get());
  }
}

// static
void SubprocessMetricsProvider::OnMergeHistogramDeltasFromAllocator(
    scoped_refptr<RefCountedAllocator> allocator) {
  // This method does nothing except have ownership on |allocator|. When this
  // method exits, |allocator| will be released (unless there are background
  // tasks currently holding references).
}

// static
void SubprocessMetricsProvider::OnMergeHistogramDeltasFromAllocators(
    std::unique_ptr<AllocatorByIdMap> allocators,
    base::OnceClosure done_callback) {
  std::move(done_callback).Run();
  // When this method exits, |allocators| will be released. It's possible some
  // allocators are from subprocesses that have already been deregistered, so
  // they will also be released here (assuming no other background tasks
  // currently hold references).
}

}  // namespace metrics
