chromium/chrome/browser/ash/power/auto_screen_brightness/modeller_impl.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/power/auto_screen_brightness/modeller_impl.h"

#include <cmath>

#include "ash/constants/ash_features.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.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/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/ash/power/auto_screen_brightness/utils.h"
#include "content/public/browser/browser_thread.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"

namespace ash {
namespace power {
namespace auto_screen_brightness {

namespace {

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class ModelLoadingStatus {
  // Global curve, personal curve and model iteration count are all loaded
  // successfully.
  kSuccess = 0,
  // Global curve data is missing.
  kMissingGlobal = 1,
  // Global curve data exists but cannot be used to create a curve.
  kIllFormattedGlobal = 2,
  // Personal curve data is missing.
  kMissingPersonal = 3,
  // Personal curve data exists but cannot be used to create a curve.
  kIllFormattedPersonal = 4,
  // Model iteration count is missing or is invalid.
  kMissingIterationCount = 5,
  kMaxValue = kMissingIterationCount
};

void LogModelLoadingStatus(ModelLoadingStatus status) {
  UMA_HISTOGRAM_ENUMERATION("AutoScreenBrightness.ModelLoadingStatus", status);
  VLOG(1) << "ABModel model loading status: " << static_cast<int>(status);
}

// 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.
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)) {
    LogModelLoadingStatus(ModelLoadingStatus::kMissingGlobal);
    return loaded_model;
  }
  loaded_model.global_curve = MonotoneCubicSpline::FromString(content);
  if (!loaded_model.global_curve) {
    LogModelLoadingStatus(ModelLoadingStatus::kIllFormattedGlobal);
    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)) {
    LogModelLoadingStatus(ModelLoadingStatus::kMissingPersonal);
    return loaded_model;
  }
  loaded_model.personal_curve = MonotoneCubicSpline::FromString(content);
  if (!loaded_model.personal_curve) {
    LogModelLoadingStatus(ModelLoadingStatus::kIllFormattedPersonal);
    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)) {
    LogModelLoadingStatus(ModelLoadingStatus::kMissingIterationCount);
    return loaded_model;
  }
  loaded_model.iteration_count = iteration_count;

  LogModelLoadingStatus(ModelLoadingStatus::kSuccess);
  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) {
  if (!base::WriteFile(path, data)) {
    LOG(ERROR) << "Writing to " << path.value() << " failed.";
    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).
TrainingResult 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 std::optional<MonotoneCubicSpline>& global_curve,
             const std::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::ThreadPool::CreateSequencedTaskRunner(
                       {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 std::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(
    std::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 */));
}

std::optional<double> ModellerImpl::AverageAmbientForTesting(
    base::TimeTicks now) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(log_als_values_);
  const std::optional<AlsAvgStdDev> log_als_avg_stddev =
      log_als_values_->AverageAmbientWithStdDev(now);
  if (!log_als_avg_stddev)
    return std::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::GetModelSavingSpecFromProfilePath(
    const base::FilePath& profile_path) {
  ModelSavingSpec model_saving_spec;
  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)) {
    VLOG(1) << "ABModel auto screen brightness model dir does not exist.";
    return model_saving_spec;
  }

  VLOG(1) << "ABModel auto screen brightness model dir: " << model_dir.value();
  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),
      blocking_task_runner_(blocking_task_runner),
      trainer_(trainer.release(),
               base::OnTaskRunnerDeleter(blocking_task_runner_)),
      tick_clock_(tick_clock),
      model_timer_(tick_clock_) {
  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;
  }

  als_reader_observation_.Observe(als_reader);
  brightness_monitor_observation_.Observe(brightness_monitor);
  model_config_loader_observation_.Observe(model_config_loader);

  user_activity_observation_.Observe(user_activity_detector);

  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&ModellerImpl::GetModelSavingSpecFromProfilePath,
                     profile->GetPath()),
      base::BindOnce(&ModellerImpl::OnModelSavingSpecReadFromProfile,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ModellerImpl::OnModelSavingSpecReadFromProfile(
    const ModelSavingSpec& spec) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!model_saving_spec_.has_value());

  model_saving_spec_ = spec;
  HandleStatusUpdate();
}

void ModellerImpl::HandleStatusUpdate() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (is_modeller_enabled_.has_value())
    return;

  if (!model_saving_spec_.has_value())
    return;

  if (model_saving_spec_->global_curve.empty()) {
    is_modeller_enabled_ = false;
    OnInitializationComplete();
    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;
  }

  blocking_task_runner_->PostTaskAndReplyWithResult(
      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::Seconds(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::Seconds(training_delay_in_seconds);
  }

  curve_error_tolerance_ = GetFieldTrialParamByFeatureAsDouble(
      features::kAutoScreenBrightness, "curve_error_tolerance",
      curve_error_tolerance_);

  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());

  UMA_HISTOGRAM_COUNTS_1000(
      "AutoScreenBrightness.ModelIterationCountAtInitialization",
      model_.iteration_count);

  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 and erase personal curve from |model_| if it exists.
    model_.global_curve = initial_global_curve_;
    ErasePersonalCurve();
    global_curve_reset_ = true;
    VLOG(1) << "ABModel global curve reset";
  }
  UMA_HISTOGRAM_BOOLEAN("AutoScreenBrightness.GlobalCurveResetOnInitialization",
                        global_curve_reset_);

  DCHECK(model_.global_curve);
  // Run SetInitialCurves calculations on background thread to avoid blocking UI
  // thread.
  blocking_task_runner_->PostTaskAndReplyWithResult(
      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);
  VLOG(1) << "ABModel initial personal curve valid: "
          << 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) {
    ErasePersonalCurve();
  } 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();
  blocking_task_runner_->PostTaskAndReplyWithResult(
      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 TrainingResult& result) {
  const base::TimeTicks now = tick_clock_->NowTicks();
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Only export the curve if there's a new curve and the error is small.
  // "Export" means we update personal curve in |model_| and notify observers.
  const bool export_personal_curve = result.new_curve &&
                                     result.error <= curve_error_tolerance_ &&
                                     result.new_curve != model_.personal_curve;

  if (export_personal_curve) {
    ++model_.iteration_count;
    model_.personal_curve = result.new_curve;
    for (auto& observer : observers_)
      observer.OnModelTrained(*result.new_curve);
  }

  VLOG(1) << "ABModel training finished (has_new_curve,error,updated): "
          << result.new_curve.has_value() << ", " << FormatToPrint(result.error)
          << ", " << export_personal_curve;

  const std::string histogram_name =
      std::string("AutoScreenBrightness.TrainingCompleteDuration.") +
      (export_personal_curve ? "NewCurve" : "NoNewCurve");
  base::UmaHistogramTimes(histogram_name, now - training_start_.value());

  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&SaveModelToDisk, *model_saving_spec_, model_,
                     global_curve_reset_, export_personal_curve, is_testing_),
      base::BindOnce(&ModellerImpl::OnModelSavedToDisk,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ModellerImpl::ErasePersonalCurve() {
  model_.personal_curve = std::nullopt;
  model_.iteration_count = 0;
}

}  // namespace auto_screen_brightness
}  // namespace power
}  // namespace ash