blob: 13933d0e842f82cbd77dcc19380561f10ad1bc40 [file] [log] [blame]
// Copyright 2014 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 "components/metrics/clean_exit_beacon.h"
#include <memory>
#include <utility>
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/cxx17_backports.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/variations/pref_names.h"
#include "components/variations/service/variations_safe_mode_constants.h"
#include "components/variations/variations_switches.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#include "base/strings/string_util_win.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/registry.h"
#endif
namespace metrics {
namespace {
using ::variations::kControlGroup;
using ::variations::kDefaultGroup;
using ::variations::kEnabledGroup;
using ::variations::kExtendedSafeModeTrial;
using ::variations::prefs::kVariationsCrashStreak;
// Denotes whether Chrome should perform clean shutdown steps: signaling that
// Chrome is exiting cleanly and then CHECKing that is has shutdown cleanly.
// This may be modified by SkipCleanShutdownStepsForTesting().
bool g_skip_clean_shutdown_steps = false;
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
// Records the the combined state of two distinct beacons' values in the given
// histogram.
void RecordBeaconConsistency(const std::string& histogram_name,
absl::optional<bool> beacon_value1,
absl::optional<bool> beacon_value2) {
CleanExitBeaconConsistency consistency =
CleanExitBeaconConsistency::kDirtyDirty;
if (!beacon_value1) {
if (!beacon_value2) {
consistency = CleanExitBeaconConsistency::kMissingMissing;
} else {
consistency = beacon_value2.value()
? CleanExitBeaconConsistency::kMissingClean
: CleanExitBeaconConsistency::kMissingDirty;
}
} else if (!beacon_value2) {
consistency = beacon_value1.value()
? CleanExitBeaconConsistency::kCleanMissing
: CleanExitBeaconConsistency::kDirtyMissing;
} else if (beacon_value1.value()) {
consistency = beacon_value2.value()
? CleanExitBeaconConsistency::kCleanClean
: CleanExitBeaconConsistency::kCleanDirty;
} else {
consistency = beacon_value2.value()
? CleanExitBeaconConsistency::kDirtyClean
: CleanExitBeaconConsistency::kDirtyDirty;
}
base::UmaHistogramEnumeration(histogram_name, consistency);
}
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
// Increments kVariationsCrashStreak if |did_previous_session_exit_cleanly| is
// false. Also, emits the crash streak to a histogram.
//
// Either |beacon_file_contents| or |local_state| is used to retrieve the crash
// streak depending on the client's Extended Variations Safe Mode experiment
// group in the last session.
void MaybeIncrementCrashStreak(bool did_previous_session_exit_cleanly,
base::Value* beacon_file_contents,
PrefService* local_state) {
int num_crashes =
beacon_file_contents
? beacon_file_contents->FindKey(kVariationsCrashStreak)->GetInt()
: local_state->GetInteger(kVariationsCrashStreak);
// Increment the crash streak if the previous session crashed. Note that the
// streak is not cleared if the previous run didn’t crash. Instead, it’s
// incremented on each crash until Chrome is able to successfully fetch a new
// seed. This way, a seed update that mostly destabilizes Chrome still results
// in a fallback to safe mode.
//
// The crash streak is incremented here rather than in a variations-related
// class for two reasons. First, the crash streak depends on the value of
// kStabilityExitedCleanly. Second, if kVariationsCrashStreak were updated in
// another function, any crash between CleanExitBeacon() and that function
// would cause the crash streak to not be to incremented. A consequence of
// failing to increment the crash streak is that Variations Safe Mode might
// undercount or be completely unaware of repeated crashes early on in
// startup.
if (!did_previous_session_exit_cleanly) {
++num_crashes;
local_state->SetInteger(kVariationsCrashStreak, num_crashes);
#if BUILDFLAG(IS_ANDROID)
// Schedule a Local State write on Android Chrome, WebLayer, and WebView
// only as this write is expensive, and other platforms use the beacon file
// as the source of truth. For other platforms, the crask streak is written
// synchronously to disk later on in startup. See
// MaybeExtendVariationsSafeMode() and WriteBeaconValue().
local_state->CommitPendingWrite();
#endif
}
base::UmaHistogramSparse("Variations.SafeMode.Streak.Crashes",
base::clamp(num_crashes, 0, 100));
}
// Records |file_state| in a histogram.
void RecordBeaconFileState(BeaconFileState file_state) {
base::UmaHistogramEnumeration(
"Variations.ExtendedSafeMode.BeaconFileStateAtStartup", file_state);
}
// Returns the contents of the file at |beacon_file_path| if the following
// conditions are all true. Otherwise, returns nullptr.
//
// 1. The file path is non-empty.
// 2. The file exists.
// 3. The file is successfully read.
// 4. The file contents are in the expected format with the expected info.
//
// The file is not expected to exist for clients that have never been in the
// Extended Variations Safe Mode experiment's enabled group. Also, the file may
// not exist for all enabled-group clients because there are some edge cases.
// First, MaybeGetFileContents() is called before clients are assigned to an
// Extended Variations Safe Mode experiment group, so a client that is later
// assigned to the enabled group will not have the file in the first session
// after updating to or installing a Chrome version with the experiment. Second,
// Android Chrome enabled-group clients with repeated background sessions may
// never write a beacon file. Third, it is possible for a user to delete the
// file or to switch groups by resetting their variations state. Finally,
// clients also switch groups when the FieldTrial name is updated.
std::unique_ptr<base::Value> MaybeGetFileContents(
const base::FilePath& beacon_file_path) {
if (beacon_file_path.empty())
return nullptr;
int error_code;
JSONFileValueDeserializer deserializer(beacon_file_path);
std::unique_ptr<base::Value> beacon_file_contents =
deserializer.Deserialize(&error_code, /*error_message=*/nullptr);
if (!beacon_file_contents) {
RecordBeaconFileState(BeaconFileState::kNotDeserializable);
base::UmaHistogramSparse(
"Variations.ExtendedSafeMode.BeaconFileDeserializationError",
error_code);
return nullptr;
}
if (!beacon_file_contents->is_dict() || beacon_file_contents->DictEmpty()) {
RecordBeaconFileState(BeaconFileState::kMissingDictionary);
return nullptr;
}
if (!beacon_file_contents->FindKeyOfType(kVariationsCrashStreak,
base::Value::Type::INTEGER)) {
RecordBeaconFileState(BeaconFileState::kMissingCrashStreak);
return nullptr;
}
if (!beacon_file_contents->FindKeyOfType(prefs::kStabilityExitedCleanly,
base::Value::Type::BOOLEAN)) {
RecordBeaconFileState(BeaconFileState::kMissingBeacon);
return nullptr;
}
RecordBeaconFileState(BeaconFileState::kReadable);
return beacon_file_contents;
}
std::string SetUpExtendedSafeModeTrial() {
int default_group;
scoped_refptr<base::FieldTrial> trial(
base::FieldTrialList::FactoryGetFieldTrial(
kExtendedSafeModeTrial, 100, kDefaultGroup,
base::FieldTrial::ONE_TIME_RANDOMIZED, &default_group));
// The new behavior launched on desktop and iOS in M102 and on Android Chrome
// in M103.
trial->AppendGroup(kEnabledGroup, 100);
return trial->group_name();
}
} // namespace
CleanExitBeacon::CleanExitBeacon(const std::wstring& backup_registry_key,
const base::FilePath& user_data_dir,
PrefService* local_state,
version_info::Channel channel)
: backup_registry_key_(backup_registry_key),
user_data_dir_(user_data_dir),
local_state_(local_state),
initial_browser_last_live_timestamp_(
local_state->GetTime(prefs::kStabilityBrowserLastLiveTimeStamp)),
channel_(channel) {
DCHECK_NE(PrefService::INITIALIZATION_STATUS_WAITING,
local_state_->GetInitializationStatus());
}
void CleanExitBeacon::Initialize() {
DCHECK(!initialized_);
std::string group;
if (!user_data_dir_.empty()) {
// Platforms that pass an empty path do so deliberately. They should not
// participate in this experiment.
group = SetUpExtendedSafeModeTrial();
}
if (group == kEnabledGroup) {
beacon_file_path_ =
user_data_dir_.Append(variations::kCleanExitBeaconFilename);
}
std::unique_ptr<base::Value> beacon_file_contents =
MaybeGetFileContents(beacon_file_path_);
did_previous_session_exit_cleanly_ =
DidPreviousSessionExitCleanly(beacon_file_contents.get());
MaybeIncrementCrashStreak(did_previous_session_exit_cleanly_,
beacon_file_contents.get(), local_state_);
initialized_ = true;
}
bool CleanExitBeacon::DidPreviousSessionExitCleanly(
base::Value* beacon_file_contents) {
absl::optional<bool> local_state_beacon_value;
if (local_state_->HasPrefPath(prefs::kStabilityExitedCleanly)) {
local_state_beacon_value = absl::make_optional(
local_state_->GetBoolean(prefs::kStabilityExitedCleanly));
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
absl::optional<bool> backup_beacon_value = ExitedCleanly();
#endif
absl::optional<bool> beacon_file_beacon_value;
bool use_beacon_file = base::FieldTrialList::FindFullName(
kExtendedSafeModeTrial) == kEnabledGroup;
if (use_beacon_file) {
if (beacon_file_contents) {
beacon_file_beacon_value = absl::make_optional(
beacon_file_contents->FindKey(prefs::kStabilityExitedCleanly)
->GetBool());
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
RecordBeaconConsistency("UMA.CleanExitBeaconConsistency3",
beacon_file_beacon_value, backup_beacon_value);
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
}
bool did_previous_session_exit_cleanly =
use_beacon_file ? beacon_file_beacon_value.value_or(true)
: local_state_beacon_value.value_or(true);
#if BUILDFLAG(IS_IOS)
// For the time being, this is a no-op to avoid interference with the Extended
// Variations Safe Mode experiment; i.e., ShouldUseUserDefaultsBeacon() always
// returns false.
if (ShouldUseUserDefaultsBeacon())
return backup_beacon_value.value_or(true);
#endif // BUILDFLAG(IS_IOS)
return did_previous_session_exit_cleanly;
}
void CleanExitBeacon::WriteBeaconValue(bool exited_cleanly,
bool is_extended_safe_mode) {
DCHECK(initialized_);
if (g_skip_clean_shutdown_steps)
return;
UpdateLastLiveTimestamp();
if (has_exited_cleanly_ && has_exited_cleanly_.value() == exited_cleanly) {
// It is possible to call WriteBeaconValue() with the same value for
// |exited_cleanly| twice during startup and shutdown on some platforms. If
// the current beacon value matches |exited_cleanly|, then return here to
// skip redundantly updating Local State, writing a beacon file, and on
// Windows and iOS, writing to platform-specific locations.
return;
}
const std::string group_name =
base::FieldTrialList::FindFullName(kExtendedSafeModeTrial);
if (is_extended_safe_mode) {
// Only enabled-group clients should extend Variations Safe Mode.
DCHECK_EQ(group_name, kEnabledGroup);
// |has_exited_cleanly_| should always be unset before starting to watch for
// browser crashes.
DCHECK(!has_exited_cleanly_);
// When starting to watch for browser crashes in the code covered by
// Extended Variations Safe Mode, the only valid value for |exited_cleanly|
// is `false`. `true` signals that Chrome should stop watching for crashes.
DCHECK(!exited_cleanly);
WriteBeaconFile(exited_cleanly);
} else {
local_state_->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
#if BUILDFLAG(IS_ANDROID)
// Schedule a Local State write on Android for WebLayer and WebView. Other
// platforms use the beacon file as the source of truth.
local_state_->CommitPendingWrite();
#endif // BUILDFLAG(IS_ANDROID)
if (group_name == kEnabledGroup) {
// Clients in this group write to the Variations Safe Mode file whenever
// |kStabilityExitedCleanly| is updated. The file is kept in sync with the
// pref because the file is used in the next session.
WriteBeaconFile(exited_cleanly);
}
}
#if BUILDFLAG(IS_WIN)
base::win::RegKey regkey;
if (regkey.Create(HKEY_CURRENT_USER, backup_registry_key_.c_str(),
KEY_ALL_ACCESS) == ERROR_SUCCESS) {
regkey.WriteValue(base::ASCIIToWide(prefs::kStabilityExitedCleanly).c_str(),
exited_cleanly ? 1u : 0u);
}
#elif BUILDFLAG(IS_IOS)
SetUserDefaultsBeacon(exited_cleanly);
#endif // BUILDFLAG(IS_WIN)
has_exited_cleanly_ = absl::make_optional(exited_cleanly);
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
absl::optional<bool> CleanExitBeacon::ExitedCleanly() {
#if BUILDFLAG(IS_WIN)
base::win::RegKey regkey;
DWORD value = 0u;
if (regkey.Open(HKEY_CURRENT_USER, backup_registry_key_.c_str(),
KEY_ALL_ACCESS) == ERROR_SUCCESS &&
regkey.ReadValueDW(
base::ASCIIToWide(prefs::kStabilityExitedCleanly).c_str(), &value) ==
ERROR_SUCCESS) {
return value ? true : false;
}
return absl::nullopt;
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_IOS)
if (HasUserDefaultsBeacon())
return GetUserDefaultsBeacon();
return absl::nullopt;
#endif // BUILDFLAG(IS_IOS)
}
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
void CleanExitBeacon::UpdateLastLiveTimestamp() {
local_state_->SetTime(prefs::kStabilityBrowserLastLiveTimeStamp,
base::Time::Now());
}
const base::FilePath CleanExitBeacon::GetUserDataDirForTesting() const {
return user_data_dir_;
}
base::FilePath CleanExitBeacon::GetBeaconFilePathForTesting() const {
return beacon_file_path_;
}
// static
void CleanExitBeacon::RegisterPrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kStabilityExitedCleanly, true);
registry->RegisterTimePref(prefs::kStabilityBrowserLastLiveTimeStamp,
base::Time(), PrefRegistry::LOSSY_PREF);
// This Variations-Safe-Mode-related pref is registered here rather than in
// SafeSeedManager::RegisterPrefs() because the CleanExitBeacon is
// responsible for incrementing this value. (See the comments in
// MaybeIncrementCrashStreak() for more details.)
registry->RegisterIntegerPref(kVariationsCrashStreak, 0);
}
// static
void CleanExitBeacon::EnsureCleanShutdown(PrefService* local_state) {
if (!g_skip_clean_shutdown_steps)
CHECK(local_state->GetBoolean(prefs::kStabilityExitedCleanly));
}
// static
void CleanExitBeacon::SetStabilityExitedCleanlyForTesting(
PrefService* local_state,
bool exited_cleanly) {
local_state->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
#if BUILDFLAG(IS_IOS)
SetUserDefaultsBeacon(exited_cleanly);
#endif // BUILDFLAG(IS_IOS)
}
// static
void CleanExitBeacon::ResetStabilityExitedCleanlyForTesting(
PrefService* local_state) {
local_state->ClearPref(prefs::kStabilityExitedCleanly);
#if BUILDFLAG(IS_IOS)
ResetUserDefaultsBeacon();
#endif // BUILDFLAG(IS_IOS)
}
// static
void CleanExitBeacon::SkipCleanShutdownStepsForTesting() {
g_skip_clean_shutdown_steps = true;
}
void CleanExitBeacon::WriteBeaconFile(bool exited_cleanly) const {
DCHECK_EQ(base::FieldTrialList::FindFullName(kExtendedSafeModeTrial),
kEnabledGroup);
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetBoolKey(prefs::kStabilityExitedCleanly, exited_cleanly);
dict.SetIntKey(kVariationsCrashStreak,
local_state_->GetInteger(kVariationsCrashStreak));
std::string json_string;
JSONStringValueSerializer serializer(&json_string);
bool success = serializer.Serialize(dict);
DCHECK(success);
int data_size = static_cast<int>(json_string.size());
DCHECK_NE(data_size, 0);
int bytes_written;
{
base::ScopedAllowBlocking allow_io;
// WriteFile() returns -1 on error.
bytes_written =
base::WriteFile(beacon_file_path_, json_string.data(), data_size);
}
base::UmaHistogramBoolean("Variations.ExtendedSafeMode.BeaconFileWrite",
bytes_written != -1);
}
} // namespace metrics