| // 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/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 { |
| public: |
| 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); |
| |
| private: |
| // 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}; |
| |
| DISALLOW_COPY_AND_ASSIGN(AccumulatedActiveTime); |
| }; |
| |
| CumulativeUseTimeMetric::AccumulatedActiveTime::AccumulatedActiveTime( |
| 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_) |
| return; |
| |
| 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()) |
| return; |
| |
| accumulated_time_ += time; |
| WriteMetricsFile(); |
| } |
| |
| void CumulativeUseTimeMetric::AccumulatedActiveTime::Reset( |
| const base::TimeDelta& remaining_time, int day) { |
| accumulated_time_ = remaining_time; |
| start_day_ = day; |
| WriteMetricsFile(); |
| } |
| |
| bool CumulativeUseTimeMetric::AccumulatedActiveTime::ReadMetricsFile() { |
| std::string data_json; |
| if (!base::ReadFileToStringWithMaxSize(metrics_file_, &data_json, |
| kMetricFileSizeLimit)) { |
| return false; |
| } |
| |
| auto data_value = base::JSONReader::Read(data_json, base::JSON_PARSE_RFC); |
| if (!data_value) { |
| 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; |
| } |
| data.SetInteger(kElapsedMillisecondsKey, |
| static_cast<int>(elapsed_milliseconds)); |
| |
| 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_json.data(), data_size) != |
| data_size) { |
| LOG(ERROR) << "Failed to write metric data to " << metrics_file_.value(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| CumulativeUseTimeMetric::CumulativeUseTimeMetric( |
| 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), |
| metric_name_(metric_name), |
| accumulated_active_time_( |
| new AccumulatedActiveTime(metrics_files_dir.AppendASCII(metric_name_) |
| .AddExtension(kMetricFileExtension))), |
| time_clock_(std::move(time_clock)), |
| time_tick_clock_(std::move(time_tick_clock)) {} |
| |
| CumulativeUseTimeMetric::~CumulativeUseTimeMetric() {} |
| |
| void CumulativeUseTimeMetric::Init(const std::string& os_version_string) { |
| accumulated_active_time_->Init( |
| static_cast<int>(base::Hash(os_version_string))); |
| |
| // Test if there is any persisted accumulated data that should be sent to UMA. |
| IncreaseActiveTimeAndSendUmaIfNeeded(base::TimeDelta()); |
| |
| initialized_ = true; |
| } |
| |
| void CumulativeUseTimeMetric::Start() { |
| CHECK(initialized_); |
| |
| last_update_time_ = time_tick_clock_->NowTicks(); |
| IncreaseActiveTimeAndSendUmaIfNeeded(base::TimeDelta()); |
| |
| // Timer will be stopped when this goes out of scope, so Unretained is safe. |
| update_stats_timer_.Start( |
| FROM_HERE, base::TimeDelta::FromSeconds(kMetricsUpdateIntervalSeconds), |
| base::Bind(&CumulativeUseTimeMetric::UpdateStats, |
| base::Unretained(this))); |
| } |
| |
| void CumulativeUseTimeMetric::Stop() { |
| CHECK(initialized_); |
| if (!last_update_time_.is_null()) |
| UpdateStats(); |
| |
| update_stats_timer_.Stop(); |
| 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; |
| |
| IncreaseActiveTimeAndSendUmaIfNeeded(elapsed_time); |
| } |
| |
| 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) { |
| accumulated_active_time_->AddTime(additional_time); |
| return; |
| } |
| |
| // 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); |
| return; |
| } |
| |
| 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) { |
| metrics_lib_->SendToUMA( |
| 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_active_time_->Reset( |
| accumulated_time - base::TimeDelta::FromSeconds(seconds_to_send), day); |
| } |
| |
| } // namespace login_manager |