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