blob: 87a40ab630d0b2f34c22f6daa79ef75b56238965 [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 "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "chrome/browser/chromeos/power/auto_screen_brightness/fake_als_reader.h"
#include "chrome/browser/chromeos/power/auto_screen_brightness/fake_brightness_monitor.h"
#include "chrome/browser/chromeos/power/auto_screen_brightness/monotone_cubic_spline.h"
#include "chrome/browser/chromeos/power/auto_screen_brightness/trainer.h"
#include "chrome/browser/chromeos/power/auto_screen_brightness/utils.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/chromeos_features.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/user_activity/user_activity_detector.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
namespace chromeos {
namespace power {
namespace auto_screen_brightness {
namespace {
MonotoneCubicSpline CreateTestCurveFromTrainingData(
const std::vector<TrainingDataPoint>& data) {
CHECK_GT(data.size(), 1u);
std::vector<double> xs;
std::vector<double> ys;
const auto& data_point = data[0];
xs.push_back(data_point.ambient_log_lux);
ys.push_back((data_point.brightness_old + data_point.brightness_new) / 2);
for (size_t i = 1; i < data.size(); ++i) {
xs.push_back(xs[i - 1] + 1);
ys.push_back(ys[i - 1] + 1);
}
return MonotoneCubicSpline(xs, ys);
}
void CheckOptionalCurves(
const base::Optional<MonotoneCubicSpline>& result_curve,
const base::Optional<MonotoneCubicSpline>& expected_curve) {
EXPECT_EQ(result_curve.has_value(), expected_curve.has_value());
if (result_curve) {
EXPECT_EQ(*result_curve, *expected_curve);
}
}
// Fake testing trainer that has configuration status and validity of personal
// curve specified in the constructor.
class FakeTrainer : public Trainer {
public:
FakeTrainer(bool is_configured, bool is_personal_curve_valid)
: is_configured_(is_configured),
is_personal_curve_valid_(is_personal_curve_valid) {
// If personal curve is valid, then the trainer must be configured.
DCHECK(!is_personal_curve_valid_ || is_configured_);
}
~FakeTrainer() override = default;
// Trainer overrides:
bool HasValidConfiguration() const override { return is_configured_; }
bool SetInitialCurves(const MonotoneCubicSpline& global_curve,
const MonotoneCubicSpline& current_curve) override {
DCHECK(is_configured_);
global_curve_.emplace(global_curve);
current_curve_.emplace(is_personal_curve_valid_ ? current_curve
: global_curve);
return is_personal_curve_valid_;
}
MonotoneCubicSpline GetGlobalCurve() const override {
DCHECK(is_configured_);
DCHECK(global_curve_);
return *global_curve_;
}
MonotoneCubicSpline GetCurrentCurve() const override {
DCHECK(is_configured_);
DCHECK(current_curve_);
return *current_curve_;
}
MonotoneCubicSpline Train(
const std::vector<TrainingDataPoint>& data) override {
DCHECK(is_configured_);
DCHECK(current_curve_);
std::vector<TrainingDataPoint> used_data = data;
// We need at least 2 points to create a MonotoneCubicSpline. Hence we
// insert another one if |data| has only 1 point.
if (data.size() == 1) {
used_data.push_back(data[0]);
}
current_curve_.emplace(CreateTestCurveFromTrainingData(used_data));
return *current_curve_;
}
private:
bool is_configured_;
bool is_personal_curve_valid_;
base::Optional<MonotoneCubicSpline> global_curve_;
base::Optional<MonotoneCubicSpline> current_curve_;
DISALLOW_COPY_AND_ASSIGN(FakeTrainer);
};
class TestObserver : public Modeller::Observer {
public:
TestObserver() {}
~TestObserver() override = default;
// Modeller::Observer overrides:
void OnModelTrained(const MonotoneCubicSpline& brightness_curve) override {
personal_curve_.emplace(brightness_curve);
trained_curve_received_ = true;
}
void OnModelInitialized(
const base::Optional<MonotoneCubicSpline>& global_curve,
const base::Optional<MonotoneCubicSpline>& personal_curve) override {
model_initialized_ = true;
if (global_curve)
global_curve_.emplace(*global_curve);
if (personal_curve)
personal_curve_.emplace(*personal_curve);
}
base::Optional<MonotoneCubicSpline> global_curve() const {
return global_curve_;
}
base::Optional<MonotoneCubicSpline> personal_curve() const {
return personal_curve_;
}
void CheckStatus(bool is_model_initialized,
const base::Optional<MonotoneCubicSpline>& global_curve,
const base::Optional<MonotoneCubicSpline>& personal_curve) {
EXPECT_EQ(is_model_initialized, model_initialized_);
CheckOptionalCurves(global_curve, global_curve_);
CheckOptionalCurves(personal_curve, personal_curve_);
}
bool trained_curve_received() { return trained_curve_received_; }
private:
bool model_initialized_ = false;
base::Optional<MonotoneCubicSpline> global_curve_;
base::Optional<MonotoneCubicSpline> personal_curve_;
bool trained_curve_received_ = false;
DISALLOW_COPY_AND_ASSIGN(TestObserver);
};
} // namespace
class ModellerImplTest : public testing::Test {
public:
ModellerImplTest()
: thread_bundle_(
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME) {
CHECK(temp_dir_.CreateUniqueTempDir());
TestingProfile::Builder profile_builder;
profile_builder.SetProfileName("testuser@gmail.com");
profile_builder.SetPath(temp_dir_.GetPath().AppendASCII("TestProfile"));
profile_ = profile_builder.Build();
}
~ModellerImplTest() override {
base::TaskScheduler::GetInstance()->FlushForTesting();
}
// Sets up |modeller_| with a FakeTrainer.
void SetUpModeller(bool is_trainer_configured, bool is_personal_curve_valid) {
modeller_ = ModellerImpl::CreateForTesting(
profile_.get(), &fake_als_reader_, &fake_brightness_monitor_,
&user_activity_detector_,
std::make_unique<FakeTrainer>(is_trainer_configured,
is_personal_curve_valid),
base::SequencedTaskRunnerHandle::Get(),
thread_bundle_.GetMockTickClock());
test_observer_ = std::make_unique<TestObserver>();
modeller_->AddObserver(test_observer_.get());
}
void Init(AlsReader::AlsInitStatus als_reader_status,
BrightnessMonitor::Status brightness_monitor_status,
bool is_trainer_configured = true,
bool is_personal_curve_valid = true) {
fake_als_reader_.set_als_init_status(als_reader_status);
fake_brightness_monitor_.set_status(brightness_monitor_status);
SetUpModeller(is_trainer_configured, is_personal_curve_valid);
thread_bundle_.RunUntilIdle();
}
protected:
void WriteCurveToFile(const MonotoneCubicSpline& curve) {
const base::FilePath curve_path =
ModellerImpl::ModellerImpl::GetCurvePathFromProfile(profile_.get());
CHECK(!curve_path.empty());
const std::string data = curve.ToString();
const int bytes_written =
base::WriteFile(curve_path, data.data(), data.size());
ASSERT_EQ(bytes_written, static_cast<int>(data.size()))
<< "Wrote " << bytes_written << " byte(s) instead of " << data.size()
<< " to " << curve_path;
}
content::TestBrowserThreadBundle thread_bundle_;
base::HistogramTester histogram_tester_;
ui::UserActivityDetector user_activity_detector_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<TestingProfile> profile_;
FakeAlsReader fake_als_reader_;
FakeBrightnessMonitor fake_brightness_monitor_;
std::unique_ptr<ModellerImpl> modeller_;
std::unique_ptr<TestObserver> test_observer_;
private:
DISALLOW_COPY_AND_ASSIGN(ModellerImplTest);
};
// AlsReader is |kDisabled| when Modeller is created.
TEST_F(ModellerImplTest, AlsReaderDisabledOnInit) {
Init(AlsReader::AlsInitStatus::kDisabled,
BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
}
// BrightnessMonitor is |kDisabled| when Modeller is created.
TEST_F(ModellerImplTest, BrightnessMonitorDisabledOnInit) {
Init(AlsReader::AlsInitStatus::kSuccess,
BrightnessMonitor::Status::kDisabled);
test_observer_->CheckStatus(true /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
}
// AlsReader is |kDisabled| on later notification.
TEST_F(ModellerImplTest, AlsReaderDisabledOnNotification) {
Init(AlsReader::AlsInitStatus::kInProgress,
BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(false /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
fake_als_reader_.set_als_init_status(AlsReader::AlsInitStatus::kDisabled);
fake_als_reader_.ReportReaderInitialized();
thread_bundle_.RunUntilIdle();
test_observer_->CheckStatus(true /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
}
// AlsReader is |kSuccess| on later notification.
TEST_F(ModellerImplTest, AlsReaderEnabledOnNotification) {
Init(AlsReader::AlsInitStatus::kInProgress,
BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(false /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
fake_als_reader_.set_als_init_status(AlsReader::AlsInitStatus::kSuccess);
fake_als_reader_.ReportReaderInitialized();
thread_bundle_.RunUntilIdle();
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
}
// BrightnessMonitor is |kDisabled| on later notification.
TEST_F(ModellerImplTest, BrightnessMonitorDisabledOnNotification) {
Init(AlsReader::AlsInitStatus::kSuccess,
BrightnessMonitor::Status::kInitializing);
test_observer_->CheckStatus(false /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
fake_brightness_monitor_.set_status(BrightnessMonitor::Status::kDisabled);
fake_brightness_monitor_.ReportBrightnessMonitorInitialized();
test_observer_->CheckStatus(true /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
}
// BrightnessMonitor is |kSuccess| on later notification.
TEST_F(ModellerImplTest, BrightnessMonitorEnabledOnNotification) {
Init(AlsReader::AlsInitStatus::kSuccess,
BrightnessMonitor::Status::kInitializing);
test_observer_->CheckStatus(false /* is_model_initialized */,
base::nullopt /* global_curve */,
base::nullopt /* personal_curve */);
fake_brightness_monitor_.set_status(BrightnessMonitor::Status::kSuccess);
fake_brightness_monitor_.ReportBrightnessMonitorInitialized();
thread_bundle_.RunUntilIdle();
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
}
// There is no saved curve, hence a global curve is created.
TEST_F(ModellerImplTest, NoSavedCurve) {
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
}
// A curve is loaded from disk, this is a personal curve.
TEST_F(ModellerImplTest, CurveLoadedFromProfilePath) {
const std::vector<double> xs = {0, 10, 20, 40, 60, 80, 90, 100};
const std::vector<double> ys = {0, 5, 10, 15, 20, 25, 30, 40};
MonotoneCubicSpline curve(xs, ys);
WriteCurveToFile(curve);
thread_bundle_.RunUntilIdle();
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(), curve);
histogram_tester_.ExpectUniqueSample(
"AutoScreenBrightness.PersonalCurveValid", true, 1);
}
// A curve is loaded from disk, this is a personal curve. This personal curve
// doesn't satisfy Trainer slope constraint, hence it's ignored and the global
// curve is used instead.
TEST_F(ModellerImplTest, PersonalCurveError) {
const std::vector<double> xs = {0, 10, 20, 40, 60, 80, 90, 100};
const std::vector<double> ys = {0, 5, 10, 15, 20, 25, 30, 40};
MonotoneCubicSpline curve(xs, ys);
WriteCurveToFile(curve);
thread_bundle_.RunUntilIdle();
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
true /* is_trainer_configured */, false /* is_personal_curve_valid */);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
histogram_tester_.ExpectUniqueSample(
"AutoScreenBrightness.PersonalCurveValid", false, 1);
}
// Ambient light values are received. We check average ambient light has been
// calculated from the past |kNumberAmbientValuesToTrack| samples only.
TEST_F(ModellerImplTest, OnAmbientLightUpdated) {
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
const int first_lux = 1000;
double running_sum = first_lux;
fake_als_reader_.ReportAmbientLightUpdate(first_lux);
for (int i = 1; i < ModellerImpl::kNumberAmbientValuesToTrack; ++i) {
const int lux = i;
fake_als_reader_.ReportAmbientLightUpdate(lux);
running_sum += lux;
EXPECT_DOUBLE_EQ(running_sum / (i + 1),
modeller_->AverageAmbientForTesting());
}
// Add another one should push the oldest |first_lux| out of the horizon.
fake_als_reader_.ReportAmbientLightUpdate(100);
running_sum = running_sum + 100 - first_lux;
EXPECT_DOUBLE_EQ(running_sum / ModellerImpl::kNumberAmbientValuesToTrack,
modeller_->AverageAmbientForTesting());
}
// User brightness changes are received, training example cache reaches
// |max_training_data_points_| to trigger early training. This all happens
// within a small window shorter than |training_delay_|.
TEST_F(ModellerImplTest, OnUserBrightnessChanged) {
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
std::vector<TrainingDataPoint> expected_data;
for (size_t i = 0; i < modeller_->GetMaxTrainingDataPointsForTesting() - 1;
++i) {
EXPECT_EQ(i, modeller_->NumberTrainingDataPointsForTesting());
thread_bundle_.FastForwardBy(base::TimeDelta::FromMilliseconds(1));
const base::TimeTicks now = thread_bundle_.GetMockTickClock()->NowTicks();
const int lux = i * 20;
fake_als_reader_.ReportAmbientLightUpdate(lux);
const double brightness_old = 10.0 + i;
const double brightness_new = 20.0 + i;
modeller_->OnUserBrightnessChanged(brightness_old, brightness_new);
expected_data.push_back(
{brightness_old, brightness_new,
ConvertToLog(modeller_->AverageAmbientForTesting()), now});
}
// Training should not have started.
EXPECT_EQ(modeller_->GetMaxTrainingDataPointsForTesting() - 1,
modeller_->NumberTrainingDataPointsForTesting());
// Add one more data point to trigger the training early.
thread_bundle_.FastForwardBy(base::TimeDelta::FromMilliseconds(1));
const base::TimeTicks now = thread_bundle_.GetMockTickClock()->NowTicks();
const double brightness_old = 85;
const double brightness_new = 95;
modeller_->OnUserBrightnessChanged(brightness_old, brightness_new);
expected_data.push_back({brightness_old, brightness_new,
ConvertToLog(modeller_->AverageAmbientForTesting()),
now});
thread_bundle_.RunUntilIdle();
EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
const base::Optional<MonotoneCubicSpline>& result_curve =
test_observer_->personal_curve();
DCHECK(result_curve);
const MonotoneCubicSpline expected_curve =
CreateTestCurveFromTrainingData(expected_data);
EXPECT_EQ(expected_curve, *result_curve);
}
// User activities resets timer used to start training.
TEST_F(ModellerImplTest, MultipleUserActivities) {
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
fake_als_reader_.ReportAmbientLightUpdate(30);
std::vector<TrainingDataPoint> expected_data;
for (size_t i = 0; i < 10; ++i) {
EXPECT_EQ(i, modeller_->NumberTrainingDataPointsForTesting());
thread_bundle_.FastForwardBy(base::TimeDelta::FromMilliseconds(1));
const base::TimeTicks now = thread_bundle_.GetMockTickClock()->NowTicks();
const int lux = i * 20;
fake_als_reader_.ReportAmbientLightUpdate(lux);
const double brightness_old = 10.0 + i;
const double brightness_new = 20.0 + i;
modeller_->OnUserBrightnessChanged(brightness_old, brightness_new);
expected_data.push_back(
{brightness_old, brightness_new,
ConvertToLog(modeller_->AverageAmbientForTesting()), now});
}
EXPECT_EQ(modeller_->NumberTrainingDataPointsForTesting(), 10u);
thread_bundle_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 2);
// A user activity is received, timer should be reset.
const ui::MouseEvent mouse_event(ui::ET_MOUSE_EXITED, gfx::Point(0, 0),
gfx::Point(0, 0), base::TimeTicks(), 0, 0);
modeller_->OnUserActivity(&mouse_event);
thread_bundle_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 3);
EXPECT_EQ(modeller_->NumberTrainingDataPointsForTesting(), 10u);
// Another user event is received.
modeller_->OnUserActivity(&mouse_event);
// After |training_delay_|/2, no training has started.
thread_bundle_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 2);
EXPECT_EQ(modeller_->NumberTrainingDataPointsForTesting(), 10u);
// After another |training_delay_|/2, training is scheduled.
thread_bundle_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 2);
EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
const base::Optional<MonotoneCubicSpline>& result_curve =
test_observer_->personal_curve();
DCHECK(result_curve);
const MonotoneCubicSpline expected_curve =
CreateTestCurveFromTrainingData(expected_data);
EXPECT_EQ(expected_curve, *result_curve);
}
// Global curve specified by valid experiment parameter.
TEST_F(ModellerImplTest, GlobaCurveFromValidExperimentParam) {
const std::string global_curve_spec("1,10\n2,20\n3,30");
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
features::kAutoScreenBrightness, {{"global_curve", global_curve_spec}});
const MonotoneCubicSpline expected_global_curve({1, 2, 3}, {10, 20, 30});
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
EXPECT_EQ(modeller_->GetGlobalCurveForTesting(), expected_global_curve);
test_observer_->CheckStatus(true /* is_model_initialized */,
expected_global_curve,
base::nullopt /* personal_curve */);
}
// Global curve specified by invalid experiment parameter leads to default curve
// instead.
TEST_F(ModellerImplTest, GlobaCurveFromInvalidExperimentParam) {
const std::string global_curve_spec("1,10");
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
features::kAutoScreenBrightness, {{"global_curve", global_curve_spec}});
// Defined by default values.
const MonotoneCubicSpline expected_global_curve(
{
3.69, 4.83, 6.54, 7.68, 8.25, 8.82,
},
{
36.14, 47.62, 85.83, 93.27, 93.27, 100,
});
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
EXPECT_EQ(modeller_->GetGlobalCurveForTesting(), expected_global_curve);
test_observer_->CheckStatus(true /* is_model_initialized */,
expected_global_curve,
base::nullopt /* personal_curve */);
}
// Training delay is 0, hence we train as soon as we have 1 data point.
TEST_F(ModellerImplTest, ZeroTrainingDelay) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
features::kAutoScreenBrightness, {
{"training_delay_in_seconds", "0"},
});
Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess);
test_observer_->CheckStatus(true /* is_model_initialized */,
modeller_->GetGlobalCurveForTesting(),
base::nullopt /* personal_curve */);
fake_als_reader_.ReportAmbientLightUpdate(30);
const ui::MouseEvent mouse_event(ui::ET_MOUSE_EXITED, gfx::Point(0, 0),
gfx::Point(0, 0), base::TimeTicks(), 0, 0);
modeller_->OnUserActivity(&mouse_event);
modeller_->OnUserBrightnessChanged(10, 20);
thread_bundle_.RunUntilIdle();
EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
EXPECT_TRUE(test_observer_->trained_curve_received());
}
} // namespace auto_screen_brightness
} // namespace power
} // namespace chromeos