// 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 #include #include #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 #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( std::optional beacon_file_beacon_value, std::optional 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) { std::optional 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.com/40850830): 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 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 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 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); std::optional beacon_file_beacon_value = beacon_file_contents ? beacon_file_contents->GetDict().FindBool( prefs::kStabilityExitedCleanly) : std::nullopt; #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS) std::optional 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.com/40190558): 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.com/40851383): 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_ = std::make_optional(exited_cleanly); } #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS) std::optional 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 std::nullopt; #endif // BUILDFLAG(IS_WIN) #if BUILDFLAG(IS_IOS) if (HasUserDefaultsBeacon()) return GetUserDefaultsBeacon(); return std::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