1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
ash / metrics / feature_discovery_duration_reporter_impl.cc [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/metrics/feature_discovery_duration_reporter_impl.h"
#include "ash/public/cpp/feature_discovery_metric_util.h"
#include "ash/shell.h"
#include "base/containers/contains.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "ui/display/screen.h"
namespace ash {
namespace {
// Parameters used by the time duration metrics.
constexpr base::TimeDelta kTimeMetricsMin = base::Seconds(1);
constexpr base::TimeDelta kTimeMetricsMax = base::Days(7);
constexpr int kTimeMetricsBucketCount = 100;
// A dictionary that maps the features observed by
// `FeatureDiscoveryDurationReporter` to observation data (which is also a
// dictionary. See observation data dictionary keys for more details). A
// key-value mapping is added to the dictionary when the observation on a
// feature starts. The entries of the dictionary are never deleted after
// addition. It helps to avoid duplicate recordings on the same feature.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kObservedFeatures[] = "FeatureDiscoveryReporterObservedFeatures";
// Observation data dictionary keys --------------------------------------------
// The key to the cumulated time duration since the onbservation starts. This
// key and its paired value get cleared when the observation finishes.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kCumulatedDuration[] = "cumulative_duration";
// The key to the boolean value that indicates whether the observation finishes.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kIsObservationFinished[] = "is_observation_finished";
// The key to the boolean value that is true if the observation starts in
// tablet. This key should only be used when the metrics data collected from a
// tracked feature should be split by tablet mode.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kActivatedInTablet[] = "activated_in_tablet";
// Helper functions ------------------------------------------------------------
void ReportFeatureDiscoveryDuration(const char* histogram,
const base::TimeDelta& duration) {
base::UmaHistogramCustomTimes(histogram, duration, kTimeMetricsMin,
kTimeMetricsMax, kTimeMetricsBucketCount);
}
// Returns a trackable feature's info.
const feature_discovery::TrackableFeatureInfo& FindMappedFeatureInfo(
feature_discovery::TrackableFeature feature) {
auto iter =
base::ranges::find(feature_discovery::kTrackableFeatureArray, feature,
&feature_discovery::TrackableFeatureInfo::feature);
DCHECK(feature_discovery::kTrackableFeatureArray.cend() != iter);
return *iter;
}
// Returns a trackable feature's name.
const char* FindMappedName(feature_discovery::TrackableFeature feature) {
return FindMappedFeatureInfo(feature).name;
}
// Calculates the histogram for metric reporting. `feature` specifies a
// trackable feature. `in_tablet` is true if the observation on `feature` is
// activated in tablet.
// NOTE: if the metric reporting for `feature` is not separated by tablet mode,
// `in_tablet` is null.
const char* CalculateHistogram(feature_discovery::TrackableFeature feature,
std::optional<bool> in_tablet) {
const feature_discovery::TrackableFeatureInfo& info =
FindMappedFeatureInfo(feature);
if (!info.split_by_tablet_mode)
return info.histogram;
DCHECK(in_tablet);
return *in_tablet ? info.histogram_tablet : info.histogram_clamshell;
}
} // namespace
FeatureDiscoveryDurationReporterImpl::FeatureDiscoveryDurationReporterImpl(
SessionController* session_controller) {
session_controller_observation_.Observe(session_controller);
}
FeatureDiscoveryDurationReporterImpl::~FeatureDiscoveryDurationReporterImpl() {
// Handle the case when a user signs out all accounts. Store the states of
// the ongoing observations through the pref service.
SetActive(false);
}
// static
void FeatureDiscoveryDurationReporterImpl::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kObservedFeatures);
}
void FeatureDiscoveryDurationReporterImpl::MaybeActivateObservation(
feature_discovery::TrackableFeature feature) {
if (!is_active())
return;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
// If `feature` is already under observation, return early.
// TODO(https://crbug.com/1311344): implement the option that allows the
// observation start time gets reset by the subsequent observation
// activation callings.
const feature_discovery::TrackableFeatureInfo& info =
FindMappedFeatureInfo(feature);
const char* feature_name = info.name;
if (observed_features.Find(feature_name))
return;
// Initialize the pref data for the new observation.
base::Value::Dict observed_feature_data;
observed_feature_data.Set(kCumulatedDuration,
base::TimeDeltaToValue(base::TimeDelta()));
observed_feature_data.Set(kIsObservationFinished, false);
if (info.split_by_tablet_mode) {
// Record the current tablet mode if `feature`'s discovery duration data
// should be separated by tablet mode.
observed_feature_data.Set(kActivatedInTablet,
display::Screen::GetScreen()->InTabletMode());
}
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
update->Set(feature_name, std::move(observed_feature_data));
// Record observation start time.
DCHECK(!base::Contains(active_time_recordings_, feature));
active_time_recordings_.emplace(feature, base::TimeTicks::Now());
}
void FeatureDiscoveryDurationReporterImpl::MaybeFinishObservation(
feature_discovery::TrackableFeature feature) {
if (!is_active())
return;
// If the observation on the given metric has not started yet, return early.
auto iter = active_time_recordings_.find(feature);
if (iter == active_time_recordings_.end())
return;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const char* const feature_name = FindMappedName(feature);
const base::Value::Dict* feature_pref_data =
observed_features.Find(feature_name)->GetIfDict();
DCHECK(feature_pref_data);
const std::optional<base::TimeDelta> accumulated_duration =
base::ValueToTimeDelta(feature_pref_data->Find(kCumulatedDuration));
DCHECK(accumulated_duration);
bool skip_report = false;
// Get the boolean that indicates under which mode (clamshell or tablet) the
// observation is activated. If the metric data should not be separated, the
// value is null.
std::optional<bool> activated_in_tablet;
if (FindMappedFeatureInfo(feature).split_by_tablet_mode) {
activated_in_tablet = feature_pref_data->FindBool(kActivatedInTablet);
DCHECK(activated_in_tablet);
// It is abnormal to miss `activated_in_tablet`. Handle this case for
// safety. Skip metric reporting if `activated_in_tablet` is missing when
// the metric data should be split by tablet mode. One reason leading to
// this case is that a feature switches from non-split to tablet-mode-split
// due to later code changes.
if (!activated_in_tablet) {
LOG(ERROR) << "Cannot find the tablet mode state under which the feature "
"observation starts for "
<< FindMappedName(feature);
skip_report = true;
}
}
// Report metric data if there is no errors.
if (!skip_report) {
ReportFeatureDiscoveryDuration(
CalculateHistogram(feature, activated_in_tablet),
*accumulated_duration + base::TimeTicks::Now() - iter->second);
}
// Update the observed feature pref data by:
// 1. Clearing the cumulated duration
// 2. Marking that the observation finishes
// 3. Erasing the saved tablet state if any
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
base::Value::Dict* mutable_feature_pref_data = update->FindDict(feature_name);
mutable_feature_pref_data->Remove(kCumulatedDuration);
mutable_feature_pref_data->Set(kIsObservationFinished, true);
mutable_feature_pref_data->Remove(kActivatedInTablet);
active_time_recordings_.erase(iter);
}
void FeatureDiscoveryDurationReporterImpl::AddObserver(
ReporterObserver* observer) {
observers_.AddObserver(observer);
}
void FeatureDiscoveryDurationReporterImpl::RemoveObserver(
ReporterObserver* observer) {
observers_.RemoveObserver(observer);
}
void FeatureDiscoveryDurationReporterImpl::SetActive(bool active) {
// Return early if:
// 1. the activity state does not change; or
// 2. `active_pref_service_` is not set.
if (active == is_active() || !active_pref_service_)
return;
if (!active) {
Deactivate();
return;
}
Activate();
}
void FeatureDiscoveryDurationReporterImpl::Activate() {
// Disable the reporter for secondary accounts so that the feature discovery
// duration is only reported on primary accounts.
if (!Shell::Get()->session_controller()->IsUserPrimary())
return;
// Verify data members before activation.
DCHECK(active_time_recordings_.empty());
DCHECK(!is_active_);
DCHECK(active_pref_service_);
is_active_ = true;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const base::Value::Dict& immutable_observed_features_dict = observed_features;
// Iterate trackable features and resume unfinished observations.
for (const auto& feature_info : feature_discovery::kTrackableFeatureArray) {
// Skip the features that are not under observation.
const base::Value* feature_data =
immutable_observed_features_dict.Find(feature_info.name);
if (!feature_data)
continue;
// Skip the finished observations.
std::optional<bool> is_finished =
feature_data->GetDict().FindBool(kIsObservationFinished);
DCHECK(is_finished);
if (*is_finished)
continue;
active_time_recordings_.emplace(feature_info.feature,
base::TimeTicks::Now());
}
for (ReporterObserver& observer : observers_)
observer.OnReporterActivated();
}
void FeatureDiscoveryDurationReporterImpl::Deactivate() {
if (!active_time_recordings_.empty()) {
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
base::Value::Dict& mutable_observed_features_dict = update.Get();
// Store the accumulated time duration as pref data.
for (const auto& name_timestamp_pair : active_time_recordings_) {
// Fetch cumulated duration from pref service.
const char* feature_name = FindMappedName(name_timestamp_pair.first);
base::Value* feature_data =
mutable_observed_features_dict.Find(feature_name);
DCHECK(feature_data);
base::Value::Dict& mutable_data_dict = feature_data->GetDict();
const base::Value* cumulated_duration_value =
mutable_data_dict.Find(kCumulatedDuration);
DCHECK(cumulated_duration_value);
std::optional<base::TimeDelta> cumulated_duration =
base::ValueToTimeDelta(cumulated_duration_value);
DCHECK(cumulated_duration);
// Add the observation duration under the current active session. Then
// store the total duration.
mutable_data_dict.Set(
kCumulatedDuration,
base::TimeDeltaToValue(*cumulated_duration + base::TimeTicks::Now() -
name_timestamp_pair.second));
}
active_time_recordings_.clear();
}
is_active_ = false;
}
void FeatureDiscoveryDurationReporterImpl::OnSessionStateChanged(
session_manager::SessionState state) {
SetActive(state == session_manager::SessionState::ACTIVE);
}
void FeatureDiscoveryDurationReporterImpl::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
// Halt the observations for the old active account if any.
if (is_active())
SetActive(false);
active_pref_service_ = pref_service;
SetActive(Shell::Get()->session_controller()->GetSessionState() ==
session_manager::SessionState::ACTIVE);
}
} // namespace ash