Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8669)

Unified Diff: chrome/installer/util/experiment_storage.cc

Issue 2889323004: Win 10 Inactive toast experiment metrics and storage modifications. (Closed)
Patch Set: Apply some comments and try bots errors Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/installer/util/experiment_storage.h ('k') | chrome/installer/util/experiment_storage_unittest.cc » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/installer/util/experiment_storage.cc
diff --git a/chrome/installer/util/experiment_storage.cc b/chrome/installer/util/experiment_storage.cc
new file mode 100644
index 0000000000000000000000000000000000000000..6f8fe0e7d1bec16b7ee1b59095f6a02b315cd04b
--- /dev/null
+++ b/chrome/installer/util/experiment_storage.cc
@@ -0,0 +1,479 @@
+// Copyright 2017 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/installer/util/experiment_storage.h"
+
+#include <windows.h>
+
+#include <stdint.h>
+
+#include <limits>
+#include <string>
+
+#include "base/base64.h"
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/task_scheduler/post_task.h"
+#include "base/task_scheduler/task_traits.h"
+#include "base/time/time.h"
+#include "base/win/registry.h"
+#include "base/win/win_util.h"
+#include "chrome/install_static/install_details.h"
+#include "chrome/install_static/install_modes.h"
+#include "chrome/install_static/install_util.h"
+#include "chrome/installer/util/experiment.h"
+#include "chrome/installer/util/experiment_labels.h"
+#include "chrome/installer/util/experiment_metrics.h"
+#include "chrome/installer/util/google_update_settings.h"
+#include "chrome/installer/util/shell_util.h"
+
+namespace installer {
+
+namespace {
+
+constexpr base::char16 kExperimentLabelName[] = L"CrExp60";
+constexpr wchar_t kRegKeyRetention[] = L"\\Retention";
+constexpr wchar_t kRegValueActionDelay[] = L"ActionDelay";
+constexpr wchar_t kRegValueFirstDisplayTime[] = L"FirstDisplayTime";
+constexpr wchar_t kRegValueGroup[] = L"Group";
+constexpr wchar_t kRegValueInactiveDays[] = L"InactiveDays";
+constexpr wchar_t kRegValueLatestDisplayTime[] = L"LatestDisplayTime";
+constexpr wchar_t kRegValueRetentionStudy[] = L"RetentionStudy";
+constexpr wchar_t kRegValueState[] = L"State";
+constexpr wchar_t kRegValueToastCount[] = L"ToastCount";
+constexpr wchar_t kRegValueToastLocation[] = L"ToastLocation";
+constexpr wchar_t kRegValueUserSessionUptime[] = L"UserSessionUptime";
+
+constexpr int kSessionLengthBucketLowestBit = 0;
+constexpr int kActionDelayBucketLowestBit =
+ ExperimentMetrics::kSessionLengthBucketBits + kSessionLengthBucketLowestBit;
+constexpr int kLastUsedBucketLowestBit =
+ ExperimentMetrics::kActionDelayBucketBits + kActionDelayBucketLowestBit;
+constexpr int kToastHourLowestBit =
+ ExperimentMetrics::kLastUsedBucketBits + kLastUsedBucketLowestBit;
+constexpr int kFirstToastOffsetLowestBit =
+ ExperimentMetrics::kToastHourBits + kToastHourLowestBit;
+constexpr int kToastCountLowestBit =
+ ExperimentMetrics::kFirstToastOffsetBits + kFirstToastOffsetLowestBit;
+constexpr int kToastLocationLowestBit =
+ ExperimentMetrics::kToastCountBits + kToastCountLowestBit;
+constexpr int kStateLowestBit =
+ ExperimentMetrics::kToastLocationBits + kToastLocationLowestBit;
+constexpr int kGroupLowestBit = ExperimentMetrics::kStateBits + kStateLowestBit;
+constexpr int kLowestUnusedBit =
+ ExperimentMetrics::kGroupBits + kGroupLowestBit;
+
+// Helper functions ------------------------------------------------------------
+
+// Returns the name of the global mutex used to protect the storage location.
+base::string16 GetMutexName() {
+ base::string16 name(L"Global\\");
+ name.append(install_static::kCompanyPathName);
+ name.append(ShellUtil::GetBrowserModelId(!install_static::IsSystemInstall()));
+ name.append(L"ExperimentStorageMutex");
+ return name;
+}
+
+// Populates |path| with the path to the registry key in which the current
+// user's experiment state is stored. Returns false if the path cannot be
+// determined.
+bool GetExperimentStateKeyPath(bool system_level, base::string16* path) {
+ const install_static::InstallDetails& install_details =
+ install_static::InstallDetails::Get();
+
+ if (!system_level) {
+ *path = install_details.GetClientStateKeyPath().append(kRegKeyRetention);
+ return true;
+ }
+
+ base::string16 user_sid;
+ if (base::win::GetUserSidString(&user_sid)) {
+ *path = install_details.GetClientStateMediumKeyPath()
+ .append(kRegKeyRetention)
+ .append(L"\\")
+ .append(user_sid);
+ return true;
+ }
+
+ NOTREACHED();
+ return false;
+}
+
+bool OpenParticipationKey(bool write_access, base::win::RegKey* key) {
+ const install_static::InstallDetails& details =
+ install_static::InstallDetails::Get();
+ LONG result = key->Open(
+ details.system_level() ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER,
+ details.GetClientStateKeyPath().c_str(),
+ KEY_WOW64_32KEY | (write_access ? KEY_SET_VALUE : KEY_QUERY_VALUE));
+ return result == ERROR_SUCCESS;
+}
+
+// Reads |value_name| into |result|. Returns false if the value is not found or
+// is out of range.
+template <class T>
+bool ReadBoundedDWORD(base::win::RegKey* key,
+ const wchar_t* value_name,
+ DWORD min_value,
+ DWORD max_value,
+ T* result) {
+ DWORD dword_value;
+ if (key->ReadValueDW(value_name, &dword_value) != ERROR_SUCCESS)
+ return false;
+ if (dword_value < min_value || dword_value > max_value)
+ return false;
+ *result = static_cast<T>(dword_value);
+ return true;
+}
+
+// Reads the internal representation of a Time or TimeDelta from |value_name|
+// into |result|. Returns false if the value is not found or is out of range.
+template <class T>
+bool ReadTime(base::win::RegKey* key, const wchar_t* value_name, T* result) {
+ int64_t qword_value;
+ if (key->ReadInt64(value_name, &qword_value) != ERROR_SUCCESS)
+ return false;
+ *result = T::FromInternalValue(qword_value);
+ return true;
+}
+
+void WriteTime(base::win::RegKey* key,
+ const wchar_t* value_name,
+ int64_t internal_time_value) {
+ key->WriteValue(value_name, &internal_time_value, sizeof(internal_time_value),
+ REG_QWORD);
+}
+
+} // namespace
+
+// ExperimentStorage::Lock -----------------------------------------------------
+
+ExperimentStorage::Lock::~Lock() {
+ BOOL result = ::ReleaseMutex(storage_->mutex_.Get());
+ DCHECK(result);
+}
+
+bool ExperimentStorage::Lock::ReadParticipation(Participation* participation) {
+ base::win::RegKey key;
+ // A failure to open the key likely indicates that this isn't running from a
+ // real install of Chrome.
+ if (!OpenParticipationKey(false /* !write_access */, &key))
+ return false;
+
+ DWORD value = 0;
+ LONG result = key.ReadValueDW(kRegValueRetentionStudy, &value);
+ if (result != ERROR_SUCCESS) {
+ // This likely means that the value is not present.
+ *participation = Participation::kNotEvaluated;
+ } else if (value == 0) {
+ *participation = Participation::kNotParticipating;
+ } else {
+ *participation = Participation::kIsParticipating;
+ }
+ return true;
+}
+
+bool ExperimentStorage::Lock::WriteParticipation(Participation participation) {
+ base::win::RegKey key;
+ // A failure to open the key likely indicates that this isn't running from a
+ // real install of Chrome.
+ if (!OpenParticipationKey(true /* write_access */, &key))
+ return false;
+
+ if (participation == Participation::kNotEvaluated)
+ return key.DeleteValue(kRegValueRetentionStudy) == ERROR_SUCCESS;
+ const DWORD value = participation == Participation::kIsParticipating ? 1 : 0;
+ return key.WriteValue(kRegValueRetentionStudy, value) == ERROR_SUCCESS;
+}
+
+bool ExperimentStorage::Lock::LoadExperiment(Experiment* experiment) {
+ // This function loads both the experiment metrics and state from the
+ // registry.
+ // - If no metrics are found: |experiment| is cleared, and true is returned.
+ // (Per-user experiment data in the registry is ignored for all users.)
+ // - If metrics indicate an initial state (prior to a user being elected into
+ // an experiment group): |experiment| is populated with the metrics and true
+ // is returned. (Per-user experiment data in the registry is ignored for all
+ // users.)
+ // - If metrics indicate an intermediate or terminal state and per-user
+ // experiment data is in the same state: |experiment| is populated with all
+ // data from the registry and true is returned.
+ // Otherwise, the metrics correspond to a different user on the machine, so
+ // false is returned.
+
+ *experiment = Experiment();
+
+ ExperimentMetrics metrics;
+ if (!storage_->LoadMetricsUnsafe(&metrics))
+ return false; // Error reading metrics -- do nothing.
+
+ if (metrics.InInitialState()) {
+ // There should be no per-user experiment data present (ignore it if there
+ // happens to be somehow).
+ experiment->InitializeFromMetrics(metrics);
+ return true;
+ }
+
+ Experiment temp_experiment;
+ if (!storage_->LoadStateUnsafe(&temp_experiment))
+ return false;
+
+ // Verify that the state matches the metrics. Ignore the state if this is not
+ // the case, as the metrics are the source of truth.
+ if (temp_experiment.state() != metrics.state)
+ return false;
+
+ *experiment = temp_experiment;
+ return true;
+}
+
+bool ExperimentStorage::Lock::StoreExperiment(const Experiment& experiment) {
+ bool ret = storage_->StoreMetricsUnsafe(experiment.metrics());
+ return storage_->StoreStateUnsafe(experiment) && ret;
+}
+
+bool ExperimentStorage::Lock::LoadMetrics(ExperimentMetrics* metrics) {
+ DCHECK_EQ(ExperimentMetrics::kUninitialized, metrics->state);
+ return storage_->LoadMetricsUnsafe(metrics);
+}
+
+bool ExperimentStorage::Lock::StoreMetrics(const ExperimentMetrics& metrics) {
+ DCHECK_NE(ExperimentMetrics::kUninitialized, metrics.state);
+ return storage_->StoreMetricsUnsafe(metrics);
+}
+
+ExperimentStorage::Lock::Lock(ExperimentStorage* storage) : storage_(storage) {
+ DCHECK(storage);
+ DWORD result = ::WaitForSingleObject(storage_->mutex_.Get(), INFINITE);
+ PLOG_IF(FATAL, result == WAIT_FAILED)
+ << "Failed to lock ExperimentStorage mutex";
+}
+
+// ExperimentStorage -----------------------------------------------------------
+
+ExperimentStorage::ExperimentStorage()
+ : mutex_(::CreateMutex(nullptr, FALSE, GetMutexName().c_str())) {}
+
+ExperimentStorage::~ExperimentStorage() {}
+
+std::unique_ptr<ExperimentStorage::Lock> ExperimentStorage::AcquireLock() {
+ return base::WrapUnique(new Lock(this));
+}
+
+// static
+int ExperimentStorage::ReadUint64Bits(uint64_t source,
+ int bit_length,
+ int low_bit) {
+ DCHECK(bit_length > 0 && bit_length <= static_cast<int>(sizeof(int) * 8) &&
+ low_bit + bit_length <= static_cast<int>(sizeof(source) * 8));
+ uint64_t bit_mask = (1ULL << bit_length) - 1;
+ return static_cast<int>((source >> low_bit) & bit_mask);
+}
+
+// static
+void ExperimentStorage::SetUint64Bits(int value,
+ int bit_length,
+ int low_bit,
+ uint64_t* target) {
+ DCHECK(bit_length > 0 && bit_length <= static_cast<int>(sizeof(value) * 8) &&
+ low_bit + bit_length <= static_cast<int>(sizeof(*target) * 8));
+ uint64_t bit_mask = (1ULL << bit_length) - 1;
+ *target |= ((static_cast<uint64_t>(value) & bit_mask) << low_bit);
+}
+
+bool ExperimentStorage::DecodeMetrics(base::StringPiece16 encoded_metrics,
+ ExperimentMetrics* metrics) {
+ std::string metrics_data;
+
+ if (!base::Base64Decode(base::UTF16ToASCII(encoded_metrics), &metrics_data))
+ return false;
+
+ if (metrics_data.size() != 6)
+ return false;
+
+ uint64_t metrics_value = 0;
+ for (size_t i = 0; i < metrics_data.size(); ++i)
+ SetUint64Bits(metrics_data[i], 8, 8 * i, &metrics_value);
+
+ ExperimentMetrics result;
+ result.session_length_bucket =
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kSessionLengthBucketBits,
+ kSessionLengthBucketLowestBit);
+ result.action_delay_bucket =
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kActionDelayBucketBits,
+ kActionDelayBucketLowestBit);
+ result.last_used_bucket =
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kLastUsedBucketBits,
+ kLastUsedBucketLowestBit);
+ result.toast_hour = ReadUint64Bits(
+ metrics_value, ExperimentMetrics::kToastHourBits, kToastHourLowestBit);
+ result.first_toast_offset_days =
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kFirstToastOffsetBits,
+ kFirstToastOffsetLowestBit);
+ result.toast_count = ReadUint64Bits(
+ metrics_value, ExperimentMetrics::kToastCountBits, kToastCountLowestBit);
+ result.toast_location = static_cast<ExperimentMetrics::ToastLocation>(
+ ReadUint64Bits(metrics_value, ExperimentMetrics::kToastLocationBits,
+ kToastLocationLowestBit));
+
+ static_assert(ExperimentMetrics::State::NUM_STATES <= (1 << 4),
+ "Too many states for ExperimentMetrics encoding.");
+ result.state = static_cast<ExperimentMetrics::State>(ReadUint64Bits(
+ metrics_value, ExperimentMetrics::kStateBits, kStateLowestBit));
+ result.group = ReadUint64Bits(metrics_value, ExperimentMetrics::kGroupBits,
+ kGroupLowestBit);
+
+ if (ReadUint64Bits(metrics_value,
+ sizeof(metrics_value) * 8 - kLowestUnusedBit,
+ kLowestUnusedBit)) {
+ return false;
+ }
+
+ *metrics = result;
+ return true;
+}
+
+// static
+base::string16 ExperimentStorage::EncodeMetrics(
+ const ExperimentMetrics& metrics) {
+ uint64_t metrics_value = 0;
+ SetUint64Bits(metrics.session_length_bucket,
+ ExperimentMetrics::kSessionLengthBucketBits,
+ kSessionLengthBucketLowestBit, &metrics_value);
+ SetUint64Bits(metrics.action_delay_bucket,
+ ExperimentMetrics::kActionDelayBucketBits,
+ kActionDelayBucketLowestBit, &metrics_value);
+ SetUint64Bits(metrics.last_used_bucket,
+ ExperimentMetrics::kLastUsedBucketBits,
+ kLastUsedBucketLowestBit, &metrics_value);
+ SetUint64Bits(metrics.toast_hour, ExperimentMetrics::kToastHourBits,
+ kToastHourLowestBit, &metrics_value);
+ SetUint64Bits(metrics.first_toast_offset_days,
+ ExperimentMetrics::kFirstToastOffsetBits,
+ kFirstToastOffsetLowestBit, &metrics_value);
+ SetUint64Bits(metrics.toast_count, ExperimentMetrics::kToastCountBits,
+ kToastCountLowestBit, &metrics_value);
+ SetUint64Bits(metrics.toast_location, ExperimentMetrics::kToastLocationBits,
+ kToastLocationLowestBit, &metrics_value);
+ static_assert(ExperimentMetrics::State::NUM_STATES <= (1 << 4),
+ "Too many states for ExperimentMetrics encoding.");
+ SetUint64Bits(metrics.state, ExperimentMetrics::kStateBits, kStateLowestBit,
+ &metrics_value);
+ SetUint64Bits(metrics.group, ExperimentMetrics::kGroupBits, kGroupLowestBit,
+ &metrics_value);
+
+ std::string metrics_data(6, '\0');
+ for (size_t i = 0; i < metrics_data.size(); ++i) {
+ metrics_data[i] =
+ static_cast<char>(ReadUint64Bits(metrics_value, 8, 8 * i));
+ }
+ std::string encoded_metrics;
+ base::Base64Encode(metrics_data, &encoded_metrics);
+ return base::ASCIIToUTF16(encoded_metrics);
+}
+
+bool ExperimentStorage::LoadMetricsUnsafe(ExperimentMetrics* metrics) {
+ base::string16 value;
+
+ if (!GoogleUpdateSettings::ReadExperimentLabels(
+ install_static::IsSystemInstall(), &value)) {
+ return false;
+ }
+
+ ExperimentLabels experiment_labels(value);
+ base::StringPiece16 encoded_metrics =
+ experiment_labels.GetValueForLabel(kExperimentLabelName);
+ if (encoded_metrics.empty()) {
+ *metrics = ExperimentMetrics();
+ return true;
+ }
+
+ return DecodeMetrics(encoded_metrics, metrics);
+}
+
+bool ExperimentStorage::StoreMetricsUnsafe(const ExperimentMetrics& metrics) {
+ base::string16 value;
+ if (!GoogleUpdateSettings::ReadExperimentLabels(
+ install_static::IsSystemInstall(), &value)) {
+ return false;
+ }
+ ExperimentLabels experiment_labels(value);
+
+ experiment_labels.SetValueForLabel(kExperimentLabelName,
+ EncodeMetrics(metrics),
+ base::TimeDelta::FromDays(182));
+
+ return GoogleUpdateSettings::SetExperimentLabels(
+ install_static::IsSystemInstall(), experiment_labels.value());
+}
+
+bool ExperimentStorage::LoadStateUnsafe(Experiment* experiment) {
+ const bool system_level = install_static::IsSystemInstall();
+
+ base::string16 path;
+ if (!GetExperimentStateKeyPath(system_level, &path))
+ return false;
+
+ const HKEY root = system_level ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
+ base::win::RegKey key;
+ if (key.Open(root, path.c_str(), KEY_QUERY_VALUE | KEY_WOW64_32KEY) !=
+ ERROR_SUCCESS) {
+ return false;
+ }
+
+ return ReadBoundedDWORD(&key, kRegValueState, 0,
+ ExperimentMetrics::NUM_STATES, &experiment->state_) &&
+ ReadBoundedDWORD(&key, kRegValueGroup, 0,
+ ExperimentMetrics::kNumGroups - 1,
+ &experiment->group_) &&
+ ReadBoundedDWORD(&key, kRegValueToastLocation, 0, 1,
+ &experiment->toast_location_) &&
+ ReadBoundedDWORD(&key, kRegValueInactiveDays, 0, INT_MAX,
+ &experiment->inactive_days_) &&
+ ReadBoundedDWORD(&key, kRegValueToastCount, 0,
+ ExperimentMetrics::kMaxToastCount,
+ &experiment->toast_count_) &&
+ ReadTime(&key, kRegValueFirstDisplayTime,
+ &experiment->first_display_time_) &&
+ ReadTime(&key, kRegValueLatestDisplayTime,
+ &experiment->latest_display_time_) &&
+ ReadTime(&key, kRegValueUserSessionUptime,
+ &experiment->user_session_uptime_) &&
+ ReadTime(&key, kRegValueActionDelay, &experiment->action_delay_);
+}
+
+bool ExperimentStorage::StoreStateUnsafe(const Experiment& experiment) {
+ const bool system_level = install_static::IsSystemInstall();
+
+ base::string16 path;
+ if (!GetExperimentStateKeyPath(system_level, &path))
+ return false;
+
+ const HKEY root = system_level ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
+ base::win::RegKey key;
+ if (key.Create(root, path.c_str(), KEY_SET_VALUE | KEY_WOW64_32KEY) !=
+ ERROR_SUCCESS) {
+ return false;
+ }
+
+ key.WriteValue(kRegValueState, experiment.state());
+ key.WriteValue(kRegValueGroup, experiment.group());
+ key.WriteValue(kRegValueToastLocation, experiment.toast_location());
+ key.WriteValue(kRegValueInactiveDays, experiment.inactive_days());
+ key.WriteValue(kRegValueToastCount, experiment.toast_count());
+ WriteTime(&key, kRegValueFirstDisplayTime,
+ experiment.first_display_time().ToInternalValue());
+ WriteTime(&key, kRegValueLatestDisplayTime,
+ experiment.latest_display_time().ToInternalValue());
+ WriteTime(&key, kRegValueUserSessionUptime,
+ experiment.user_session_uptime().ToInternalValue());
+ WriteTime(&key, kRegValueActionDelay,
+ experiment.action_delay().ToInternalValue());
+ return true;
+}
+
+} // namespace installer
« no previous file with comments | « chrome/installer/util/experiment_storage.h ('k') | chrome/installer/util/experiment_storage_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698