123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- // Copyright 2022 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 "ash/metrics/feature_discovery_duration_reporter_impl.h"
- #include "ash/public/cpp/feature_discovery_metric_util.h"
- #include "ash/public/cpp/tablet_mode.h"
- #include "ash/shell.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"
- 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_NE(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,
- absl::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_->GetValueDict(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,
- TabletMode::Get()->IsInTabletMode());
- }
- DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures);
- update->GetDict().Set(feature_name,
- base::Value(std::move(observed_feature_data)));
- // Record observation start time.
- DCHECK(active_time_recordings_.find(feature) ==
- active_time_recordings_.cend());
- 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_->GetValueDict(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 absl::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.
- absl::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
- DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures);
- base::Value::Dict* mutable_feature_pref_data =
- update->GetDict().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_->GetValueDict(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.
- absl::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()) {
- DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures);
- base::Value* observed_features = update.Get();
- DCHECK(observed_features);
- base::Value::Dict& mutable_observed_features_dict =
- observed_features->GetDict();
- // 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);
- absl::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
|