// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/tracing/public/cpp/background_tracing/background_tracing_rule.h"

#include <limits>
#include <optional>
#include <string>
#include <type_traits>

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram.h"
#include "base/metrics/metrics_hashes.h"
#include "base/metrics/statistics_recorder.h"
#include "base/rand_util.h"
#include "base/strings/safe_sprintf.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/histogram_scope.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/trace_id_helper.h"
#include "base/unguessable_token.h"
#include "services/tracing/public/cpp/background_tracing/tracing_agent_observer_manager.h"
#include "services/tracing/public/cpp/perfetto/macros.h"
#include "services/tracing/public/mojom/background_tracing_agent.mojom.h"
#include "third_party/perfetto/protos/perfetto/trace/track_event/chrome_histogram_sample.pbzero.h"

namespace tracing {

BackgroundTracingRule::BackgroundTracingRule() = default;

BackgroundTracingRule::~BackgroundTracingRule() {
  DCHECK(!installed());
}

void BackgroundTracingRule::Install(RuleTriggeredCallback trigger_callback) {
  DCHECK(!installed());
  installed_ = true;
  trigger_callback_ = std::move(trigger_callback);
  if (activation_delay_) {
    activation_timer_.Start(FROM_HERE, *activation_delay_,
                            base::BindOnce(&BackgroundTracingRule::DoInstall,
                                           base::Unretained(this)));
  } else {
    DoInstall();
  }
}

void BackgroundTracingRule::Uninstall() {
  if (!installed()) {
    return;
  }
  installed_ = false;
  trigger_timer_.Stop();
  activation_timer_.Stop();
  trigger_callback_.Reset();
  DoUninstall();
}

bool BackgroundTracingRule::OnNamedTrigger(std::optional<int32_t> value,
                                           uint64_t flow_id) {
  return OnRuleTriggered(value, flow_id);
}

bool BackgroundTracingRule::OnRuleTriggered(std::optional<int32_t> value,
                                            uint64_t flow_id) {
  if (!installed()) {
    return false;
  }
  DCHECK(trigger_callback_);
  if (trigger_chance_ < 1.0 && base::RandDouble() > trigger_chance_) {
    return false;
  }
  triggered_value_ = value;
  flow_id_ = flow_id;
  if (delay_) {
    trigger_timer_.Start(FROM_HERE, *delay_,
                         base::BindOnce(base::IgnoreResult(trigger_callback_),
                                        base::Unretained(this)));
    return true;
  } else {
    return trigger_callback_.Run(this);
  }
}

std::string BackgroundTracingRule::GetDefaultRuleName() const {
  return "trigger";
}

perfetto::protos::gen::TriggerRule
BackgroundTracingRule::ToProtoForTesting()  // IN-TEST
    const {
  perfetto::protos::gen::TriggerRule config;
  if (trigger_chance_ < 1.0) {
    config.set_trigger_chance(trigger_chance_);
  }

  if (delay_) {
    config.set_delay_ms(delay_->InMilliseconds());
  }

  config.set_name(rule_name_);

  return config;
}

void BackgroundTracingRule::GenerateMetadataProto(
    BackgroundTracingRule::MetadataProto* out) const {
  uint32_t name_hash = base::HashFieldTrialName(rule_name());
  out->set_name_hash(name_hash);
}

void BackgroundTracingRule::Setup(
    const perfetto::protos::gen::TriggerRule& config) {
  if (config.has_trigger_chance()) {
    trigger_chance_ = config.trigger_chance();
  }
  if (config.has_delay_ms()) {
    delay_ = base::Milliseconds(config.delay_ms());
  }
  if (config.has_activation_delay_ms()) {
    activation_delay_ = base::Milliseconds(config.activation_delay_ms());
  }
  if (config.has_name()) {
    rule_name_ = config.name();
  } else {
    rule_name_ = base::StrCat(
        {"org.chromium.background_tracing.", GetDefaultRuleName()});
  }
}

namespace {

class NamedTriggerRule : public BackgroundTracingRule {
 private:
  explicit NamedTriggerRule(const std::string& named_event)
      : named_event_(named_event) {}

