// Copyright 2022 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/performance_manager/public/metrics/page_resource_monitor.h"

#include <stdint.h>

#include <optional>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/scoped_observation.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/resource_attribution/cpu_proportion_tracker.h"
#include "components/performance_manager/public/resource_attribution/resource_types.h"
#include "components/system_cpu/cpu_probe.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"

namespace performance_manager::metrics {

namespace {

using system_cpu::CpuProbe;
using system_cpu::CpuSample;
using PageMeasurementBackgroundState =
    PageResourceMonitor::PageMeasurementBackgroundState;

using PageContext = resource_attribution::PageContext;
using QueryResultMap = resource_attribution::QueryResultMap;
using ResourceContext = resource_attribution::ResourceContext;
using ResourceType = resource_attribution::ResourceType;

// CPU usage metrics are provided as a double in the [0.0, number of cores *
// 100.0] range. The CPU usage is usually below 1%, so the UKM is
// reported out of 10,000 instead of out of 100 to make analyzing the data
// easier. This is the same scale factor used by the
// PerformanceMonitor.AverageCPU10 histograms recorded in
// chrome/browser/metrics/power/process_metrics_recorder_util.cc.
constexpr int kCPUUsageFactor = 100 * 100;

// The time between calls to OnResourceUsageUpdated()
constexpr base::TimeDelta kCollectionDelay = base::Minutes(2);

PageMeasurementBackgroundState GetBackgroundStateForMeasurementPeriod(
    const PageNode* page_node,
    base::TimeTicks now,
    base::TimeTicks time_of_last_resource_usage) {
  if (time_of_last_resource_usage < page_node->GetLastVisibilityChangeTime()) {
    return PageMeasurementBackgroundState::kMixedForegroundBackground;
  }
  if (page_node->IsVisible()) {
    return PageMeasurementBackgroundState::kForeground;
  }
  // Check if the page was audible for the entire measurement period.
  const base::TimeDelta time_since_last_resource_usage =
      now - time_of_last_resource_usage;
  if (page_node->GetTimeSinceLastAudibleChange().value_or(
          base::TimeDelta::Max()) < time_since_last_resource_usage) {
    return PageMeasurementBackgroundState::kBackgroundMixedAudible;
  }
  if (page_node->IsAudible()) {
    return PageMeasurementBackgroundState::kAudibleInBackground;
  }
  return PageMeasurementBackgroundState::kBackground;
}

resource_attribution::QueryBuilder CPUQueryBuilder() {
  resource_attribution::QueryBuilder builder;
  builder.AddAllContextsOfType<PageContext>().AddResourceType(
      ResourceType::kCPUTime);
  return builder;
}

const PageNode* PageNodeFromContext(const ResourceContext& context) {
  // The query returned by CPUQueryBuilder() should only measure PageContexts.
  // AsContext() asserts that `context` is a PageContext.
  return resource_attribution::AsContext<PageContext>(context).GetPageNode();
}

bool ContextIsTab(const ResourceContext& context) {
  const PageNode* page_node = PageNodeFromContext(context);
  return page_node && page_node->GetType() == PageType::kTab;
}

}  // namespace

class PageResourceMonitor::CPUResultConverter {
 public:
  // A callback that's invoked with the converted results.
  using ResultCallback = base::OnceCallback<void(const PageCPUUsageMap&,
                                                 std::optional<CpuSample>)>;

  explicit CPUResultConverter(std::unique_ptr<CpuProbe> system_cpu_probe);
  ~CPUResultConverter() = default;

  base::WeakPtr<CPUResultConverter> GetWeakPtr();

  bool HasSystemCPUProbe() const;

  // Invokes `result_callback_` with the converted `results`.
  void OnResourceUsageUpdated(ResultCallback result_callback,
                              const QueryResultMap& results);

 private:
  void StartFirstInterval(base::TimeTicks time, const QueryResultMap& results);
  void StartNextInterval(ResultCallback result_callback,
                         base::TimeTicks time,
                         const QueryResultMap& results,
                         std::optional<CpuSample> system_cpu);

  std::unique_ptr<CpuProbe> system_cpu_probe_;
  resource_attribution::CPUProportionTracker proportion_tracker_;
  base::WeakPtrFactory<CPUResultConverter> weak_factory_{this};
};

PageResourceMonitor::PageResourceMonitor(bool enable_system_cpu_probe)
    : resource_query_(CPUQueryBuilder()
                          .AddResourceType(ResourceType::kMemorySummary)
                          .CreateScopedQuery()) {
  query_observation_.Observe(&resource_query_);
  resource_query_.Start(kCollectionDelay);
  cpu_result_converter_ = std::make_unique<CPUResultConverter>(
      enable_system_cpu_probe ? CpuProbe::Create() : nullptr);
}

PageResourceMonitor::~PageResourceMonitor() = default;

void PageResourceMonitor::OnResourceUsageUpdated(
    const QueryResultMap& results) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  cpu_result_converter_->OnResourceUsageUpdated(
      base::BindOnce(&PageResourceMonitor::OnPageResourceUsageResult,
                     weak_factory_.GetWeakPtr(), results),
      results);
}

