blob: c488867b34b07cf09680f3843c4df0203a495fe3 [file] [log] [blame]
// Copyright 2016 The Chromium OS 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 "login_manager/cumulative_use_time_metric.h"
#include <limits>
#include <utility>
#include <base/bind.h>
#include <base/files/file_util.h>
#include <base/hash.h>
#include <base/json/json_reader.h>
#include <base/json/json_writer.h>
#include <base/values.h>
#include <metrics/metrics_library.h>
namespace login_manager {
namespace {
// Time interval between cumulative use time metric updates.
const int kMetricsUpdateIntervalSeconds = 5 * 60;
// Used to calculate max size of accumulated seconds per UMA upload, which is
// needed to define histogram parameters.
const int kSecondsInADay = 24 * 60 * 60;
// Constants used for the UMA metric representing the accumulated usage time:
const int kAccumulatedActiveTimeBucketCount = 50;
const int kAccumulatedActiveTimeMin = 1;
// Set to expected max time sent to UMA - usage values are sent only if it is
// detected that day index (amount of time in days since base::Time::UnixEpoch)
// has changed at the time metric value is updated. So max elapsed time since
// last update is seconds in a day + metric update interval.
const int kAccumulatedActiveTimeMax =
kSecondsInADay + kMetricsUpdateIntervalSeconds;
// This should be enough for writing JSON file containing information about
// usage time metric (can be increased if needed).
const size_t kMetricFileSizeLimit = 1024;
// File extension that should be used for the file backing cumulative use time
// metric.
const char kMetricFileExtension[] = "json";
// Keys for for usage metric parameters in the JSON saved in the metrics file.
const char kOsVersionHashKey[] = "os_version_hash";
const char kStartDayKey[] = "start_day";
const char kElapsedMillisecondsKey[] = "elapsed_milliseconds";
} // namespace
class CumulativeUseTimeMetric::AccumulatedActiveTime {
explicit AccumulatedActiveTime(const base::FilePath& metrics_file);
base::FilePath metrics_file() const { return metrics_file_; }
base::TimeDelta accumulated_time() const { return accumulated_time_; }
int start_day() const { return start_day_; }
// Loads previously persisted metric info from disk and checks if the loaded
// OS version hash matches |os_version_hash|. If the OS version hashes don't
// match, resets the accumulated time value and sets the new OS version hash.
void Init(int os_version_hash);
// Increases current accumulated usage time by |time|.
void AddTime(const base::TimeDelta& time);
// Sets accumulated usage time to |remaining_time|. Sets usage start day to
// |day|.
void Reset(const base::TimeDelta& remaining_time, int day);
// Methods used to sync usage time parameters to file system.
bool ReadMetricsFile();
bool WriteMetricsFile();
// File path of the file to which current metric info is saved in order to
// persist metric value across reboots.
const base::FilePath metrics_file_;
// Hash of the OS version on which current usage time was accumulated.
int os_version_hash_{0};
// Current accumulated usage time.
base::TimeDelta accumulated_time_;
// ID of the day on which accumulating current usage time started.
// The day id is the number of 24-hour periods that passed from
// Time::UnixEpoch() (though, this class does not directly depend on this).
int start_day_{0};
const base::FilePath& metrics_file)
: metrics_file_(metrics_file) {}
void CumulativeUseTimeMetric::AccumulatedActiveTime::Init(int os_version_hash) {
// Read persisted metric data and then compare read OS version hash to
// |os_version_hash|. If the hashes do not match (or metric file could not be
// read), accumulated usage time should be reset - the goal of this is to
// avoid usage time from before version update to be reported as part of the
// current version usage.
if (ReadMetricsFile() && os_version_hash == os_version_hash_)
os_version_hash_ = os_version_hash;
// Note that these have to be reset even if reading metric file failed (as
// some data might have been partially read).
Reset(base::TimeDelta(), 0);
void CumulativeUseTimeMetric::AccumulatedActiveTime::AddTime(
const base::TimeDelta& time) {
if (time.is_zero())
accumulated_time_ += time;
void CumulativeUseTimeMetric::AccumulatedActiveTime::Reset(
const base::TimeDelta& remaining_time, int day) {
accumulated_time_ = remaining_time;
start_day_ = day;
bool CumulativeUseTimeMetric::AccumulatedActiveTime::ReadMetricsFile() {
std::string data_json;
if (!base::ReadFileToStringWithMaxSize(metrics_file_, &data_json,
kMetricFileSizeLimit)) {
return false;
std::unique_ptr<base::Value> data_value(base::JSONReader::Read(data_json));
if (!data_value.get()) {
LOG(ERROR) << "Contents of " << metrics_file_.value() << " invalid JSON";
return false;
const base::DictionaryValue* data = nullptr;
if (!data_value->GetAsDictionary(&data)) {
LOG(ERROR) << "Content of " << metrics_file_.value() << " not a dictionary";
return false;
if (!data->GetInteger(kOsVersionHashKey, &os_version_hash_)) {
LOG(ERROR) << "OS version hash missing in " << metrics_file_.value();
return false;
if (!data->GetInteger(kStartDayKey, &start_day_)) {
LOG(ERROR) << "Start day missing in " << metrics_file_.value();
return false;
int elapsed_milliseconds = 0;
if (!data->GetInteger(kElapsedMillisecondsKey, &elapsed_milliseconds)) {
LOG(ERROR) << "Elapsed milliseconds missing in " << metrics_file_.value();
return false;
accumulated_time_ = base::TimeDelta::FromMilliseconds(elapsed_milliseconds);
return true;
bool CumulativeUseTimeMetric::AccumulatedActiveTime::WriteMetricsFile() {
base::DictionaryValue data;
data.SetInteger(kOsVersionHashKey, os_version_hash_);
data.SetInteger(kStartDayKey, start_day_);
int64_t elapsed_milliseconds = accumulated_time_.InMilliseconds();
if (elapsed_milliseconds < 0 ||
elapsed_milliseconds > std::numeric_limits<int>::max()) {
LOG(ERROR) << "Elapsed milliseconds not in int bounds: "
<< elapsed_milliseconds;
// Something is wrong here. Reset the stored amount.
accumulated_time_ = base::TimeDelta();
elapsed_milliseconds = 0;
std::string data_json;
if (!base::JSONWriter::Write(data, &data_json)) {
LOG(ERROR) << "Failed to create JSON string for " << data;
return false;
int data_size = data_json.size();
if (base::WriteFile(metrics_file_,, data_size) !=
data_size) {
LOG(ERROR) << "Failed to write metric data to " << metrics_file_.value();
return false;
return true;
const std::string& metric_name,
MetricsLibraryInterface* metrics_lib,
const base::FilePath& metrics_files_dir,
std::unique_ptr<base::Clock> time_clock,
std::unique_ptr<base::TickClock> time_tick_clock)
: metrics_lib_(metrics_lib),
new AccumulatedActiveTime(metrics_files_dir.AppendASCII(metric_name_)
time_tick_clock_(std::move(time_tick_clock)) {}
CumulativeUseTimeMetric::~CumulativeUseTimeMetric() {}
void CumulativeUseTimeMetric::Init(const std::string& os_version_string) {
// Test if there is any persisted accumulated data that should be sent to UMA.
initialized_ = true;
void CumulativeUseTimeMetric::Start() {
last_update_time_ = time_tick_clock_->NowTicks();
// Timer will be stopped when this goes out of scope, so Unretained is safe.
FROM_HERE, base::TimeDelta::FromSeconds(kMetricsUpdateIntervalSeconds),
void CumulativeUseTimeMetric::Stop() {
if (!last_update_time_.is_null())
last_update_time_ = base::TimeTicks();
base::TimeDelta CumulativeUseTimeMetric::GetMetricsUpdateCycle() const {
return base::TimeDelta::FromSeconds(kMetricsUpdateIntervalSeconds);
base::TimeDelta CumulativeUseTimeMetric::GetMetricsUploadCycle() const {
return base::TimeDelta::FromSeconds(kSecondsInADay);
base::FilePath CumulativeUseTimeMetric::GetMetricsFileForTest() const {
return accumulated_active_time_->metrics_file();
void CumulativeUseTimeMetric::UpdateStats() {
base::TimeTicks now = time_tick_clock_->NowTicks();
const base::TimeDelta elapsed_time = now - last_update_time_;
last_update_time_ = now;
void CumulativeUseTimeMetric::IncreaseActiveTimeAndSendUmaIfNeeded(
const base::TimeDelta& additional_time) {
const int day = (time_clock_->Now() - base::Time::UnixEpoch()).InDays();
// If not enough time has passed since the metric was last sent, just update
// the time.
if (accumulated_active_time_->start_day() == day) {
// If metric has not previously been set, do it now, and make sure initial
// update is not sent to UMA.
if (accumulated_active_time_->start_day() == 0 &&
accumulated_active_time_->accumulated_time().is_zero()) {
accumulated_active_time_->Reset(additional_time, day);
base::TimeDelta accumulated_time =
accumulated_active_time_->accumulated_time() + additional_time;
int seconds_to_send = accumulated_time.InSeconds();
// Avoid sending 0 values to UMA.
if (seconds_to_send != 0) {
metric_name_, seconds_to_send, kAccumulatedActiveTimeMin,
kAccumulatedActiveTimeMax, kAccumulatedActiveTimeBucketCount);
// Keep any data unreported due to rounding time to seconds, and set the time
// accumulation start day to the new value.
accumulated_time - base::TimeDelta::FromSeconds(seconds_to_send), day);
} // namespace login_manager