 public:
  static std::unique_ptr<BackgroundTracingRule> Create(
      const perfetto::protos::gen::TriggerRule& config) {
    if (config.has_manual_trigger_name()) {
      return base::WrapUnique<BackgroundTracingRule>(
          new NamedTriggerRule(config.manual_trigger_name()));
    }
    return nullptr;
  }

  void DoInstall() override {
    base::trace_event::NamedTriggerManager::GetInstance()->AddObserver(
        named_event_, this);
  }

  void DoUninstall() override {
    base::trace_event::NamedTriggerManager::GetInstance()->RemoveObserver(
        named_event_, this);
  }

  perfetto::protos::gen::TriggerRule ToProtoForTesting()  // IN-TEST
      const override {
    perfetto::protos::gen::TriggerRule config =
        BackgroundTracingRule::ToProtoForTesting();  // IN-TEST
    config.set_manual_trigger_name(named_event_);
    return config;
  }

  void GenerateMetadataProto(
      BackgroundTracingRule::MetadataProto* out) const override {
    DCHECK(out);
    BackgroundTracingRule::GenerateMetadataProto(out);
    out->set_trigger_type(MetadataProto::MONITOR_AND_DUMP_WHEN_TRIGGER_NAMED);
  }

 protected:
  std::string GetDefaultRuleName() const override { return named_event_; }

 private:
  std::string named_event_;
};

class HistogramRule : public BackgroundTracingRule,
                      public TracingAgentObserverManager::AgentObserver {
 private:
  HistogramRule(const std::string& histogram_name,
                int histogram_lower_value,
                int histogram_upper_value)
      : histogram_name_(histogram_name),
        rule_id_(base::UnguessableToken::Create().ToString()),
        histogram_lower_value_(histogram_lower_value),
        histogram_upper_value_(histogram_upper_value) {}

 public:
  static std::unique_ptr<BackgroundTracingRule> Create(
      const perfetto::protos::gen::TriggerRule& config) {
    DCHECK(config.has_histogram());

    if (!config.histogram().has_histogram_name()) {
      return nullptr;
    }
    int histogram_lower_value = 0;
    if (config.histogram().has_min_value()) {
      histogram_lower_value = config.histogram().min_value();
    }
    int histogram_upper_value = std::numeric_limits<int>::max();
    if (config.histogram().has_max_value()) {
      histogram_upper_value = config.histogram().max_value();
    }
    if (histogram_lower_value > histogram_upper_value) {
      return nullptr;
    }

    return base::WrapUnique(
        new HistogramRule(config.histogram().histogram_name(),
                          histogram_lower_value, histogram_upper_value));
  }

  ~HistogramRule() override = default;

  // BackgroundTracingRule implementation
  void DoInstall() override {
    histogram_sample_callback_.emplace(
        histogram_name_,
        base::BindRepeating(&HistogramRule::OnHistogramChangedCallback,
                            base::Unretained(this), histogram_lower_value_,
                            histogram_upper_value_));
    base::trace_event::NamedTriggerManager::GetInstance()->AddObserver(rule_id_,
                                                                       this);
    if (auto* manager = TracingAgentObserverManager::GetInstance()) {
      manager->AddAgentObserver(this);
    }
  }

  void DoUninstall() override {
    histogram_sample_callback_.reset();
    if (auto* manager = TracingAgentObserverManager::GetInstance()) {
      manager->RemoveAgentObserver(this);
    }
    base::trace_event::NamedTriggerManager::GetInstance()->RemoveObserver(
        rule_id_, this);
  }

  perfetto::protos::gen::TriggerRule ToProtoForTesting()  // IN-TEST
      const override {
    perfetto::protos::gen::TriggerRule config =
        BackgroundTracingRule::ToProtoForTesting();  // IN-TEST
    auto* histogram = config.mutable_histogram();
    histogram->set_histogram_name(histogram_name_);
    histogram->set_min_value(histogram_lower_value_);
    histogram->set_max_value(histogram_upper_value_);
    return config;
  }

  void GenerateMetadataProto(
      BackgroundTracingRule::MetadataProto* out) const override {
    DCHECK(out);
    BackgroundTracingRule::GenerateMetadataProto(out);
    out->set_trigger_type(
        MetadataProto::MONITOR_AND_DUMP_WHEN_SPECIFIC_HISTOGRAM_AND_VALUE);
    auto* rule = out->set_histogram_rule();
    rule->set_histogram_name_hash(base::HashMetricName(histogram_name_));
    rule->set_histogram_min_trigger(histogram_lower_value_);
    rule->set_histogram_max_trigger(histogram_upper_value_);
  }

  // TracingAgentObserverManager::AgentObserver implementation
  void OnAgentAdded(tracing::mojom::BackgroundTracingAgent* agent) override {
    agent->SetUMACallback(tracing::mojom::BackgroundTracingRule::New(rule_id_),
                          histogram_name_, histogram_lower_value_,
                          histogram_upper_value_);
  }

  void OnAgentRemoved(tracing::mojom::BackgroundTracingAgent* agent) override {
    agent->ClearUMACallback(
        tracing::mojom::BackgroundTracingRule::New(rule_id_));
  }

  void OnHistogramChangedCallback(
      base::Histogram::Sample32 reference_lower_value,
      base::Histogram::Sample32 reference_upper_value,
      std::optional<uint64_t> event_id,
      std::string_view histogram_name,
      uint64_t name_hash,
      base::Histogram::Sample32 actual_value) {
    DCHECK_EQ(histogram_name, histogram_name_);
    if (reference_lower_value > actual_value ||
        reference_upper_value < actual_value) {
      return;
    }

    uint64_t flow_id =
        event_id.value_or(base::trace_event::GetNextGlobalTraceId());

    // Add the histogram name and its corresponding value to the trace.
    const auto trace_details = [&](perfetto::EventContext& ctx) {
      perfetto::protos::pbzero::ChromeHistogramSample* new_sample =
          ctx.event()->set_chrome_histogram_sample();
      new_sample->set_name_hash(base::HashMetricName(histogram_name));
      new_sample->set_sample(actual_value);
      perfetto::Flow::Global(flow_id)(ctx);
    };
    auto track = perfetto::NamedTrack("HistogramSamples");
    TRACE_EVENT_INSTANT("tracing.background", "HistogramSampleTrigger", track,
                        trace_details);
    OnRuleTriggered(actual_value, flow_id);
  }

 protected:
  std::string GetDefaultRuleName() const override { return histogram_name_; }

 private:
  std::string histogram_name_;
  std::string rule_id_;
  int histogram_lower_value_;
  int histogram_upper_value_;
  std::optional<base::StatisticsRecorder::ScopedHistogramSampleObserver>
      histogram_sample_callback_;
};

class TimerRule : public BackgroundTracingRule {
 private:
  explicit TimerRule() = default;

