blob: 5112339e548848d37bc7532be2ec5286e50288ab [file] [log] [blame]
// Copyright 2018 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 "chrome/browser/chromeos/power/auto_screen_brightness/modeller_impl.h"
#include <cmath>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/logging.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/post_task.h"
#include "base/task_runner_util.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "chromeos/constants/chromeos_features.h"
#include "content/public/browser/browser_thread.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
namespace chromeos {
namespace power {
namespace auto_screen_brightness {
namespace {
// Loads saved model from locations specified by |spec|. This
// should run in another thread to be non-blocking to the main thread (if
// |is_testing| is false). The ambient values read from disk should be in the
// log-domain already.
// TODO(jiameng): add UMA metrics to record model loading status.
Model LoadModelFromDisk(const ModellerImpl::ModelSavingSpec& spec,
bool is_testing) {
DCHECK(is_testing ||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
Model loaded_model;
std::string content;
// If global curve doesn't exist or can't be parsed, then we ignore all saved
// data.
if (!PathExists(spec.global_curve) ||
!base::ReadFileToString(spec.global_curve, &content))
return loaded_model;
loaded_model.global_curve = MonotoneCubicSpline::FromString(content);
if (!loaded_model.global_curve)
return loaded_model;
// If personal curve doesn't exist or can't be parsed, then we ignore any
// saved personal model. The iteration count is implicitly set to 0.
if (!PathExists(spec.personal_curve) ||
!base::ReadFileToString(spec.personal_curve, &content))
return loaded_model;
loaded_model.personal_curve = MonotoneCubicSpline::FromString(content);
if (!loaded_model.personal_curve)
return loaded_model;
int iteration_count = 0;
// If iteration count doesn't exist or can't be parsed, it's reset to 0.
if (!PathExists(spec.iteration_count) ||
!base::ReadFileToString(spec.iteration_count, &content) ||
content.empty() || !base::StringToInt(content, &iteration_count))
return loaded_model;
loaded_model.iteration_count = iteration_count;
return loaded_model;
}
// Saves |data| to |path|. Returns whether successful and logs error if an
// error occurs.
bool SaveDataAndLogError(const base::FilePath& path, const std::string& data) {
const int bytes_written = base::WriteFile(path, data.data(), data.size());
if (bytes_written != static_cast<int>(data.size())) {
LOG(ERROR) << "Wrote " << bytes_written << " byte(s) instead of "
<< data.size() << " to " << path.value();
return false;
}
return true;
}
// Trains a new curve using training |data| and returns the new curve. This
// should only be called after trainer has been initialized with a global curve
// and a latest curve.
// This should run in another thread to be non-blocking to the main
// thread (if |is_testing| is false).
MonotoneCubicSpline TrainModel(Trainer* trainer,
const std::vector<TrainingDataPoint>& data,
bool is_testing) {
DCHECK(is_testing ||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
return trainer->Train(data);
}
// Sets initial global and personal curve.
// This should run in another thread to be non-blocking to the main
// thread (if |is_testing| is false).
bool SetInitialCurves(Trainer* trainer,
const MonotoneCubicSpline& global_curve,
const MonotoneCubicSpline& current_curve,
bool is_testing) {
DCHECK(is_testing ||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
return trainer->SetInitialCurves(global_curve, current_curve);
}
} // namespace
constexpr char ModellerImpl::kModelDir[];
constexpr char ModellerImpl::kGlobalCurveFileName[];
constexpr char ModellerImpl::kPersonalCurveFileName[];
constexpr char ModellerImpl::kModelIterationCountFileName[];
Model::Model() = default;
Model::Model(const base::Optional<MonotoneCubicSpline>& global_curve,
const base::Optional<MonotoneCubicSpline>& personal_curve,
int iteration_count)
: global_curve(global_curve),
personal_curve(personal_curve),
iteration_count(iteration_count) {}
Model::Model(const Model& model) = default;
Model::~Model() = default;
bool SaveModelToDisk(const ModellerImpl::ModelSavingSpec& model_saving_spec,
const Model& model,
bool save_global_curve,
bool save_personal_curve,
bool is_testing) {
DCHECK(is_testing ||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (save_global_curve) {
DCHECK(model.global_curve);
const std::string data = model.global_curve->ToString();
DCHECK(!data.empty());
if (!SaveDataAndLogError(model_saving_spec.global_curve, data))
return false;
}
if (save_personal_curve) {
DCHECK(model.personal_curve);
const std::string data = model.personal_curve->ToString();
DCHECK(!data.empty());
if (!SaveDataAndLogError(model_saving_spec.personal_curve, data))
return false;
}
const std::string data = base::NumberToString(model.iteration_count);
DCHECK(!data.empty());
return SaveDataAndLogError(model_saving_spec.iteration_count, data);
}
ModellerImpl::ModellerImpl(const Profile* profile,
AlsReader* als_reader,
BrightnessMonitor* brightness_monitor,
ModelConfigLoader* model_config_loader,
ui::UserActivityDetector* user_activity_detector,
std::unique_ptr<Trainer> trainer)
: ModellerImpl(profile,
als_reader,
brightness_monitor,
model_config_loader,
user_activity_detector,
std::move(trainer),
base::CreateSequencedTaskRunnerWithTraits(
{base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
base::DefaultTickClock::GetInstance()) {}
ModellerImpl::~ModellerImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void ModellerImpl::AddObserver(Modeller::Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(observer);
observers_.AddObserver(observer);
if (is_modeller_enabled_.has_value()) {
NotifyObserverInitStatus(*observer);
}
}
void ModellerImpl::RemoveObserver(Modeller::Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(observer);
observers_.RemoveObserver(observer);
}
void ModellerImpl::OnAmbientLightUpdated(int lux) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_modeller_enabled_.has_value() || !*is_modeller_enabled_)
return;
DCHECK(log_als_values_);
log_als_values_->SaveToBuffer({ConvertToLog(lux), tick_clock_->NowTicks()});
}
void ModellerImpl::OnAlsReaderInitialized(AlsReader::AlsInitStatus status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!als_init_status_);
als_init_status_ = status;
HandleStatusUpdate();
}
void ModellerImpl::OnBrightnessMonitorInitialized(bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!brightness_monitor_success_.has_value());
brightness_monitor_success_ = success;
HandleStatusUpdate();
}
void ModellerImpl::OnUserBrightnessChanged(double old_brightness_percent,
double new_brightness_percent) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_modeller_enabled_.has_value() || !*is_modeller_enabled_)
return;
DCHECK(log_als_values_);
const base::TimeTicks now = tick_clock_->NowTicks();
// We don't add any training data if there is no ambient light sample.
const base::Optional<AlsAvgStdDev> log_als_avg_stddev =
log_als_values_->AverageAmbientWithStdDev(now);
if (!log_als_avg_stddev)
return;
data_cache_.push_back({old_brightness_percent, new_brightness_percent,
log_als_avg_stddev->avg, now});
ScheduleTrainerStart();
}
void ModellerImpl::OnUserBrightnessChangeRequested() {}
void ModellerImpl::OnModelConfigLoaded(
base::Optional<ModelConfig> model_config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!model_config_exists_.has_value());
model_config_exists_ = model_config.has_value();
if (model_config_exists_.value()) {
model_config_ = model_config.value();
}
HandleStatusUpdate();
}
void ModellerImpl::OnUserActivity(const ui::Event* event) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!event)
return;
ScheduleTrainerStart();
}
std::unique_ptr<ModellerImpl> ModellerImpl::CreateForTesting(
const Profile* profile,
AlsReader* als_reader,
BrightnessMonitor* brightness_monitor,
ModelConfigLoader* model_config_loader,
ui::UserActivityDetector* user_activity_detector,
std::unique_ptr<Trainer> trainer,
scoped_refptr<base::SequencedTaskRunner> blocking_task_runner,
const base::TickClock* tick_clock) {
return base::WrapUnique(new ModellerImpl(
profile, als_reader, brightness_monitor, model_config_loader,
user_activity_detector, std::move(trainer), blocking_task_runner,
tick_clock, true /* is_testing */));
}
base::Optional<double> ModellerImpl::AverageAmbientForTesting(
base::TimeTicks now) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(log_als_values_);
const base::Optional<AlsAvgStdDev> log_als_avg_stddev =
log_als_values_->AverageAmbientWithStdDev(now);
if (!log_als_avg_stddev)
return base::nullopt;
return log_als_avg_stddev->avg;
}
size_t ModellerImpl::NumberTrainingDataPointsForTesting() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return data_cache_.size();
}
size_t ModellerImpl::GetMaxTrainingDataPointsForTesting() const {
return max_training_data_points_;
}
base::TimeDelta ModellerImpl::GetTrainingDelayForTesting() const {
return training_delay_;
}
ModelConfig ModellerImpl::GetModelConfigForTesting() const {
return model_config_;
}
ModellerImpl::ModelSavingSpec ModellerImpl::GetModelSavingSpecFromProfile(
const Profile* profile) {
DCHECK(profile);
ModelSavingSpec model_saving_spec;
const base::FilePath profile_path = profile->GetPath();
if (profile_path.empty()) {
return model_saving_spec;
}
const base::FilePath model_dir = profile_path.Append(kModelDir);
if (!base::DirectoryExists(model_dir) && !base::CreateDirectory(model_dir)) {
return model_saving_spec;
}
model_saving_spec.global_curve = model_dir.Append(kGlobalCurveFileName);
model_saving_spec.personal_curve = model_dir.Append(kPersonalCurveFileName);
model_saving_spec.iteration_count =
model_dir.Append(kModelIterationCountFileName);
return model_saving_spec;
}
ModellerImpl::ModellerImpl(
const Profile* profile,
AlsReader* als_reader,
BrightnessMonitor* brightness_monitor,
ModelConfigLoader* model_config_loader,
ui::UserActivityDetector* user_activity_detector,
std::unique_ptr<Trainer> trainer,
const scoped_refptr<base::SequencedTaskRunner> blocking_task_runner,
const base::TickClock* tick_clock,
bool is_testing)
: is_testing_(is_testing),
als_reader_observer_(this),
brightness_monitor_observer_(this),
model_config_loader_observer_(this),
user_activity_observer_(this),
blocking_task_runner_(blocking_task_runner),
trainer_(trainer.release(),
base::OnTaskRunnerDeleter(blocking_task_runner_)),
tick_clock_(tick_clock),
model_timer_(tick_clock_),
weak_ptr_factory_(this) {
DCHECK(als_reader);
DCHECK(brightness_monitor);
DCHECK(model_config_loader);
DCHECK(trainer_);
DCHECK(user_activity_detector);
if (!profile) {
is_modeller_enabled_ = false;
return;
}
if (!trainer_->HasValidConfiguration()) {
is_modeller_enabled_ = false;
return;
}
model_saving_spec_ = GetModelSavingSpecFromProfile(profile);
if (model_saving_spec_.global_curve.empty()) {
is_modeller_enabled_ = false;
return;
}
als_reader_observer_.Add(als_reader);
brightness_monitor_observer_.Add(brightness_monitor);
model_config_loader_observer_.Add(model_config_loader);
user_activity_observer_.Add(user_activity_detector);
}
void ModellerImpl::HandleStatusUpdate() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (is_modeller_enabled_.has_value())
return;
if (!als_init_status_.has_value())
return;
const bool als_success =
*als_init_status_ == AlsReader::AlsInitStatus::kSuccess;
if (!als_success) {
is_modeller_enabled_ = false;
OnInitializationComplete();
return;
}
if (!brightness_monitor_success_.has_value()) {
return;
}
if (!*brightness_monitor_success_) {
is_modeller_enabled_ = false;
OnInitializationComplete();
return;
}
if (!model_config_exists_.has_value())
return;
if (!model_config_exists_.value()) {
is_modeller_enabled_ = false;
OnInitializationComplete();
return;
}
if (!ApplyCustomization()) {
is_modeller_enabled_ = false;
OnInitializationComplete();
return;
}
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(), FROM_HERE,
base::BindOnce(&LoadModelFromDisk, model_saving_spec_, is_testing_),
base::BindOnce(&ModellerImpl::OnModelLoadedFromDisk,
weak_ptr_factory_.GetWeakPtr()));
}
bool ModellerImpl::ApplyCustomization() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(*model_config_exists_);
initial_global_curve_ = MonotoneCubicSpline::CreateMonotoneCubicSpline(
model_config_.log_lux, model_config_.brightness);
if (!initial_global_curve_)
return false;
log_als_values_ = std::make_unique<AmbientLightSampleBuffer>(
base::TimeDelta::FromSeconds(model_config_.model_als_horizon_seconds));
// TODO(jiameng): the following params are probably not useful and can be
// removed.
const int max_training_data_points = GetFieldTrialParamByFeatureAsInt(
features::kAutoScreenBrightness, "max_training_data_points", -1);
if (max_training_data_points > 0) {
max_training_data_points_ = max_training_data_points;
}
const int training_delay_in_seconds = GetFieldTrialParamByFeatureAsInt(
features::kAutoScreenBrightness, "training_delay_in_seconds",
training_delay_.InSeconds());
if (training_delay_in_seconds >= 0) {
training_delay_ = base::TimeDelta::FromSeconds(training_delay_in_seconds);
}
return true;
}
void ModellerImpl::OnInitializationComplete() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(is_modeller_enabled_.has_value());
DCHECK(*is_modeller_enabled_ == model_.global_curve.has_value());
// TODO(jiameng): log model status to UMA.
for (auto& observer : observers_) {
NotifyObserverInitStatus(observer);
}
}
void ModellerImpl::NotifyObserverInitStatus(Modeller::Observer& observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(is_modeller_enabled_.has_value());
observer.OnModelInitialized(model_);
}
void ModellerImpl::OnModelLoadedFromDisk(const Model& model) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(initial_global_curve_);
model_ = model;
if (!model_.global_curve || *model_.global_curve != *initial_global_curve_) {
// Reset the model.
model_.global_curve = initial_global_curve_;
model_.personal_curve = base::nullopt;
model_.iteration_count = 0;
global_curve_reset_ = true;
}
DCHECK(model_.global_curve);
// Run SetInitialCurves calculations on background thread to avoid blocking UI
// thread.
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(), FROM_HERE,
base::BindOnce(
&SetInitialCurves, trainer_.get(), *model_.global_curve,
model_.personal_curve ? *model_.personal_curve : *model_.global_curve,
is_testing_),
base::BindOnce(&ModellerImpl::OnSetInitialCurves,
weak_ptr_factory_.GetWeakPtr()));
}
void ModellerImpl::OnModelSavedToDisk(bool is_successful) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const base::TimeTicks now = tick_clock_->NowTicks();
UMA_HISTOGRAM_BOOLEAN("AutoScreenBrightness.NewCurveSaved.Success",
is_successful);
if (is_successful) {
UMA_HISTOGRAM_TIMES("AutoScreenBrightness.NewCurveSaved.Duration",
now - training_start_.value());
}
// We don't want to repeatedly save the global curve.
global_curve_reset_ = false;
}
void ModellerImpl::OnSetInitialCurves(bool is_personal_curve_valid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
UMA_HISTOGRAM_BOOLEAN("AutoScreenBrightness.PersonalCurveValid",
is_personal_curve_valid);
const bool has_loaded_and_valid_personal_curve =
model_.personal_curve && is_personal_curve_valid;
DCHECK(model_.global_curve);
DCHECK(trainer_->GetGlobalCurve() == *model_.global_curve);
DCHECK(trainer_->GetCurrentCurve() == (has_loaded_and_valid_personal_curve
? *model_.personal_curve
: *model_.global_curve));
if (!has_loaded_and_valid_personal_curve) {
model_.personal_curve = base::nullopt;
model_.iteration_count = 0;
} else if (model_.iteration_count == 0) {
model_.iteration_count = 1;
}
is_modeller_enabled_ = true;
OnInitializationComplete();
// We may have received a brightness change as a training example before the
// model is set up. Call |ScheduleTrainerStart| to prepare training.
ScheduleTrainerStart();
}
void ModellerImpl::ScheduleTrainerStart() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_modeller_enabled_.has_value() || !*is_modeller_enabled_)
return;
if (data_cache_.size() >= max_training_data_points_ ||
training_delay_.is_zero()) {
model_timer_.Stop();
StartTraining();
return;
}
// Reset the timer if it's already running.
model_timer_.Start(FROM_HERE, training_delay_, this,
&ModellerImpl::StartTraining);
}
void ModellerImpl::StartTraining() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (data_cache_.empty()) {
return;
}
training_start_ = tick_clock_->NowTicks();
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(), FROM_HERE,
base::BindOnce(&TrainModel, trainer_.get(), std::move(data_cache_),
is_testing_),
base::BindOnce(&ModellerImpl::OnTrainingFinished,
weak_ptr_factory_.GetWeakPtr()));
data_cache_ = std::vector<TrainingDataPoint>();
}
void ModellerImpl::OnTrainingFinished(const MonotoneCubicSpline& curve) {
const base::TimeTicks now = tick_clock_->NowTicks();
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
++model_.iteration_count;
for (auto& observer : observers_)
observer.OnModelTrained(curve);
// Save personal curve if it doesn't exist or has been updated.
const bool save_personal_curve =
!model_.personal_curve || *model_.personal_curve != curve;
const std::string histogram_name =
std::string("AutoScreenBrightness.TrainingCompleteDuration.") +
(save_personal_curve ? "NewCurve" : "NoNewCurve");
base::UmaHistogramTimes(histogram_name, now - training_start_.value());
if (save_personal_curve)
model_.personal_curve = curve;
base::PostTaskAndReplyWithResult(
blocking_task_runner_.get(), FROM_HERE,
base::BindOnce(&SaveModelToDisk, model_saving_spec_, model_,
global_curve_reset_, save_personal_curve, is_testing_),
base::BindOnce(&ModellerImpl::OnModelSavedToDisk,
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace auto_screen_brightness
} // namespace power
} // namespace chromeos