| // 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( |
| std::optional<bool> beacon_file_beacon_value, |
| std::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) { |
| std::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.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<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); |
| |
| std::optional<bool> 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<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.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<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 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 |