// 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