 public:
  static std::unique_ptr<BackgroundTracingRule> Create(
      const perfetto::protos::gen::TriggerRule& config) {
    return base::WrapUnique<TimerRule>(new TimerRule());
  }

  void DoInstall() override {
    OnRuleTriggered(std::nullopt, base::trace_event::GetNextGlobalTraceId());
  }
  void DoUninstall() override {}

  void GenerateMetadataProto(
      BackgroundTracingRule::MetadataProto* out) const override {
    DCHECK(out);
    BackgroundTracingRule::GenerateMetadataProto(out);
    out->set_trigger_type(MetadataProto::TRIGGER_UNSPECIFIED);
  }

 protected:
  std::string GetDefaultRuleName() const override { return "timer"; }
};

class RepeatingIntervalRule : public BackgroundTracingRule {
 private:
  RepeatingIntervalRule(base::TimeDelta period, bool randomized)
      : period_(period),
        randomized_(randomized),
        interval_phase_(base::TimeTicks::Now()) {}

 public:
  static std::unique_ptr<BackgroundTracingRule> Create(
      const perfetto::protos::gen::TriggerRule& config) {
    DCHECK(config.has_repeating_interval());

    const auto& interval = config.repeating_interval();
    if (!interval.has_period_ms()) {
      return nullptr;
    }
    return base::WrapUnique<RepeatingIntervalRule>(new RepeatingIntervalRule(
        base::Milliseconds(interval.period_ms()), interval.randomized()));
  }

  void DoInstall() override { ScheduleNextTick(); }
  void DoUninstall() override { timer_.Stop(); }

