blob: 7dd26f019209912737d68da4b04c8de642c4e510 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/metrics/login_event_recorder.h"
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/task/current_thread.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
namespace chromeos {
namespace {
constexpr char kUptime[] = "uptime";
constexpr char kDisk[] = "disk";
// The pointer to this object is used as a perfetto async event id.
constexpr char kBootTimes[] = "BootTimes";
#define FPL(value) FILE_PATH_LITERAL(value)
// Dir uptime & disk logs are located in.
constexpr const base::FilePath::CharType kLogPath[] = FPL("/tmp");
// Dir log{in,out} logs are located in.
constexpr base::FilePath::CharType kLoginLogPath[] = FPL("/home/chronos/user");
// Prefix for the time measurement files.
constexpr base::FilePath::CharType kUptimePrefix[] = FPL("uptime-");
// Prefix for the disk usage files.
constexpr base::FilePath::CharType kDiskPrefix[] = FPL("disk-");
// Prefix and suffix for the "stats saved" flags file.
constexpr base::FilePath::CharType kStatsPrefix[] = FPL("stats-");
constexpr base::FilePath::CharType kWrittenSuffix[] = FPL(".written");
// Names of login stats files.
constexpr base::FilePath::CharType kLoginSuccess[] = FPL("login-success");
// The login times will be written immediately when the login animation ends,
// and this is used to ensure the data is always written if this amount is
// elapsed after login.
constexpr int64_t kLoginTimeWriteDelayMs = 20000;
// Appends the given buffer into the file. Returns the number of bytes
// written, or -1 on error.<
// TODO(satorux): Move this to file_util.
int AppendFile(const base::FilePath& file_path, const char* data, int size) {
// Appending boot times to (probably) a symlink in /tmp is a security risk for
// developers with chromeos=1 builds.
if (!base::SysInfo::IsRunningOnChromeOS())
return -1;
FILE* file = base::OpenFile(file_path, "a");
if (!file)
return -1;
const int num_bytes_written = fwrite(data, 1, size, file);
base::CloseFile(file);
return num_bytes_written;
}
void WriteTimes(const std::string base_name,
const std::string uma_name,
const std::string uma_prefix,
std::vector<LoginEventRecorder::TimeMarker> times) {
DCHECK(times.size());
const int kMinTimeMillis = 1;
const int kMaxTimeMillis = 30000;
const int kNumBuckets = 100;
const base::FilePath log_path(kLoginLogPath);
// Need to sort by time since the entries may have been pushed onto the
// vector (on the UI thread) in a different order from which they were
// created (potentially on other threads).
std::sort(times.begin(), times.end());
base::Time first = times.front().time();
base::Time last = times.back().time();
base::TimeDelta total = last - first;
base::HistogramBase* total_hist = base::Histogram::FactoryTimeGet(
uma_name, base::Milliseconds(kMinTimeMillis),
base::Milliseconds(kMaxTimeMillis), kNumBuckets,
base::HistogramBase::kUmaTargetedHistogramFlag);
total_hist->AddTime(total);
std::string output =
base::StringPrintf("%s: %.2f", uma_name.c_str(), total.InSecondsF());
base::Time prev = first;
// Convert base::Time to base::TimeTicks for tracing.
auto time2timeticks = [](const base::Time& ts) {
return base::TimeTicks::Now() - (base::Time::Now() - ts);
};
// Send first event to name the track:
// "In Chrome, we usually don't bother setting explicit track names. If none
// is provided, the track is named after the first event on the track."
TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
"startup", kBootTimes, TRACE_ID_LOCAL(kBootTimes), time2timeticks(prev));
for (unsigned int i = 0; i < times.size(); ++i) {
const LoginEventRecorder::TimeMarker& tm = times[i];
if (tm.url().has_value()) {
TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1(
"startup", tm.name(), TRACE_ID_LOCAL(kBootTimes),
time2timeticks(prev), "url", *tm.url());
TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP1(
"startup", tm.name(), TRACE_ID_LOCAL(kBootTimes),
time2timeticks(tm.time()), "url", *tm.url());
} else {
TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
"startup", tm.name(), TRACE_ID_LOCAL(kBootTimes),
time2timeticks(prev));
TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0("startup", tm.name(),
TRACE_ID_LOCAL(kBootTimes),
time2timeticks(tm.time()));
}
base::TimeDelta since_first = tm.time() - first;
base::TimeDelta since_prev = tm.time() - prev;
std::string name;
if (tm.send_to_uma()) {
name = uma_prefix + tm.name();
base::HistogramBase* prev_hist = base::Histogram::FactoryTimeGet(
name, base::Milliseconds(kMinTimeMillis),
base::Milliseconds(kMaxTimeMillis), kNumBuckets,
base::HistogramBase::kUmaTargetedHistogramFlag);
prev_hist->AddTime(since_prev);
} else {
name = tm.name();
}
if (tm.write_to_file()) {
output += base::StringPrintf("\n%.2f +%.4f %s", since_first.InSecondsF(),
since_prev.InSecondsF(), name.data());
if (tm.url().has_value()) {
output += ": ";
output += *tm.url();
}
}
prev = tm.time();
}
output += '\n';
TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0(
"startup", kBootTimes, TRACE_ID_LOCAL(kBootTimes), time2timeticks(prev));
base::WriteFile(log_path.Append(base_name), output.data(), output.size());
}
} // namespace
LoginEventRecorder::TimeMarker::TimeMarker(const char* name,
absl::optional<std::string> url,
bool send_to_uma,
bool write_to_file)
: name_(name),
url_(url),
send_to_uma_(send_to_uma),
write_to_file_(write_to_file) {}
LoginEventRecorder::TimeMarker::TimeMarker(const TimeMarker& other) = default;
LoginEventRecorder::TimeMarker::~TimeMarker() = default;
// static
LoginEventRecorder::Stats LoginEventRecorder::Stats::GetCurrentStats() {
const base::FilePath kProcUptime(FPL("/proc/uptime"));
const base::FilePath kDiskStat(FPL("/sys/block/sda/stat"));
Stats stats;
// Callers of this method expect synchronous behavior.
// It's safe to allow IO here, because only virtual FS are accessed.
base::ThreadRestrictions::ScopedAllowIO allow_io;
base::ReadFileToString(kProcUptime, &stats.uptime_);
base::ReadFileToString(kDiskStat, &stats.disk_);
return stats;
}
std::string LoginEventRecorder::Stats::SerializeToString() const {
if (uptime_.empty() && disk_.empty())
return std::string();
base::DictionaryValue dictionary;
dictionary.SetString(kUptime, uptime_);
dictionary.SetString(kDisk, disk_);
std::string result;
if (!base::JSONWriter::Write(dictionary, &result)) {
LOG(WARNING) << "LoginEventRecorder::Stats::SerializeToString(): failed.";
return std::string();
}
return result;
}
// static
LoginEventRecorder::Stats LoginEventRecorder::Stats::DeserializeFromString(
const std::string& source) {
if (source.empty())
return Stats();
std::unique_ptr<base::Value> value = base::JSONReader::ReadDeprecated(source);
base::DictionaryValue* dictionary;
if (!value || !value->GetAsDictionary(&dictionary)) {
LOG(ERROR) << "LoginEventRecorder::Stats::DeserializeFromString(): not a "
"dictionary: '"
<< source << "'";
return Stats();
}
Stats result;
if (!dictionary->GetString(kUptime, &result.uptime_) ||
!dictionary->GetString(kDisk, &result.disk_)) {
LOG(ERROR)
<< "LoginEventRecorder::Stats::DeserializeFromString(): format error: '"
<< source << "'";
return Stats();
}
return result;
}
bool LoginEventRecorder::Stats::UptimeDouble(double* result) const {
std::string uptime = uptime_;
const size_t space_at = uptime.find_first_of(' ');
if (space_at == std::string::npos)
return false;
uptime.resize(space_at);
if (base::StringToDouble(uptime, result))
return true;
return false;
}
void LoginEventRecorder::Stats::RecordStats(const std::string& name,
bool write_flag_file) const {
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&LoginEventRecorder::Stats::RecordStatsAsync,
base::Owned(new Stats(*this)), name, write_flag_file));
}
void LoginEventRecorder::Stats::RecordStatsWithCallback(
const std::string& name,
bool write_flag_file,
base::OnceClosure callback) const {
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&LoginEventRecorder::Stats::RecordStatsAsync,
base::Owned(new Stats(*this)), name, write_flag_file),
std::move(callback));
}
void LoginEventRecorder::Stats::RecordStatsAsync(
const base::FilePath::StringType& name,
bool write_flag_file) const {
const base::FilePath log_path(kLogPath);
const base::FilePath uptime_output =
log_path.Append(base::FilePath(kUptimePrefix + name));
const base::FilePath disk_output =
log_path.Append(base::FilePath(kDiskPrefix + name));
// Append numbers to the files.
AppendFile(uptime_output, uptime_.data(), uptime_.size());
AppendFile(disk_output, disk_.data(), disk_.size());
if (write_flag_file) {
const base::FilePath flag_path =
log_path.Append(base::FilePath(kStatsPrefix + name + kWrittenSuffix));
AppendFile(flag_path, "", 0);
}
}
static base::LazyInstance<LoginEventRecorder>::DestructorAtExit
g_login_event_recorder = LAZY_INSTANCE_INITIALIZER;
LoginEventRecorder::LoginEventRecorder()
: task_runner_(base::SequencedTaskRunnerHandle::Get()) {
DCHECK(base::CurrentUIThread::IsSet());
login_time_markers_.reserve(30);
logout_time_markers_.reserve(30);
// Remember login events for later retrieval by tests.
constexpr char kKeepLoginEventsForTesting[] = "keep-login-events-for-testing";
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
kKeepLoginEventsForTesting)) {
PrepareEventCollectionForTesting(); // IN-TEST
}
}
LoginEventRecorder::~LoginEventRecorder() = default;
// static
LoginEventRecorder* LoginEventRecorder::Get() {
return g_login_event_recorder.Pointer();
}
void LoginEventRecorder::AddLoginTimeMarker(const char* marker_name,
bool send_to_uma,
bool write_to_file) {
AddLoginTimeMarkerWithURL(marker_name, absl::optional<std::string>(),
send_to_uma, write_to_file);
}
void LoginEventRecorder::AddLoginTimeMarkerWithURL(
const char* marker_name,
absl::optional<std::string> url,
bool send_to_uma,
bool write_to_file) {
AddMarker(&login_time_markers_,
TimeMarker(marker_name, url, send_to_uma, write_to_file));
// Store a copy for testing.
if (login_time_markers_for_testing_.has_value()) {
login_time_markers_for_testing_.value().push_back(
login_time_markers_.back());
}
}
void LoginEventRecorder::AddLogoutTimeMarker(const char* marker_name,
bool send_to_uma) {
AddMarker(&logout_time_markers_,
TimeMarker(marker_name, absl::optional<std::string>(), send_to_uma,
/*write_to_file=*/true));
}
void LoginEventRecorder::RecordAuthenticationSuccess() {
AddLoginTimeMarker("Authenticate", true);
RecordCurrentStats(kLoginSuccess);
}
void LoginEventRecorder::RecordAuthenticationFailure() {
// no nothing for now.
}
void LoginEventRecorder::RecordCurrentStats(const std::string& name) {
Stats::GetCurrentStats().RecordStats(name, /*write_flag_file=*/false);
}
void LoginEventRecorder::ClearLoginTimeMarkers() {
login_time_markers_.clear();
}
void LoginEventRecorder::ScheduleWriteLoginTimes(const std::string base_name,
const std::string uma_name,
const std::string uma_prefix) {
callback_ = base::BindOnce(&WriteTimes, base_name, uma_name, uma_prefix);
DCHECK(task_runner_->RunsTasksInCurrentSequence());
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&LoginEventRecorder::WriteLoginTimesDelayed,
weak_ptr_factory_.GetWeakPtr()),
base::Milliseconds(kLoginTimeWriteDelayMs));
}
void LoginEventRecorder::RunScheduledWriteLoginTimes() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
if (callback_.is_null())
return;
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(std::move(callback_), std::move(login_time_markers_)));
}
void LoginEventRecorder::WriteLogoutTimes(const std::string base_name,
const std::string uma_name,
const std::string uma_prefix) {
WriteTimes(base_name, uma_name, uma_prefix, std::move(logout_time_markers_));
}
void LoginEventRecorder::PrepareEventCollectionForTesting() {
if (login_time_markers_for_testing_.has_value())
return;
login_time_markers_for_testing_ = login_time_markers_;
}
const std::vector<LoginEventRecorder::TimeMarker>&
LoginEventRecorder::GetCollectedLoginEventsForTesting() {
PrepareEventCollectionForTesting(); // IN-TEST
return login_time_markers_for_testing_.value();
}
void LoginEventRecorder::AddMarker(std::vector<TimeMarker>* vector,
TimeMarker&& marker) {
// The marker vectors can only be safely manipulated on the main thread.
// If we're late in the process of shutting down (eg. as can be the case at
// logout), then we have to assume we're on the main thread already.
if (task_runner_->RunsTasksInCurrentSequence()) {
vector->push_back(marker);
} else {
// Add the marker on the UI thread.
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&LoginEventRecorder::AddMarker,
weak_ptr_factory_.GetWeakPtr(),
base::Unretained(vector), marker));
}
}
void LoginEventRecorder::WriteLoginTimesDelayed() {
if (callback_.is_null())
return;
DCHECK(task_runner_->RunsTasksInCurrentSequence());
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(std::move(callback_), std::move(login_time_markers_)));
}
} // namespace chromeos