| // 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 BUILDFLAG(IS_ANDROID) |
| if (!extended_monitoring_stage_start_time_.is_null()) { |
| // The time exists, so this is the transition from the extended browser |
| // crash monitoring stage to the status quo stage. |
| // |
| // TODO(crbug/1321989): Clean up this metric and |
| // |extended_monitoring_stage_start_time_| once Android Chrome |
| // stakeholders have enough data on the duration. |
| base::UmaHistogramLongTimes( |
| "UMA.CleanExitBeacon.ExtendedMonitoringStageDuration", |
| base::TimeTicks::Now() - extended_monitoring_stage_start_time_); |
| extended_monitoring_stage_start_time_ = base::TimeTicks(); // Null time. |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| 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); |
| #if BUILDFLAG(IS_ANDROID) |
| extended_monitoring_stage_start_time_ = base::TimeTicks::Now(); |
| #endif |
| |
| 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 |