  perfetto::protos::gen::TriggerRule ToProtoForTesting()  // IN-TEST
      const override {
    perfetto::protos::gen::TriggerRule config =
        BackgroundTracingRule::ToProtoForTesting();  // IN-TEST
    auto* histogram = config.mutable_repeating_interval();
    histogram->set_period_ms(period_.InMilliseconds());
    histogram->set_randomized(randomized_);
    return config;
  }

  void GenerateMetadataProto(
      BackgroundTracingRule::MetadataProto* out) const override {
    DCHECK(out);
    BackgroundTracingRule::GenerateMetadataProto(out);
    out->set_trigger_type(MetadataProto::TRIGGER_UNSPECIFIED);
  }

 protected:
  std::string GetDefaultRuleName() const override {
    return "repeating_interval";
  }

 private:
  base::TimeTicks GetFireTimeForInterval(base::TimeTicks interval_start) const {
    if (randomized_) {
      return interval_start +
             base::Microseconds(base::RandGenerator(period_.InMicroseconds()));
    }
    return interval_start;
  }
  base::TimeTicks GetNextIntervalStart(base::TimeTicks now) const {
    base::TimeTicks next_interval_start =
        now.SnappedToNextTick(interval_phase_, period_);
    if (next_interval_start == now) {
      return next_interval_start + period_;
    }
    return next_interval_start;
  }
  void UpdateNextFireTime(base::TimeTicks now) {
    base::TimeTicks next_interval_start = GetNextIntervalStart(now);
    base::TimeTicks prev_interval_start = next_interval_start - period_;
    // If the previously scheduled fire time is in a past interval, the current
    // interval can still fire. Compute a new fire time for the current
    // interval and keep it if it hasn't passed, otherwise move on to the next
    // interval.
    if (randomized_ && (scheduled_fire_time_ < prev_interval_start)) {
      scheduled_fire_time_ = GetFireTimeForInterval(prev_interval_start);
      if (scheduled_fire_time_ >= now) {
        return;
      }
    }
    scheduled_fire_time_ = GetFireTimeForInterval(next_interval_start);
  }

  void ScheduleNextTick() {
    base::TimeTicks now = base::TimeTicks::Now();
    // Update the next fire time only if it's in the past.
    if (scheduled_fire_time_ <= now) {
      UpdateNextFireTime(now);
    }

    timer_.Start(
        FROM_HERE, scheduled_fire_time_,
        base::BindOnce(&RepeatingIntervalRule::OnTick, base::Unretained(this)),
        base::subtle::DelayPolicy::kFlexibleNoSooner);
  }

  void OnTick() {
    OnRuleTriggered(std::nullopt, base::trace_event::GetNextGlobalTraceId());
    ScheduleNextTick();
  }

  const base::TimeDelta period_;
  const bool randomized_;
  const base::TimeTicks interval_phase_;
  base::TimeTicks scheduled_fire_time_;
  base::DeadlineTimer timer_;
};

}  // namespace

std::unique_ptr<BackgroundTracingRule> BackgroundTracingRule::Create(
    const perfetto::protos::gen::TriggerRule& config) {
  std::unique_ptr<BackgroundTracingRule> tracing_rule;
  if (config.has_manual_trigger_name()) {
    tracing_rule = NamedTriggerRule::Create(config);
  } else if (config.has_histogram()) {
    tracing_rule = HistogramRule::Create(config);
  } else if (config.has_repeating_interval()) {
    tracing_rule = RepeatingIntervalRule::Create(config);
  } else if (config.has_delay_ms()) {
    tracing_rule = TimerRule::Create(config);
  } else {
    return nullptr;
  }
  if (tracing_rule) {
    tracing_rule->Setup(config);
  }
  return tracing_rule;
}

bool BackgroundTracingRule::Append(
    const std::vector<perfetto::protos::gen::TriggerRule>& configs,
    std::vector<std::unique_ptr<BackgroundTracingRule>>& rules) {
  for (const auto& rule_config : configs) {
    auto rule = BackgroundTracingRule::Create(rule_config);
    if (!rule) {
      return false;
    }
    rules.push_back(std::move(rule));
  }
  return true;
}

}  // namespace tracing
