xref: /aosp_15_r20/external/cronet/components/metrics/clean_exit_beacon.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1 // Copyright 2014 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/metrics/clean_exit_beacon.h"
6 
7 #include <algorithm>
8 #include <memory>
9 #include <utility>
10 
11 #include "base/check_op.h"
12 #include "base/command_line.h"
13 #include "base/files/file_util.h"
14 #include "base/json/json_file_value_serializer.h"
15 #include "base/json/json_string_value_serializer.h"
16 #include "base/logging.h"
17 #include "base/metrics/field_trial.h"
18 #include "base/metrics/histogram_functions.h"
19 #include "base/metrics/histogram_macros.h"
20 #include "base/path_service.h"
21 #include "base/strings/string_number_conversions.h"
22 #include "base/strings/stringprintf.h"
23 #include "base/threading/thread_restrictions.h"
24 #include "build/build_config.h"
25 #include "components/metrics/metrics_pref_names.h"
26 #include "components/prefs/pref_registry_simple.h"
27 #include "components/prefs/pref_service.h"
28 #include "components/variations/pref_names.h"
29 #include "components/variations/variations_switches.h"
30 
31 #if BUILDFLAG(IS_WIN)
32 #include <windows.h>
33 
34 #include "base/strings/string_util_win.h"
35 #include "base/strings/utf_string_conversions.h"
36 #include "base/win/registry.h"
37 #endif
38 
39 namespace metrics {
40 
41 namespace {
42 
43 using ::variations::prefs::kVariationsCrashStreak;
44 
45 // Denotes whether Chrome should perform clean shutdown steps: signaling that
46 // Chrome is exiting cleanly and then CHECKing that is has shutdown cleanly.
47 // This may be modified by SkipCleanShutdownStepsForTesting().
48 bool g_skip_clean_shutdown_steps = false;
49 
50 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
51 // Records the the combined state of two distinct beacons' values in a
52 // histogram.
RecordBeaconConsistency(std::optional<bool> beacon_file_beacon_value,std::optional<bool> platform_specific_beacon_value)53 void RecordBeaconConsistency(
54     std::optional<bool> beacon_file_beacon_value,
55     std::optional<bool> platform_specific_beacon_value) {
56   CleanExitBeaconConsistency consistency =
57       CleanExitBeaconConsistency::kDirtyDirty;
58 
59   if (!beacon_file_beacon_value) {
60     if (!platform_specific_beacon_value) {
61       consistency = CleanExitBeaconConsistency::kMissingMissing;
62     } else {
63       consistency = platform_specific_beacon_value.value()
64                         ? CleanExitBeaconConsistency::kMissingClean
65                         : CleanExitBeaconConsistency::kMissingDirty;
66     }
67   } else if (!platform_specific_beacon_value) {
68     consistency = beacon_file_beacon_value.value()
69                       ? CleanExitBeaconConsistency::kCleanMissing
70                       : CleanExitBeaconConsistency::kDirtyMissing;
71   } else if (beacon_file_beacon_value.value()) {
72     consistency = platform_specific_beacon_value.value()
73                       ? CleanExitBeaconConsistency::kCleanClean
74                       : CleanExitBeaconConsistency::kCleanDirty;
75   } else {
76     consistency = platform_specific_beacon_value.value()
77                       ? CleanExitBeaconConsistency::kDirtyClean
78                       : CleanExitBeaconConsistency::kDirtyDirty;
79   }
80   base::UmaHistogramEnumeration("UMA.CleanExitBeaconConsistency3", consistency);
81 }
82 #endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
83 
84 // Increments kVariationsCrashStreak if |did_previous_session_exit_cleanly| is
85 // false. Also, emits the crash streak to a histogram.
86 //
87 // If |beacon_file_contents| are given, then the beacon file is used to retrieve
88 // the crash streak. Otherwise, |local_state| is used.
MaybeIncrementCrashStreak(bool did_previous_session_exit_cleanly,base::Value * beacon_file_contents,PrefService * local_state)89 void MaybeIncrementCrashStreak(bool did_previous_session_exit_cleanly,
90                                base::Value* beacon_file_contents,
91                                PrefService* local_state) {
92   int num_crashes;
93   if (beacon_file_contents) {
94     std::optional<int> crash_streak =
95         beacon_file_contents->GetDict().FindInt(kVariationsCrashStreak);
96     // Any contents without the key should have been rejected by
97     // MaybeGetFileContents().
98     DCHECK(crash_streak);
99     num_crashes = crash_streak.value();
100   } else {
101     // TODO(crbug.com/40850830): Consider not falling back to Local State for
102     // clients on platforms that support the beacon file.
103     num_crashes = local_state->GetInteger(kVariationsCrashStreak);
104   }
105 
106   if (!did_previous_session_exit_cleanly) {
107     // Increment the crash streak if the previous session crashed. Note that the
108     // streak is not cleared if the previous run didn’t crash. Instead, it’s
109     // incremented on each crash until Chrome is able to successfully fetch a
110     // new seed. This way, a seed update that mostly destabilizes Chrome still
111     // results in a fallback to Variations Safe Mode.
112     //
113     // The crash streak is incremented here rather than in a variations-related
114     // class for two reasons. First, the crash streak depends on whether Chrome
115     // exited cleanly in the last session, which is first checked via
116     // CleanExitBeacon::Initialize(). Second, if the crash streak were updated
117     // in another function, any crash between beacon initialization and the
118     // other function might cause the crash streak to not be to incremented.
119     // "Might" because the updated crash streak also needs to be persisted to
120     // disk. A consequence of failing to increment the crash streak is that
121     // Chrome might undercount or be completely unaware of repeated crashes
122     // early on in startup.
123     ++num_crashes;
124     // For platforms that use the beacon file, the crash streak is written
125     // synchronously to disk later on in startup via
126     // MaybeExtendVariationsSafeMode() and WriteBeaconFile(). The crash streak
127     // is intentionally not written to the beacon file here. If the beacon file
128     // indicates that Chrome failed to exit cleanly, then Chrome got at
129     // least as far as MaybeExtendVariationsSafeMode(), which is during the
130     // PostEarlyInitialization stage when native code is being synchronously
131     // executed. Chrome should also be able to reach that point in this session.
132     //
133     // For platforms that do not use the beacon file, the crash streak is
134     // scheduled to be written to disk later on in startup. At the latest, this
135     // is done when a Local State write is scheduled via WriteBeaconFile(). A
136     // write is not scheduled here for three reasons.
137     //
138     // 1. It is an expensive operation.
139     // 2. Android WebLayer (one of the two platforms that does not use the
140     //    beacon file) did not appear to benefit from scheduling the write. See
141     //    crbug/1341850 for details.
142     // 3. Android WebView (the other beacon-file-less platform) has its own
143     //    Variations Safe Mode mechanism and does not need the crash streak.
144     local_state->SetInteger(kVariationsCrashStreak, num_crashes);
145   }
146   base::UmaHistogramSparse("Variations.SafeMode.Streak.Crashes",
147                            std::clamp(num_crashes, 0, 100));
148 }
149 
150 // Records |file_state| in a histogram.
RecordBeaconFileState(BeaconFileState file_state)151 void RecordBeaconFileState(BeaconFileState file_state) {
152   base::UmaHistogramEnumeration(
153       "Variations.ExtendedSafeMode.BeaconFileStateAtStartup", file_state);
154 }
155 
156 // Returns the contents of the file at |beacon_file_path| if the following
157 // conditions are all true. Otherwise, returns nullptr.
158 //
159 // 1. The file path is non-empty.
160 // 2. The file exists.
161 // 3. The file is successfully read.
162 // 4. The file contents are in the expected format with the expected info.
163 //
164 // The file may not exist for the below reasons:
165 //
166 // 1. The file is unsupported on the platform.
167 // 2. This is the first session after a client updates to or installs a Chrome
168 //    version that uses the beacon file. The beacon file launched on desktop
169 //    and iOS in M102 and on Android Chrome in M103.
170 // 3. Android Chrome clients with only background sessions may never write a
171 //    beacon file.
172 // 4. A user may delete the file.
MaybeGetFileContents(const base::FilePath & beacon_file_path)173 std::unique_ptr<base::Value> MaybeGetFileContents(
174     const base::FilePath& beacon_file_path) {
175   if (beacon_file_path.empty())
176     return nullptr;
177 
178   int error_code;
179   JSONFileValueDeserializer deserializer(beacon_file_path);
180   std::unique_ptr<base::Value> beacon_file_contents =
181       deserializer.Deserialize(&error_code, /*error_message=*/nullptr);
182 
183   if (!beacon_file_contents) {
184     RecordBeaconFileState(BeaconFileState::kNotDeserializable);
185     base::UmaHistogramSparse(
186         "Variations.ExtendedSafeMode.BeaconFileDeserializationError",
187         error_code);
188     return nullptr;
189   }
190   if (!beacon_file_contents->is_dict() ||
191       beacon_file_contents->GetDict().empty()) {
192     RecordBeaconFileState(BeaconFileState::kMissingDictionary);
193     return nullptr;
194   }
195   const base::Value::Dict& beacon_dict = beacon_file_contents->GetDict();
196   if (!beacon_dict.FindInt(kVariationsCrashStreak)) {
197     RecordBeaconFileState(BeaconFileState::kMissingCrashStreak);
198     return nullptr;
199   }
200   if (!beacon_dict.FindBool(prefs::kStabilityExitedCleanly)) {
201     RecordBeaconFileState(BeaconFileState::kMissingBeacon);
202     return nullptr;
203   }
204   RecordBeaconFileState(BeaconFileState::kReadable);
205   return beacon_file_contents;
206 }
207 
208 }  // namespace
209 
210 const base::FilePath::CharType kCleanExitBeaconFilename[] =
211     FILE_PATH_LITERAL("Variations");
212 
CleanExitBeacon(const std::wstring & backup_registry_key,const base::FilePath & user_data_dir,PrefService * local_state)213 CleanExitBeacon::CleanExitBeacon(const std::wstring& backup_registry_key,
214                                  const base::FilePath& user_data_dir,
215                                  PrefService* local_state)
216     : backup_registry_key_(backup_registry_key),
217       user_data_dir_(user_data_dir),
218       local_state_(local_state),
219       initial_browser_last_live_timestamp_(
220           local_state->GetTime(prefs::kStabilityBrowserLastLiveTimeStamp)) {
221   DCHECK_NE(PrefService::INITIALIZATION_STATUS_WAITING,
222             local_state_->GetInitializationStatus());
223 }
224 
Initialize()225 void CleanExitBeacon::Initialize() {
226   DCHECK(!initialized_);
227 
228   if (!user_data_dir_.empty()) {
229     // Platforms that pass an empty path do so deliberately. They should not
230     // use the beacon file.
231     beacon_file_path_ = user_data_dir_.Append(kCleanExitBeaconFilename);
232   }
233 
234   std::unique_ptr<base::Value> beacon_file_contents =
235       MaybeGetFileContents(beacon_file_path_);
236 
237   did_previous_session_exit_cleanly_ =
238       DidPreviousSessionExitCleanly(beacon_file_contents.get());
239 
240   MaybeIncrementCrashStreak(did_previous_session_exit_cleanly_,
241                             beacon_file_contents.get(), local_state_);
242   initialized_ = true;
243 }
244 
DidPreviousSessionExitCleanly(base::Value * beacon_file_contents)245 bool CleanExitBeacon::DidPreviousSessionExitCleanly(
246     base::Value* beacon_file_contents) {
247   if (!IsBeaconFileSupported())
248     return local_state_->GetBoolean(prefs::kStabilityExitedCleanly);
249 
250   std::optional<bool> beacon_file_beacon_value =
251       beacon_file_contents ? beacon_file_contents->GetDict().FindBool(
252                                  prefs::kStabilityExitedCleanly)
253                            : std::nullopt;
254 
255 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
256   std::optional<bool> backup_beacon_value = ExitedCleanly();
257   RecordBeaconConsistency(beacon_file_beacon_value, backup_beacon_value);
258 #endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
259 
260 #if BUILDFLAG(IS_IOS)
261   // TODO(crbug.com/40190558): For the time being, this is a no-op; i.e.,
262   // ShouldUseUserDefaultsBeacon() always returns false.
263   if (ShouldUseUserDefaultsBeacon())
264     return backup_beacon_value.value_or(true);
265 #endif  // BUILDFLAG(IS_IOS)
266 
267   return beacon_file_beacon_value.value_or(true);
268 }
269 
IsExtendedSafeModeSupported() const270 bool CleanExitBeacon::IsExtendedSafeModeSupported() const {
271   // All platforms that support the beacon file mechanism also happen to support
272   // Extended Variations Safe Mode.
273   return IsBeaconFileSupported();
274 }
275 
WriteBeaconValue(bool exited_cleanly,bool is_extended_safe_mode)276 void CleanExitBeacon::WriteBeaconValue(bool exited_cleanly,
277                                        bool is_extended_safe_mode) {
278   DCHECK(initialized_);
279   if (g_skip_clean_shutdown_steps)
280     return;
281 
282   UpdateLastLiveTimestamp();
283 
284   if (has_exited_cleanly_ && has_exited_cleanly_.value() == exited_cleanly) {
285     // It is possible to call WriteBeaconValue() with the same value for
286     // |exited_cleanly| twice during startup and shutdown on some platforms. If
287     // the current beacon value matches |exited_cleanly|, then return here to
288     // skip redundantly updating Local State, writing a beacon file, and on
289     // Windows and iOS, writing to platform-specific locations.
290     return;
291   }
292 
293   if (is_extended_safe_mode) {
294     // |is_extended_safe_mode| can be true for only some platforms.
295     DCHECK(IsExtendedSafeModeSupported());
296     // |has_exited_cleanly_| should always be unset before starting to watch for
297     // browser crashes.
298     DCHECK(!has_exited_cleanly_);
299     // When starting to watch for browser crashes in the code covered by
300     // Extended Variations Safe Mode, the only valid value for |exited_cleanly|
301     // is `false`. `true` signals that Chrome should stop watching for crashes.
302     DCHECK(!exited_cleanly);
303     WriteBeaconFile(exited_cleanly);
304   } else {
305     // TODO(crbug.com/40851383): Stop updating |kStabilityExitedCleanly| on
306     // platforms that support the beacon file.
307     local_state_->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
308     if (IsBeaconFileSupported()) {
309       WriteBeaconFile(exited_cleanly);
310     } else {
311       // Schedule a Local State write on platforms that back the beacon value
312       // using Local State rather than the beacon file.
313       local_state_->CommitPendingWrite();
314     }
315   }
316 
317 #if BUILDFLAG(IS_WIN)
318   base::win::RegKey regkey;
319   if (regkey.Create(HKEY_CURRENT_USER, backup_registry_key_.c_str(),
320                     KEY_ALL_ACCESS) == ERROR_SUCCESS) {
321     regkey.WriteValue(base::ASCIIToWide(prefs::kStabilityExitedCleanly).c_str(),
322                       exited_cleanly ? 1u : 0u);
323   }
324 #elif BUILDFLAG(IS_IOS)
325   SetUserDefaultsBeacon(exited_cleanly);
326 #endif  // BUILDFLAG(IS_WIN)
327 
328   has_exited_cleanly_ = std::make_optional(exited_cleanly);
329 }
330 
331 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
ExitedCleanly()332 std::optional<bool> CleanExitBeacon::ExitedCleanly() {
333 #if BUILDFLAG(IS_WIN)
334   base::win::RegKey regkey;
335   DWORD value = 0u;
336   if (regkey.Open(HKEY_CURRENT_USER, backup_registry_key_.c_str(),
337                   KEY_ALL_ACCESS) == ERROR_SUCCESS &&
338       regkey.ReadValueDW(
339           base::ASCIIToWide(prefs::kStabilityExitedCleanly).c_str(), &value) ==
340           ERROR_SUCCESS) {
341     return value ? true : false;
342   }
343   return std::nullopt;
344 #endif  // BUILDFLAG(IS_WIN)
345 #if BUILDFLAG(IS_IOS)
346   if (HasUserDefaultsBeacon())
347     return GetUserDefaultsBeacon();
348   return std::nullopt;
349 #endif  // BUILDFLAG(IS_IOS)
350 }
351 #endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS)
352 
UpdateLastLiveTimestamp()353 void CleanExitBeacon::UpdateLastLiveTimestamp() {
354   local_state_->SetTime(prefs::kStabilityBrowserLastLiveTimeStamp,
355                         base::Time::Now());
356 }
357 
GetUserDataDirForTesting() const358 const base::FilePath CleanExitBeacon::GetUserDataDirForTesting() const {
359   return user_data_dir_;
360 }
361 
GetBeaconFilePathForTesting() const362 base::FilePath CleanExitBeacon::GetBeaconFilePathForTesting() const {
363   return beacon_file_path_;
364 }
365 
366 // static
RegisterPrefs(PrefRegistrySimple * registry)367 void CleanExitBeacon::RegisterPrefs(PrefRegistrySimple* registry) {
368   registry->RegisterBooleanPref(prefs::kStabilityExitedCleanly, true);
369 
370   registry->RegisterTimePref(prefs::kStabilityBrowserLastLiveTimeStamp,
371                              base::Time(), PrefRegistry::LOSSY_PREF);
372 
373   // This Variations-Safe-Mode-related pref is registered here rather than in
374   // SafeSeedManager::RegisterPrefs() because the CleanExitBeacon is
375   // responsible for incrementing this value. (See the comments in
376   // MaybeIncrementCrashStreak() for more details.)
377   registry->RegisterIntegerPref(kVariationsCrashStreak, 0);
378 }
379 
380 // static
EnsureCleanShutdown(PrefService * local_state)381 void CleanExitBeacon::EnsureCleanShutdown(PrefService* local_state) {
382   if (!g_skip_clean_shutdown_steps)
383     CHECK(local_state->GetBoolean(prefs::kStabilityExitedCleanly));
384 }
385 
386 // static
SetStabilityExitedCleanlyForTesting(PrefService * local_state,bool exited_cleanly)387 void CleanExitBeacon::SetStabilityExitedCleanlyForTesting(
388     PrefService* local_state,
389     bool exited_cleanly) {
390   local_state->SetBoolean(prefs::kStabilityExitedCleanly, exited_cleanly);
391 #if BUILDFLAG(IS_IOS)
392   SetUserDefaultsBeacon(exited_cleanly);
393 #endif  // BUILDFLAG(IS_IOS)
394 }
395 
396 // static
CreateBeaconFileContentsForTesting(bool exited_cleanly,int crash_streak)397 std::string CleanExitBeacon::CreateBeaconFileContentsForTesting(
398     bool exited_cleanly,
399     int crash_streak) {
400   const std::string exited_cleanly_str = exited_cleanly ? "true" : "false";
401   return base::StringPrintf(
402       "{\n"
403       "  \"user_experience_metrics.stability.exited_cleanly\":%s,\n"
404       "  \"variations_crash_streak\":%s\n"
405       "}",
406       exited_cleanly_str.data(), base::NumberToString(crash_streak).data());
407 }
408 
409 // static
ResetStabilityExitedCleanlyForTesting(PrefService * local_state)410 void CleanExitBeacon::ResetStabilityExitedCleanlyForTesting(
411     PrefService* local_state) {
412   local_state->ClearPref(prefs::kStabilityExitedCleanly);
413 #if BUILDFLAG(IS_IOS)
414   ResetUserDefaultsBeacon();
415 #endif  // BUILDFLAG(IS_IOS)
416 }
417 
418 // static
SkipCleanShutdownStepsForTesting()419 void CleanExitBeacon::SkipCleanShutdownStepsForTesting() {
420   g_skip_clean_shutdown_steps = true;
421 }
422 
IsBeaconFileSupported() const423 bool CleanExitBeacon::IsBeaconFileSupported() const {
424   return !beacon_file_path_.empty();
425 }
426 
WriteBeaconFile(bool exited_cleanly) const427 void CleanExitBeacon::WriteBeaconFile(bool exited_cleanly) const {
428   base::Value::Dict dict;
429   dict.Set(prefs::kStabilityExitedCleanly, exited_cleanly);
430   dict.Set(kVariationsCrashStreak,
431            local_state_->GetInteger(kVariationsCrashStreak));
432 
433   std::string json_string;
434   JSONStringValueSerializer serializer(&json_string);
435   bool success = serializer.Serialize(dict);
436   DCHECK(success);
437   DCHECK(!json_string.empty());
438   {
439     base::ScopedAllowBlocking allow_io;
440     success = base::WriteFile(beacon_file_path_, json_string);
441   }
442   base::UmaHistogramBoolean("Variations.ExtendedSafeMode.BeaconFileWrite",
443                             success);
444 }
445 
446 }  // namespace metrics
447