446 lines
17 KiB
C++
446 lines
17 KiB
C++
// Copyright 2014 The Chromium Authors
|
||
// 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 <algorithm>
|
||
#include <memory>
|
||
#include <utility>
|
||
|
||
#include "base/check_op.h"
|
||
#include "base/command_line.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/strings/string_number_conversions.h"
|
||
#include "base/strings/stringprintf.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/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::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 a
|
||
// histogram.
|
||
void RecordBeaconConsistency(
|
||
absl::optional<bool> beacon_file_beacon_value,
|
||
absl::optional<bool> platform_specific_beacon_value) {
|
||
CleanExitBeaconConsistency consistency =
|
||
CleanExitBeaconConsistency::kDirtyDirty;
|
||
|
||
if (!beacon_file_beacon_value) {
|
||
if (!platform_specific_beacon_value) {
|
||
consistency = CleanExitBeaconConsistency::kMissingMissing;
|
||
} else {
|
||
consistency = platform_specific_beacon_value.value()
|
||
? CleanExitBeaconConsistency::kMissingClean
|
||
: CleanExitBeaconConsistency::kMissingDirty;
|
||
}
|
||
} else if (!platform_specific_beacon_value) {
|
||
consistency = beacon_file_beacon_value.value()
|
||
? CleanExitBeaconConsistency::kCleanMissing
|
||
: CleanExitBeaconConsistency::kDirtyMissing;
|
||
} else if (beacon_file_beacon_value.value()) {
|
||
consistency = platform_specific_beacon_value.value()
|
||
? CleanExitBeaconConsistency::kCleanClean
|
||
: CleanExitBeaconConsistency::kCleanDirty;
|
||
} else {
|
||
consistency = platform_specific_beacon_value.value()
|
||
? CleanExitBeaconConsistency::kDirtyClean
|
||
: CleanExitBeaconConsistency::kDirtyDirty;
|
||
}
|
||
base::UmaHistogramEnumeration("UMA.CleanExitBeaconConsistency3", 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.
|
||
//
|
||
// If |beacon_file_contents| are given, then the beacon file is used to retrieve
|
||
// the crash streak. Otherwise, |local_state| is used.
|
||
void MaybeIncrementCrashStreak(bool did_previous_session_exit_cleanly,
|
||
base::Value* beacon_file_contents,
|
||
PrefService* local_state) {
|
||
int num_crashes;
|
||
if (beacon_file_contents) {
|
||
absl::optional<int> crash_streak =
|
||
beacon_file_contents->GetDict().FindInt(kVariationsCrashStreak);
|
||
// Any contents without the key should have been rejected by
|
||
// MaybeGetFileContents().
|
||
DCHECK(crash_streak);
|
||
num_crashes = crash_streak.value();
|
||
} else {
|
||
// TODO(crbug/1341087): Consider not falling back to Local State for clients
|
||
// on platforms that support the beacon file.
|
||
num_crashes = local_state->GetInteger(kVariationsCrashStreak);
|
||
}
|
||
|
||
if (!did_previous_session_exit_cleanly) {
|
||
// 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 Variations Safe Mode.
|
||
//
|
||
// The crash streak is incremented here rather than in a variations-related
|
||
// class for two reasons. First, the crash streak depends on whether Chrome
|
||
// exited cleanly in the last session, which is first checked via
|
||
// CleanExitBeacon::Initialize(). Second, if the crash streak were updated
|
||
// in another function, any crash between beacon initialization and the
|
||
// other function might cause the crash streak to not be to incremented.
|
||
// "Might" because the updated crash streak also needs to be persisted to
|
||
// disk. A consequence of failing to increment the crash streak is that
|
||
// Chrome might undercount or be completely unaware of repeated crashes
|
||
// early on in startup.
|
||
++num_crashes;
|
||
// For platforms that use the beacon file, the crash streak is written
|
||
// synchronously to disk later on in startup via
|
||
// MaybeExtendVariationsSafeMode() and WriteBeaconFile(). The crash streak
|
||
// is intentionally not written to the beacon file here. If the beacon file
|
||
// indicates that Chrome failed to exit cleanly, then Chrome got at
|
||
// least as far as MaybeExtendVariationsSafeMode(), which is during the
|
||
// PostEarlyInitialization stage when native code is being synchronously
|
||
// executed. Chrome should also be able to reach that point in this session.
|
||
//
|
||
// For platforms that do not use the beacon file, the crash streak is
|
||
// scheduled to be written to disk later on in startup. At the latest, this
|
||
// is done when a Local State write is scheduled via WriteBeaconFile(). A
|
||
// write is not scheduled here for three reasons.
|
||
//
|
||
// 1. It is an expensive operation.
|
||
// 2. Android WebLayer (one of the two platforms that does not use the
|
||
// beacon file) did not appear to benefit from scheduling the write. See
|
||
// crbug/1341850 for details.
|
||
// 3. Android WebView (the other beacon-file-less platform) has its own
|
||
// Variations Safe Mode mechanism and does not need the crash streak.
|
||
local_state->SetInteger(kVariationsCrashStreak, num_crashes);
|
||
}
|
||
base::UmaHistogramSparse("Variations.SafeMode.Streak.Crashes",
|
||
std::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 may not exist for the below reasons:
|
||
//
|
||
// 1. The file is unsupported on the platform.
|
||
// 2. This is the first session after a client updates to or installs a Chrome
|
||
// version that uses the beacon file. The beacon file launched on desktop
|
||
// and iOS in M102 and on Android Chrome in M103.
|
||
// 3. Android Chrome clients with only background sessions may never write a
|
||
// beacon file.
|
||
// 4. A user may delete the file.
|
||
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->GetDict().empty()) {
|
||
RecordBeaconFileState(BeaconFileState::kMissingDictionary);
|
||
return nullptr;
|
||
}
|
||
const base::Value::Dict& beacon_dict = beacon_file_contents->GetDict();
|
||
if (!beacon_dict.FindInt(kVariationsCrashStreak)) {
|
||
RecordBeaconFileState(BeaconFileState::kMissingCrashStreak);
|
||
return nullptr;
|
||
}
|
||
if (!beacon_dict.FindBool(prefs::kStabilityExitedCleanly)) {
|
||
RecordBeaconFileState(BeaconFileState::kMissingBeacon);
|
||
return nullptr;
|
||
}
|
||
RecordBeaconFileState(BeaconFileState::kReadable);
|
||
return beacon_file_contents;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
const base::FilePath::CharType kCleanExitBeaconFilename[] =
|
||
FILE_PATH_LITERAL("Variations");
|
||
|
||
CleanExitBeacon::CleanExitBeacon(const std::wstring& backup_registry_key,
|
||
const base::FilePath& user_data_dir,
|
||
PrefService* local_state)
|
||
: 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)) {
|
||
DCHECK_NE(PrefService::INITIALIZATION_STATUS_WAITING,
|
||
local_state_->GetInitializationStatus());
|
||
}
|
||
|
||
void CleanExitBeacon::Initialize() {
|
||
DCHECK(!initialized_);
|
||
|
||
if (!user_data_dir_.empty()) {
|
||
// Platforms that pass an empty path do so deliberately. They should not
|
||
// use the beacon file.
|
||
beacon_file_path_ = user_data_dir_.Append(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) {
|
||
if (!IsBeaconFileSupported())
|
||
return local_state_->GetBoolean(prefs::kStabilityExitedCleanly);
|
||
|
||
absl::optional<bool> beacon_file_beacon_value =
|
||
beacon_file_contents ? beacon_file_contents->GetDict().FindBool(
|
||
prefs::kStabilityExitedCleanly)
|
||
: absl::nullopt;
|
||
|
||
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
|
||
absl::optional<bool> backup_beacon_value = ExitedCleanly();
|
||
RecordBeaconConsistency(beacon_file_beacon_value, backup_beacon_value);
|
||
#endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
|
||
|
||
#if BUILDFLAG(IS_IOS)
|
||
// TODO(crbug/1231106): For the time being, this is a no-op; i.e.,
|
||
// ShouldUseUserDefaultsBeacon() always returns false.
|
||
if (ShouldUseUserDefaultsBeacon())
|
||
return backup_beacon_value.value_or(true);
|
||
#endif // BUILDFLAG(IS_IOS)
|
||
|
||
return beacon_file_beacon_value.value_or(true);
|
||
}
|
||
|
||
bool CleanExitBeacon::IsExtendedSafeModeSupported() const {
|
||
// All platforms that support the beacon file mechanism also happen to support
|
||
// Extended Variations Safe Mode.
|
||
return IsBeaconFileSupported();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (is_extended_safe_mode) {
|
||
// |is_extended_safe_mode| can be true for only some platforms.
|
||
DCHECK(IsExtendedSafeModeSupported());
|
||
// |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 {
|
||
// TODO(crbug/1341864): Stop updating |kStabilityExitedCleanly| on platforms
|
||
// that support the beacon file.
|
||
local_state_->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
|
||
if (IsBeaconFileSupported()) {
|
||
WriteBeaconFile(exited_cleanly);
|
||
} else {
|
||
// Schedule a Local State write on platforms that back the beacon value
|
||
// using Local State rather than the beacon file.
|
||
local_state_->CommitPendingWrite();
|
||
}
|
||
}
|
||
|
||
#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
|
||
std::string CleanExitBeacon::CreateBeaconFileContentsForTesting(
|
||
bool exited_cleanly,
|
||
int crash_streak) {
|
||
const std::string exited_cleanly_str = exited_cleanly ? "true" : "false";
|
||
return base::StringPrintf(
|
||
"{\n"
|
||
" \"user_experience_metrics.stability.exited_cleanly\":%s,\n"
|
||
" \"variations_crash_streak\":%s\n"
|
||
"}",
|
||
exited_cleanly_str.data(), base::NumberToString(crash_streak).data());
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
bool CleanExitBeacon::IsBeaconFileSupported() const {
|
||
return !beacon_file_path_.empty();
|
||
}
|
||
|
||
void CleanExitBeacon::WriteBeaconFile(bool exited_cleanly) const {
|
||
base::Value::Dict dict;
|
||
dict.Set(prefs::kStabilityExitedCleanly, exited_cleanly);
|
||
dict.Set(kVariationsCrashStreak,
|
||
local_state_->GetInteger(kVariationsCrashStreak));
|
||
|
||
std::string json_string;
|
||
JSONStringValueSerializer serializer(&json_string);
|
||
bool success = serializer.Serialize(dict);
|
||
DCHECK(success);
|
||
DCHECK(!json_string.empty());
|
||
{
|
||
base::ScopedAllowBlocking allow_io;
|
||
success = base::WriteFile(beacon_file_path_, json_string);
|
||
}
|
||
base::UmaHistogramBoolean("Variations.ExtendedSafeMode.BeaconFileWrite",
|
||
success);
|
||
}
|
||
|
||
} // namespace metrics
|