base::TimeDelta PageResourceMonitor::GetCollectionDelayForTesting() const {
  return kCollectionDelay;
}

void PageResourceMonitor::OnPageResourceUsageResult(
    const QueryResultMap& results,
    const PageCPUUsageMap& page_cpu_usage,
    std::optional<CpuSample> system_cpu) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Calculate the overall CPU usage.
  double total_cpu_usage = 0;
  for (const auto& [page_context, cpu_usage] : page_cpu_usage) {
    total_cpu_usage += cpu_usage;
  }

  // Contexts in `page_cpu_usage` are a subset of contexts in `results`.
  const auto now = base::TimeTicks::Now();
  for (const auto& [page_context, result] : results) {
    const PageNode* page_node = PageNodeFromContext(page_context);
    if (!page_node) {
      // Page was deleted while waiting for system CPU. Nothing to log.
      continue;
    }
    if (page_node->GetType() != PageType::kTab) {
      continue;
    }
    const ukm::SourceId source_id = page_node->GetUkmSourceID();
    auto ukm = ukm::builders::PerformanceManager_PageResourceUsage2(source_id);
    ukm.SetBackgroundState(
        static_cast<int64_t>(GetBackgroundStateForMeasurementPeriod(
            page_node, now, time_of_last_resource_usage_)));
    ukm.SetMeasurementAlgorithm(
        static_cast<int64_t>(PageMeasurementAlgorithm::kEvenSplitAndAggregate));
    // Add CPU usage, if this page included it.
    const auto it = page_cpu_usage.find(page_context);
    if (it != page_cpu_usage.end()) {
      ukm.SetRecentCPUUsage(kCPUUsageFactor * it->second);
      ukm.SetTotalRecentCPUUsageAllPages(kCPUUsageFactor * total_cpu_usage);
    }
    // Add memory summary, if this page included it.
    if (result.memory_summary_result.has_value()) {
      ukm.SetResidentSetSizeEstimate(
          result.memory_summary_result->resident_set_size.InKiB());
      ukm.SetPrivateFootprintEstimate(
          result.memory_summary_result->private_footprint.InKiB());
    }
    ukm.Record(ukm::UkmRecorder::Get());
  }

  time_of_last_resource_usage_ = now;
}

PageResourceMonitor::CPUResultConverter::CPUResultConverter(
    std::unique_ptr<CpuProbe> system_cpu_probe)
    : system_cpu_probe_(std::move(system_cpu_probe)),
      // Only calculate results for tabs, not extensions.
      proportion_tracker_(base::BindRepeating(&ContextIsTab)) {
  CPUQueryBuilder().QueryOnce(
      base::BindOnce(&CPUResultConverter::StartFirstInterval, GetWeakPtr(),
                     base::TimeTicks::Now()));
  if (system_cpu_probe_) {
    system_cpu_probe_->StartSampling();
  }
}

base::WeakPtr<PageResourceMonitor::CPUResultConverter>
PageResourceMonitor::CPUResultConverter::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

bool PageResourceMonitor::CPUResultConverter::HasSystemCPUProbe() const {
  return static_cast<bool>(system_cpu_probe_);
}

void PageResourceMonitor::CPUResultConverter::OnResourceUsageUpdated(
    CPUResultConverter::ResultCallback result_callback,
    const QueryResultMap& results) {
  auto next_update_callback = base::BindOnce(
      &CPUResultConverter::StartNextInterval, GetWeakPtr(),
      std::move(result_callback), base::TimeTicks::Now(), results);
  if (system_cpu_probe_) {
    system_cpu_probe_->RequestSample(std::move(next_update_callback));
  } else {
    std::move(next_update_callback).Run(std::nullopt);
  }
}

void PageResourceMonitor::CPUResultConverter::StartFirstInterval(
    base::TimeTicks time,
    const QueryResultMap& results) {
  proportion_tracker_.StartFirstInterval(time, results);
}

void PageResourceMonitor::CPUResultConverter::StartNextInterval(
    CPUResultConverter::ResultCallback result_callback,
    base::TimeTicks time,
    const QueryResultMap& results,
    std::optional<CpuSample> system_cpu) {
  std::move(result_callback)
      .Run(proportion_tracker_.StartNextInterval(time, results),
           std::move(system_cpu));
}

}  // namespace performance_manager::metrics
