chromium/chromeos/ash/components/feature_usage/feature_usage_metrics.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/feature_usage/feature_usage_metrics.h"

#include <optional>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/power_monitor/power_monitor.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"

namespace ash::feature_usage {

namespace {

constexpr char kFeatureUsageMetricPrefix[] = "ChromeOS.FeatureUsage.";
constexpr char kFeatureUsetimeMetricPostfix[] = ".Usetime";

std::string FeatureToHistogram(const std::string& feature_name) {
  return kFeatureUsageMetricPrefix + feature_name;
}

}  // namespace

std::optional<bool> FeatureUsageMetrics::Delegate::IsAccessible() const {
  return std::nullopt;
}

// First time periodic metrics are reported after 'kInitialInterval` time.
constexpr base::TimeDelta FeatureUsageMetrics::kInitialInterval =
    base::Minutes(1);

// Consecutive reports run every `kRepeatedInterval`
constexpr base::TimeDelta FeatureUsageMetrics::kRepeatedInterval =
    base::Minutes(30);

FeatureUsageMetrics::FeatureUsageMetrics(const std::string& feature_name,
                                         Delegate* const delegate)
    : FeatureUsageMetrics(feature_name,
                          delegate,
                          base::DefaultClock::GetInstance(),
                          base::DefaultTickClock::GetInstance()) {}

FeatureUsageMetrics::FeatureUsageMetrics(const std::string& feature_name,
                                         Delegate* const delegate,
                                         const base::Clock* clock,
                                         const base::TickClock* tick_clock)
    : histogram_name_(FeatureToHistogram(feature_name)),
      delegate_(delegate),
      clock_(clock),
      timer_(tick_clock) {
  DCHECK(delegate_);

  // Schedule the first run some time in the future to not overload startup
  // flow.
  SetupTimer(kInitialInterval);
  base::PowerMonitor::AddPowerSuspendObserver(this);
}

void FeatureUsageMetrics::SetupTimer(base::TimeDelta delta) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  timer_.Start(FROM_HERE, delta,
               base::BindOnce(&FeatureUsageMetrics::MaybeReportPeriodicMetrics,
                              base::Unretained(this)));
}

FeatureUsageMetrics::~FeatureUsageMetrics() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::PowerMonitor::RemovePowerSuspendObserver(this);
  MaybeReportUseTime();
}

void FeatureUsageMetrics::RecordUsage(bool success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(delegate_->IsEligible());
  DCHECK(delegate_->IsEnabled());
  DCHECK(start_usage_.is_null());
#if DCHECK_IS_ON()
  last_record_usage_outcome_ = success;
#endif
  MaybeReportPeriodicMetrics();

  Event e = success ? Event::kUsedWithSuccess : Event::kUsedWithFailure;
  base::UmaHistogramEnumeration(histogram_name_, e);
}

void FeatureUsageMetrics::RecordUsetime(base::TimeDelta usetime) const {
  base::UmaHistogramLongTimes100(histogram_name_ + kFeatureUsetimeMetricPostfix,
                                 usetime);
}

void FeatureUsageMetrics::StartSuccessfulUsage() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(start_usage_.is_null());
#if DCHECK_IS_ON()
  DCHECK(last_record_usage_outcome_.value_or(false))
      << "Start usage must be preceded by RecordUsage(true)";
  last_record_usage_outcome_.reset();
#endif
  start_usage_ = Now();
}

void FeatureUsageMetrics::StopSuccessfulUsage() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!start_usage_.is_null());
#if DCHECK_IS_ON()
  DCHECK(!last_record_usage_outcome_.has_value())
      << "There must be no RecordUsage calls between Start and "
         "StopSuccessfulUsage";
#endif
  MaybeReportUseTime();
  start_usage_ = base::Time();
}

void FeatureUsageMetrics::OnSuspend() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  timer_.AbandonAndStop();
  ReportPeriodicMetrics();
}

void FeatureUsageMetrics::OnResume() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!start_usage_.is_null()) {
    // Do not report usage during suspension.
    start_usage_ = Now();
  }
  SetupTimer(kInitialInterval);
}

void FeatureUsageMetrics::MaybeReportPeriodicMetrics() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!last_time_enabled_reported_.is_null()) {
    const base::TimeDelta delta_since_enabled_reported =
        Now() - last_time_enabled_reported_;
    // Do not report periodic metrics more often than once per
    // `kRepeatedInterval`.
    if (kRepeatedInterval > delta_since_enabled_reported) {
      // This could only happen when `RecordUsage` is called. In that case
      // `IsEnabled` must be true. And because `last_time_enabled_reported_` is
      // not null - `IsEnabled` was already reported recently.
      SetupTimer(kRepeatedInterval - delta_since_enabled_reported);
      return;
    }
  }
  ReportPeriodicMetrics();
}

void FeatureUsageMetrics::ReportPeriodicMetrics() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  bool is_eligible = delegate_->IsEligible();
  bool is_enabled = delegate_->IsEnabled();
  DCHECK(!is_enabled || is_eligible);

  if (is_eligible)
    base::UmaHistogramEnumeration(histogram_name_, Event::kEligible);

  std::optional<bool> is_accessible = delegate_->IsAccessible();

  if (is_accessible.has_value()) {
    // If accessible must be eligible.
    DCHECK(!is_accessible.value() || is_eligible);

    // If enabled must be accessible
    DCHECK(!is_enabled || is_accessible.value());

    if (is_accessible.value())
      base::UmaHistogramEnumeration(histogram_name_, Event::kAccessible);
  }

  if (is_enabled) {
    last_time_enabled_reported_ = Now();
    base::UmaHistogramEnumeration(histogram_name_, Event::kEnabled);
  }

  // If the feature in the active use - it must be enabled.
  DCHECK(start_usage_.is_null() || is_enabled);
  MaybeReportUseTime();

  SetupTimer(kRepeatedInterval);
}

void FeatureUsageMetrics::MaybeReportUseTime() {
  if (start_usage_.is_null())
    return;
  base::TimeDelta use_time = Now() - start_usage_;
  RecordUsetime(use_time);
  start_usage_ = Now();
}

base::Time FeatureUsageMetrics::Now() const {
  return clock_->Now();
}

}  // namespace ash::feature_usage