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