Commit 4e966936 authored by Charles Zhao's avatar Charles Zhao Committed by Commit Bot

hindsight: CrOSActionRecorder.

This is the first CL of hindsight which creates the CrOSActionRecorder

(1) CrOSActionRecorder is a singleton,

(2) CrOSActionRecorder has one public function RecordAction.

(3) CrOSActionRecorder writes action history periodically to disk.

Bug: 1012936
Change-Id: Ie364e98cc5eb099a5743b27134ec3bc10363d142
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1846612
Commit-Queue: Charles . <charleszhao@chromium.org>
Reviewed-by: default avatarTony Yeoman <tby@chromium.org>
Reviewed-by: default avatarCharles . <charleszhao@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#705703}
parent 866c2997
......@@ -3516,6 +3516,8 @@ jumbo_split_static_library("ui") {
"app_list/search/common/file_icon_util.h",
"app_list/search/common/url_icon_source.cc",
"app_list/search/common/url_icon_source.h",
"app_list/search/cros_action_history/cros_action_recorder.cc",
"app_list/search/cros_action_history/cros_action_recorder.h",
"app_list/search/drive_quick_access_provider.cc",
"app_list/search/drive_quick_access_provider.h",
"app_list/search/drive_quick_access_result.cc",
......@@ -3591,6 +3593,7 @@ jumbo_split_static_library("ui") {
"//ash/app_list",
"//ash/public/cpp/app_list/vector_icons",
"//ash/resources/vector_icons",
"//chrome/browser/ui/app_list/search/cros_action_history:cros_action_proto",
"//chrome/browser/ui/app_list/search/search_result_ranker:app_launch_event_logger_proto",
"//chrome/browser/ui/app_list/search/search_result_ranker:app_launch_predictor_proto",
"//chrome/browser/ui/app_list/search/search_result_ranker:app_list_launch_recorder_proto",
......
# 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.
import("//third_party/protobuf/proto_library.gni")
proto_library("cros_action_proto") {
sources = [
"cros_action.proto",
]
}
charleszhao@chromium.org
jiameng@chromium.org
xiyuan@chromium.org
tby@chromium.org
// Copyright 2019 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.
syntax = "proto2";
option optimize_for = LITE_RUNTIME;
package app_list;
// CrOSActionProto records one individual action.
message CrOSActionProto {
// Name of the action.
optional string action_name = 1;
// Time of the action.
optional int64 secs_since_epoch = 2;
// CrOSActionConditionProto records other features that may useful for this
// action.
message CrOSActionConditionProto {
// name of the condition.
optional string name = 1;
// value of the condition.
optional int32 value = 2;
}
repeated CrOSActionConditionProto conditions = 3;
}
// A history consists a list of actions.
message CrOSActionHistoryProto {
repeated CrOSActionProto actions = 1;
}
// Copyright 2019 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/ui/app_list/search/cros_action_history/cros_action_recorder.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/metrics/metrics_hashes.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/post_task.h"
#include "base/threading/scoped_blocking_call.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/app_list/search/cros_action_history/cros_action.pb.h"
namespace app_list {
namespace {
constexpr int kSecondsPerDay = 86400;
void SaveToDiskOnWorkerThread(const CrOSActionHistoryProto actions,
const base::FilePath action_filepath) {
// Loads proto string from local disk.
std::string proto_str;
if (!base::ReadFileToString(action_filepath, &proto_str))
proto_str.clear();
CrOSActionHistoryProto actions_to_write;
if (!actions_to_write.ParseFromString(proto_str))
actions_to_write.Clear();
actions_to_write.MergeFrom(actions);
const std::string proto_str_to_write = actions_to_write.SerializeAsString();
// Create directory if it's not there yet.
const bool create_directory_sucess =
base::CreateDirectory(action_filepath.DirName());
DCHECK(create_directory_sucess)
<< "Error create directory for " << action_filepath;
const bool write_success = base::ImportantFileWriter::WriteFileAtomically(
action_filepath, proto_str_to_write, "CrOSActionHistory");
DCHECK(write_success) << "Error writing action_file " << action_filepath;
}
} // namespace
constexpr char CrOSActionRecorder::kActionHistoryDir[];
constexpr base::TimeDelta CrOSActionRecorder::kSaveInternal;
CrOSActionRecorder::CrOSActionRecorder()
: should_log_(false),
should_hash_(true),
last_save_timestamp_(base::Time::Now()) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Profile* profile = ProfileManager::GetPrimaryUserProfile();
if (profile) {
profile_path_ = profile->GetPath();
} else {
// If profile_path_ is not set, then there is no point to record anything.
should_log_ = false;
}
task_runner_ = base::CreateSequencedTaskRunner(
{base::ThreadPool(), base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
}
CrOSActionRecorder::~CrOSActionRecorder() = default;
CrOSActionRecorder* CrOSActionRecorder::GetCrosActionRecorder() {
static base::NoDestructor<CrOSActionRecorder> recorder;
return recorder.get();
}
void CrOSActionRecorder::RecordAction(
const CrOSAction& action,
const std::vector<std::pair<std::string, int>>& conditions) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!should_log_)
return;
CrOSActionProto& cros_action_proto = *actions_.add_actions();
// Record action.
cros_action_proto.set_action_name(
MaybeHashed(std::get<0>(action), should_hash_));
cros_action_proto.set_secs_since_epoch(base::Time::Now().ToDoubleT());
// Record conditions.
for (const auto& pair : conditions) {
auto& condition = *cros_action_proto.add_conditions();
condition.set_name(MaybeHashed(pair.first, should_hash_));
condition.set_value(pair.second);
}
// May flush to disk.
MaybeFlushToDisk();
}
void CrOSActionRecorder::MaybeFlushToDisk() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const base::Time now = base::Time::Now();
if (now - last_save_timestamp_ >= kSaveInternal) {
last_save_timestamp_ = now;
// Writes the predictor proto to disk asynchronously.
const std::string day = base::NumberToString(
static_cast<int>(now.ToDoubleT() / kSecondsPerDay));
const base::FilePath action_filepath =
profile_path_.Append(CrOSActionRecorder::kActionHistoryDir).Append(day);
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&SaveToDiskOnWorkerThread,
std::move(actions_), action_filepath));
actions_.Clear();
}
}
std::string CrOSActionRecorder::MaybeHashed(const std::string& input,
const bool should_hash) {
return should_hash ? base::NumberToString(base::HashMetricName(input))
: input;
}
} // namespace app_list
// Copyright 2019 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.
#ifndef CHROME_BROWSER_UI_APP_LIST_SEARCH_CROS_ACTION_HISTORY_CROS_ACTION_RECORDER_H_
#define CHROME_BROWSER_UI_APP_LIST_SEARCH_CROS_ACTION_HISTORY_CROS_ACTION_RECORDER_H_
#include <cstdint>
#include <string>
#include <tuple>
#include <vector>
#include "base/files/file_path.h"
#include "base/memory/scoped_refptr.h"
#include "base/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/ui/app_list/search/cros_action_history/cros_action.pb.h"
namespace app_list {
class CrOSActionHistoryProto;
// CrOSActionRecorder is a singleton that used to record any CrOSAction.
// CrOSAction may contains:
// (1) App-launchings.
// (2) File-openings.
// (3) Settings.
// (4) Tab-navigations.
// and so on.
class CrOSActionRecorder {
public:
using CrOSActionName = std::string;
using CrOSAction = std::tuple<CrOSActionName>;
CrOSActionRecorder();
~CrOSActionRecorder();
// Get the pointer of the singleton.
static CrOSActionRecorder* GetCrosActionRecorder();
// Record a user |action| with |conditions|.
void RecordAction(
const CrOSAction& action,
const std::vector<std::pair<std::string, int>>& conditions = {});
private:
friend class CrOSActionRecorderTest;
// kSaveInternal controls how often we save the action history to disk.
static constexpr base::TimeDelta kSaveInternal =
base::TimeDelta::FromHours(1);
// The sub-directory in profile path where the action history is stored.
static constexpr char kActionHistoryDir[] = "cros_action_history/";
// Saves the current |actions_| to disk and clear it when certain
// criteria is met.
void MaybeFlushToDisk();
// Hashes the |input| if |should_hash| is true; otherwise return |input|.
static std::string MaybeHashed(const std::string& input, bool should_hash);
// Controls whether the logging is enabled.
bool should_log_ = false;
// Controls whether to hash the action and condition names before log.
bool should_hash_ = true;
// The timestamp of last save to disk.
base::Time last_save_timestamp_;
// A list of actions since last save.
CrOSActionHistoryProto actions_;
// Profile path to save the actions.
base::FilePath profile_path_;
scoped_refptr<base::SequencedTaskRunner> task_runner_;
SEQUENCE_CHECKER(sequence_checker_);
DISALLOW_COPY_AND_ASSIGN(CrOSActionRecorder);
};
} // namespace app_list
#endif // CHROME_BROWSER_UI_APP_LIST_SEARCH_CROS_ACTION_HISTORY_CROS_ACTION_RECORDER_H_
// Copyright 2019 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/ui/app_list/search/cros_action_history/cros_action_recorder.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_mock_clock_override.h"
#include "base/test/task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace app_list {
namespace {
constexpr int kSecondsPerDay = 86400;
// Condition values start from this number for tests.
constexpr int kConditionValue = 11;
} // namespace
// Test functions of CrOSActionRecorder.
class CrOSActionRecorderTest : public testing::Test {
protected:
void SetUp() override {
Test::SetUp();
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
profile_path_ = temp_dir_.GetPath();
actions_ = {"Action0", "Action1", "Action2"};
conditions_ = {"Condition0", "Condition1", "Condition2"};
save_internal_secs_ = CrOSActionRecorder::kSaveInternal.InSeconds();
// Set time_ to be base::Time::UnixEpoch().
time_.Advance(base::TimeDelta::FromSeconds(11612937600));
// Reset the CrOSActionRecorder to be default.
CrOSActionRecorder::GetCrosActionRecorder()->actions_.Clear();
CrOSActionRecorder::GetCrosActionRecorder()->last_save_timestamp_ =
base::Time::UnixEpoch();
CrOSActionRecorder::GetCrosActionRecorder()->should_log_ = false;
CrOSActionRecorder::GetCrosActionRecorder()->should_hash_ = true;
CrOSActionRecorder::GetCrosActionRecorder()->profile_path_ = profile_path_;
}
CrOSActionHistoryProto GetCrOSActionHistory() {
return CrOSActionRecorder::GetCrosActionRecorder()->actions_;
}
void EnableLog() {
CrOSActionRecorder::GetCrosActionRecorder()->should_log_ = true;
}
void DisableHash() {
CrOSActionRecorder::GetCrosActionRecorder()->should_hash_ = false;
}
// Read and Parse log from |day|-th file.
CrOSActionHistoryProto ReadLog(const int day) {
const base::FilePath action_file_path =
profile_path_.Append(CrOSActionRecorder::kActionHistoryDir)
.Append(base::NumberToString(day));
std::string proto_str;
CHECK(base::ReadFileToString(action_file_path, &proto_str));
CrOSActionHistoryProto actions_history;
CHECK(actions_history.ParseFromString(proto_str));
return actions_history;
}
// Expects |action| to be actions_[i], with certain features.
void ExpectCrOSAction(const CrOSActionProto& action,
const int i,
const int64_t secs_since_epoch,
const bool should_hash = true) {
EXPECT_EQ(action.action_name(),
CrOSActionRecorder::MaybeHashed(actions_[i], should_hash));
EXPECT_EQ(action.secs_since_epoch(), secs_since_epoch);
EXPECT_EQ(action.conditions_size(), 1);
const CrOSActionProto::CrOSActionConditionProto& condition =
action.conditions(0);
EXPECT_EQ(condition.name(),
CrOSActionRecorder::MaybeHashed(conditions_[i], should_hash));
EXPECT_EQ(condition.value(), i + kConditionValue);
}
void Wait() { task_environment_.RunUntilIdle(); }
std::vector<std::string> actions_;
std::vector<std::string> conditions_;
base::ScopedTempDir temp_dir_;
base::FilePath profile_path_;
int64_t save_internal_secs_ = 0;
base::ScopedMockClockOverride time_;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::DEFAULT,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
};
// Log is disabled by default.
TEST_F(CrOSActionRecorderTest, NoLogAsDefault) {
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction({actions_[0]});
EXPECT_TRUE(GetCrOSActionHistory().actions().empty());
}
// Log is hashed by default.
TEST_F(CrOSActionRecorderTest, HashActionNameByDefault) {
EnableLog();
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[0]}, {{conditions_[0], kConditionValue}});
const CrOSActionHistoryProto& action_history = GetCrOSActionHistory();
EXPECT_EQ(action_history.actions_size(), 1);
ExpectCrOSAction(action_history.actions(0), 0, 0);
}
// DisableHash will log action name and condition name explicitly.
TEST_F(CrOSActionRecorderTest, DisableHashToLogExplicitly) {
EnableLog();
DisableHash();
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[0]}, {{conditions_[0], kConditionValue}});
const CrOSActionHistoryProto& action_history = GetCrOSActionHistory();
EXPECT_EQ(action_history.actions_size(), 1);
ExpectCrOSAction(action_history.actions(0), 0, 0, false);
}
// Check a new file is written every day for expected values.
TEST_F(CrOSActionRecorderTest, WriteToNewFileEveryDay) {
EnableLog();
time_.Advance(base::TimeDelta::FromSeconds(save_internal_secs_));
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[0]}, {{conditions_[0], kConditionValue}});
Wait();
// Expect the GetCrOSActionHistory() is already cleared.
EXPECT_TRUE(GetCrOSActionHistory().actions().empty());
// Expect the log to have correct values.
const CrOSActionHistoryProto action_history_0 = ReadLog(0);
EXPECT_EQ(action_history_0.actions_size(), 1);
ExpectCrOSAction(action_history_0.actions(0), 0, save_internal_secs_);
// Advance for 1 day.
time_.Advance(base::TimeDelta::FromSeconds(kSecondsPerDay));
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[1]}, {{conditions_[1], kConditionValue + 1}});
Wait();
// Expect the GetCrOSActionHistory() is already cleared.
EXPECT_TRUE(GetCrOSActionHistory().actions().empty());
// Expect the new log file to have correct values.
const CrOSActionHistoryProto action_history_1 = ReadLog(1);
EXPECT_EQ(action_history_1.actions_size(), 1);
ExpectCrOSAction(action_history_1.actions(0), 1,
save_internal_secs_ + kSecondsPerDay);
}
// Check that the result is appended to previous log within a day.
TEST_F(CrOSActionRecorderTest, AppendToFileEverySaveInAday) {
EnableLog();
time_.Advance(base::TimeDelta::FromSeconds(save_internal_secs_));
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[0]}, {{conditions_[0], kConditionValue}});
Wait();
// Expect the GetCrOSActionHistory() is already cleared.
EXPECT_TRUE(GetCrOSActionHistory().actions().empty());
// Expect the log has correct values.
const CrOSActionHistoryProto action_history_0 = ReadLog(0);
EXPECT_EQ(action_history_0.actions_size(), 1);
ExpectCrOSAction(action_history_0.actions(0), 0, save_internal_secs_);
// Advance for 1 kSaveInternal.
time_.Advance(base::TimeDelta::FromSeconds(save_internal_secs_));
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[1]}, {{conditions_[1], kConditionValue + 1}});
Wait();
// Expect the GetCrOSActionHistory() is already cleared.
EXPECT_TRUE(GetCrOSActionHistory().actions().empty());
// Expect the log to have correct values (two actions).
const CrOSActionHistoryProto action_history_1 = ReadLog(0);
EXPECT_EQ(action_history_1.actions_size(), 2);
ExpectCrOSAction(action_history_1.actions(0), 0, save_internal_secs_);
ExpectCrOSAction(action_history_1.actions(1), 1, save_internal_secs_ * 2);
// Advance for 3 kSaveInternal.
time_.Advance(base::TimeDelta::FromSeconds(save_internal_secs_ * 3));
CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
{actions_[2]}, {{conditions_[2], kConditionValue + 2}});
Wait();
// Expect the GetCrOSActionHistory() is already cleared.
EXPECT_TRUE(GetCrOSActionHistory().actions().empty());
// Expect the log to have correct values (three actions).
const CrOSActionHistoryProto action_history_2 = ReadLog(0);
EXPECT_EQ(action_history_2.actions_size(), 3);
ExpectCrOSAction(action_history_1.actions(0), 0, save_internal_secs_);
ExpectCrOSAction(action_history_1.actions(1), 1, save_internal_secs_ * 2);
ExpectCrOSAction(action_history_2.actions(2), 2, save_internal_secs_ * 5);
}
} // namespace app_list
......@@ -5135,6 +5135,7 @@ test("unit_tests") {
"../browser/ui/app_list/search/arc/arc_app_shortcuts_search_provider_unittest.cc",
"../browser/ui/app_list/search/arc/arc_playstore_search_provider_unittest.cc",
"../browser/ui/app_list/search/common/file_icon_util_unittest.cc",
"../browser/ui/app_list/search/cros_action_history/cros_action_recorder_unittest.cc",
"../browser/ui/app_list/search/launcher_search/launcher_search_icon_image_loader_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/app_launch_event_logger_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/app_launch_predictor_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment