chromium/chrome/browser/ash/external_metrics/external_metrics.cc

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

#include "chrome/browser/ash/external_metrics/external_metrics.h"

#include <stddef.h>

#include <map>
#include <memory>
#include <string>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/files/dir_reader_posix.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/sparse_histogram.h"
#include "base/metrics/statistics_recorder.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/metrics/chromeos_metrics_provider.h"
#include "components/metrics/serialization/metric_sample.h"
#include "components/metrics/serialization/serialization_utils.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"

namespace ash {

namespace {

bool CheckValues(const std::string& name,
                 int minimum,
                 int maximum,
                 size_t bucket_count) {
  if (!base::Histogram::InspectConstructionArguments(name, &minimum, &maximum,
                                                     &bucket_count)) {
    return false;
  }
  base::HistogramBase* histogram =
      base::StatisticsRecorder::FindHistogram(name);
  if (!histogram) {
    return true;
  }
  return histogram->HasConstructionArguments(minimum, maximum, bucket_count);
}

bool CheckLinearValues(const std::string& name, int maximum) {
  return CheckValues(name, 1, maximum, maximum + 1);
}

// Default interval between externally-reported metrics being collected.
constexpr base::TimeDelta kDefaultCollectionInterval = base::Seconds(30);

ExternalMetrics* g_instance = nullptr;

}  // namespace

ExternalMetrics::ExternalMetrics()
    : uma_events_file_(kEventsFilePath),
      uma_events_dir_(kEventsDirectoryPath),
      uma_early_metrics_dir_(kUmaEarlyMetricsDirectoryPath),
      collection_interval_(kDefaultCollectionInterval) {
  CHECK(!g_instance);
  g_instance = this;

  const std::string flag_value =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kExternalMetricsCollectionInterval);
  if (!flag_value.empty()) {
    int seconds = -1;
    if (base::StringToInt(flag_value, &seconds) && seconds > 0) {
      collection_interval_ = base::Seconds(seconds);
    } else {
      LOG(WARNING) << "Ignoring bad value \"" << flag_value << "\" in --"
                   << switches::kExternalMetricsCollectionInterval;
    }
  }
}

ExternalMetrics::~ExternalMetrics() {
  CHECK(g_instance == this);
  g_instance = nullptr;
}

// static
ExternalMetrics* ExternalMetrics::Get() {
  return g_instance;
}

void ExternalMetrics::Start() {
  ScheduleCollector();
}

// static
scoped_refptr<ExternalMetrics> ExternalMetrics::CreateForTesting(
    const std::string& filename,
    const std::string& uma_events_dir,
    const std::string& uma_early_metrics_dir) {
  scoped_refptr<ExternalMetrics> external_metrics(new ExternalMetrics());
  external_metrics->uma_events_file_ = filename;
  external_metrics->uma_events_dir_ = uma_events_dir;
  external_metrics->uma_early_metrics_dir_ = uma_early_metrics_dir;
  return external_metrics;
}

void ExternalMetrics::RecordActionUI(const std::string& action_string,
                                     int num_samples) {
  for (int i = 0; i < num_samples; ++i) {
    base::RecordComputedAction(action_string);
  }
}

void ExternalMetrics::RecordAction(const metrics::MetricSample& sample) {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&ExternalMetrics::RecordActionUI, this,
                                sample.name(), sample.num_samples()));
}

void ExternalMetrics::RecordCrashUI(const std::string& crash_kind,
                                    int num_samples) {
  ChromeOSMetricsProvider::LogCrash(crash_kind, num_samples);
}

void ExternalMetrics::RecordCrash(const metrics::MetricSample& crash_sample) {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&ExternalMetrics::RecordCrashUI, this, crash_sample.name(),
                     crash_sample.num_samples()));
}

void ExternalMetrics::RecordHistogram(const metrics::MetricSample& sample) {
  CHECK_EQ(metrics::MetricSample::HISTOGRAM, sample.type());
  if (!CheckValues(sample.name(), sample.min(), sample.max(),
                   sample.bucket_count())) {
    DLOG(ERROR) << "Invalid histogram: " << sample.name();
    return;
  }

  // We don't use base::UmaHistogramCustomCounts here because it doesn't support
  // AddCount.
  base::HistogramBase* histogram = base::Histogram::FactoryGet(
      sample.name(), sample.min(), sample.max(), sample.bucket_count(),
      base::HistogramBase::kUmaTargetedHistogramFlag);
  histogram->AddCount(sample.sample(), sample.num_samples());
}

void ExternalMetrics::RecordLinearHistogram(
    const metrics::MetricSample& sample) {
  CHECK_EQ(metrics::MetricSample::LINEAR_HISTOGRAM, sample.type());
  if (!CheckLinearValues(sample.name(), sample.max())) {
    DLOG(ERROR) << "Invalid linear histogram: " << sample.name();
    return;
  }
  // We don't use base::UmaHistogramExactLinear because it doesn't support
  // AddCount.
  base::HistogramBase* histogram = base::LinearHistogram::FactoryGet(
      sample.name(), 1, sample.max(), static_cast<size_t>(sample.max() + 1),
      base::HistogramBase::kUmaTargetedHistogramFlag);
  histogram->AddCount(sample.sample(), sample.num_samples());
}

void ExternalMetrics::RecordSparseHistogram(
    const metrics::MetricSample& sample) {
  CHECK_EQ(metrics::MetricSample::SPARSE_HISTOGRAM, sample.type());
  // We suspect a chromeos process reports a metric as regular and then later as
  // a sparse enum histogram. See https://crbug.com/1173221
  {
    base::HistogramBase* histogram =
        base::StatisticsRecorder::FindHistogram(sample.name());
    if (histogram && histogram->GetHistogramType() != base::SPARSE_HISTOGRAM) {
      LOG(FATAL) << "crbug.com/1173221 name " << sample.name() << " "
                 << sample.ToString();
    }
  }

  // We don't use base::UmaHistogramSparse because it doesn't support AddCount.
  base::HistogramBase* histogram = base::SparseHistogram::FactoryGet(
      sample.name(), base::HistogramBase::kUmaTargetedHistogramFlag);
  histogram->AddCount(sample.sample(), sample.num_samples());
}

int ExternalMetrics::ProcessSamples(
    std::vector<std::unique_ptr<metrics::MetricSample>>* samples) {
  int num_samples = 0;
  for (auto it = samples->begin(); it != samples->end(); ++it) {
    const metrics::MetricSample& sample = **it;

    num_samples += sample.num_samples();

    // Do not use the UMA_HISTOGRAM_... macros here.  They cache the Histogram
    // instance and thus only work if |sample.name()| is constant.
    switch (sample.type()) {
      case metrics::MetricSample::CRASH:
        RecordCrash(sample);
        break;
      case metrics::MetricSample::USER_ACTION:
        RecordAction(sample);
        break;
      case metrics::MetricSample::HISTOGRAM:
        RecordHistogram(sample);
        break;
      case metrics::MetricSample::LINEAR_HISTOGRAM:
        RecordLinearHistogram(sample);
        break;
      case metrics::MetricSample::SPARSE_HISTOGRAM:
        RecordSparseHistogram(sample);
        break;
    }
  }
  samples->clear();
  return num_samples;
}

int ExternalMetrics::CollectEvents() {
  std::vector<std::unique_ptr<metrics::MetricSample>> samples;
  metrics::SerializationUtils::ReadAndTruncateMetricsFromFile(uma_events_file_,
                                                              &samples);
  int cumulative_num_samples = ProcessSamples(&samples);

  // Collect UMA events for events that happen before the stateful partition is
  // mounted. Crash reporter will set the UMA events output directory since
  // the typical |uma_events_dir_| does not exist before stateful partition is
  // mounted.
  base::DirReaderPosix reader_early_metrics(uma_early_metrics_dir_.c_str());
  if (!reader_early_metrics.IsValid()) {
    LOG(ERROR) << "Failed to create DirReaderPosix. Cannot read early per-pid "
                  "uma files.";
  }

  while (reader_early_metrics.IsValid() && reader_early_metrics.Next()) {
    std::string filename(reader_early_metrics.name());
    if (filename == "." || filename == "..") {
      continue;
    }
    metrics::SerializationUtils::ReadAndDeleteMetricsFromFile(
        base::StrCat({uma_early_metrics_dir_, "/", filename}), &samples);
    cumulative_num_samples += ProcessSamples(&samples);
  }

  // Collect UMA events which happen after stateful partition is mounted.
  base::DirReaderPosix reader_metrics(uma_events_dir_.c_str());
  if (!reader_metrics.IsValid()) {
    LOG(ERROR)
        << "Failed to create DirReaderPosix. Cannot read per-pid uma files.";
  }

  while (reader_metrics.IsValid() && reader_metrics.Next()) {
    std::string filename(reader_metrics.name());
    if (filename == "." || filename == "..") {
      continue;
    }
    metrics::SerializationUtils::ReadAndDeleteMetricsFromFile(
        base::StrCat({uma_events_dir_, "/", filename}), &samples);
    cumulative_num_samples += ProcessSamples(&samples);
  }

  return cumulative_num_samples;
}

void ExternalMetrics::CollectEventsAndReschedule() {
  CollectEvents();
  ScheduleCollector();
}

void ExternalMetrics::ScheduleCollector() {
  base::ThreadPool::PostDelayedTask(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&ExternalMetrics::CollectEventsAndReschedule, this),
      collection_interval_);
}

}  // namespace ash