| // Copyright 2018 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 "chrome/browser/resource_coordinator/local_site_characteristics_data_impl.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/resource_coordinator/local_site_characteristics_database.h" |
| #include "chrome/browser/resource_coordinator/time.h" |
| |
| namespace resource_coordinator { |
| namespace internal { |
| |
| namespace { |
| |
| base::TimeDelta GetTickDeltaSinceEpoch() { |
| return NowTicks() - base::TimeTicks::UnixEpoch(); |
| } |
| |
| // Returns all the SiteCharacteristicsFeatureProto elements contained in a |
| // SiteCharacteristicsProto protobuf object. |
| std::vector<SiteCharacteristicsFeatureProto*> GetAllFeaturesFromProto( |
| SiteCharacteristicsProto* proto) { |
| std::vector<SiteCharacteristicsFeatureProto*> ret( |
| {proto->mutable_updates_favicon_in_background(), |
| proto->mutable_updates_title_in_background(), |
| proto->mutable_uses_audio_in_background(), |
| proto->mutable_uses_notifications_in_background()}); |
| |
| return ret; |
| } |
| |
| } // namespace |
| |
| void LocalSiteCharacteristicsDataImpl::NotifySiteLoaded() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Update the last loaded time when this origin gets loaded for the first |
| // time. |
| if (loaded_tabs_count_ == 0) { |
| site_characteristics_.set_last_loaded( |
| TimeDeltaToInternalRepresentation(GetTickDeltaSinceEpoch())); |
| |
| is_dirty_ = true; |
| } |
| loaded_tabs_count_++; |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifySiteUnloaded( |
| TabVisibility tab_visibility) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (tab_visibility == TabVisibility::kBackground) |
| DecrementNumLoadedBackgroundTabs(); |
| |
| loaded_tabs_count_--; |
| // Only update the last loaded time when there's no more loaded instance of |
| // this origin. |
| if (loaded_tabs_count_ > 0U) |
| return; |
| |
| base::TimeDelta current_unix_time = GetTickDeltaSinceEpoch(); |
| |
| // Update the |last_loaded_time_| field, as the moment this site gets unloaded |
| // also corresponds to the last moment it was loaded. |
| site_characteristics_.set_last_loaded( |
| TimeDeltaToInternalRepresentation(current_unix_time)); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyLoadedSiteBackgrounded() { |
| if (loaded_tabs_in_background_count_ == 0) |
| background_session_begin_ = NowTicks(); |
| |
| loaded_tabs_in_background_count_++; |
| |
| DCHECK_LE(loaded_tabs_in_background_count_, loaded_tabs_count_); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyLoadedSiteForegrounded() { |
| DecrementNumLoadedBackgroundTabs(); |
| } |
| |
| SiteFeatureUsage LocalSiteCharacteristicsDataImpl::UpdatesFaviconInBackground() |
| const { |
| return GetFeatureUsage( |
| site_characteristics_.updates_favicon_in_background(), |
| GetSiteCharacteristicsDatabaseParams().favicon_update_observation_window); |
| } |
| |
| SiteFeatureUsage LocalSiteCharacteristicsDataImpl::UpdatesTitleInBackground() |
| const { |
| return GetFeatureUsage( |
| site_characteristics_.updates_title_in_background(), |
| GetSiteCharacteristicsDatabaseParams().title_update_observation_window); |
| } |
| |
| SiteFeatureUsage LocalSiteCharacteristicsDataImpl::UsesAudioInBackground() |
| const { |
| return GetFeatureUsage( |
| site_characteristics_.uses_audio_in_background(), |
| GetSiteCharacteristicsDatabaseParams().audio_usage_observation_window); |
| } |
| |
| SiteFeatureUsage |
| LocalSiteCharacteristicsDataImpl::UsesNotificationsInBackground() const { |
| return GetFeatureUsage( |
| site_characteristics_.uses_notifications_in_background(), |
| GetSiteCharacteristicsDatabaseParams() |
| .notifications_usage_observation_window); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyUpdatesFaviconInBackground() { |
| NotifyFeatureUsage( |
| site_characteristics_.mutable_updates_favicon_in_background(), |
| "FaviconUpdateInBackground"); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyUpdatesTitleInBackground() { |
| NotifyFeatureUsage( |
| site_characteristics_.mutable_updates_title_in_background(), |
| "TitleUpdateInBackground"); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyUsesAudioInBackground() { |
| NotifyFeatureUsage(site_characteristics_.mutable_uses_audio_in_background(), |
| "AudioUsageInBackground"); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyUsesNotificationsInBackground() { |
| NotifyFeatureUsage( |
| site_characteristics_.mutable_uses_notifications_in_background(), |
| "NotificationsUsageInBackground"); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::ExpireAllObservationWindowsForTesting() { |
| auto params = GetSiteCharacteristicsDatabaseParams(); |
| base::TimeDelta longest_observation_window = |
| std::max({params.favicon_update_observation_window, |
| params.title_update_observation_window, |
| params.audio_usage_observation_window, |
| params.notifications_usage_observation_window}); |
| for (auto* iter : GetAllFeaturesFromProto(&site_characteristics_)) |
| IncrementFeatureObservationDuration(iter, longest_observation_window); |
| } |
| |
| LocalSiteCharacteristicsDataImpl::LocalSiteCharacteristicsDataImpl( |
| const url::Origin& origin, |
| OnDestroyDelegate* delegate, |
| LocalSiteCharacteristicsDatabase* database) |
| : origin_(origin), |
| loaded_tabs_count_(0U), |
| loaded_tabs_in_background_count_(0U), |
| database_(database), |
| delegate_(delegate), |
| fully_initialized_(false), |
| is_dirty_(false), |
| weak_factory_(this) { |
| DCHECK(database_); |
| DCHECK(delegate_); |
| DCHECK(!site_characteristics_.IsInitialized()); |
| |
| database_->ReadSiteCharacteristicsFromDB( |
| origin_, base::BindOnce(&LocalSiteCharacteristicsDataImpl::OnInitCallback, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| LocalSiteCharacteristicsDataImpl::~LocalSiteCharacteristicsDataImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // All users of this object should make sure that they send the same number of |
| // NotifySiteLoaded and NotifySiteUnloaded events, in practice this mean |
| // tracking the loaded state and sending an unload event in their destructor |
| // if needed. |
| DCHECK(!IsLoaded()); |
| DCHECK_EQ(0U, loaded_tabs_in_background_count_); |
| |
| DCHECK(delegate_); |
| delegate_->OnLocalSiteCharacteristicsDataImplDestroyed(this); |
| |
| // TODO(sebmarchand): Some data might be lost here if the read operation has |
| // not completed, add some metrics to measure if this is really an issue. |
| if (is_dirty_ && fully_initialized_) { |
| DCHECK(site_characteristics_.IsInitialized()); |
| database_->WriteSiteCharacteristicsIntoDB(origin_, site_characteristics_); |
| } |
| } |
| |
| base::TimeDelta LocalSiteCharacteristicsDataImpl::FeatureObservationDuration( |
| const SiteCharacteristicsFeatureProto& feature_proto) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Get the current observation duration value, this'll be equal to 0 for |
| // features that have been observed. |
| base::TimeDelta observation_time_for_feature = |
| InternalRepresentationToTimeDelta(feature_proto.observation_duration()); |
| |
| // If this site is still in background and the feature isn't in use then the |
| // observation time since load needs to be added. |
| if (loaded_tabs_in_background_count_ > 0U && |
| InternalRepresentationToTimeDelta(feature_proto.use_timestamp()) |
| .is_zero()) { |
| base::TimeDelta observation_time_since_backgrounded = |
| NowTicks() - background_session_begin_; |
| observation_time_for_feature += observation_time_since_backgrounded; |
| } |
| |
| return observation_time_for_feature; |
| } |
| |
| // static: |
| void LocalSiteCharacteristicsDataImpl::IncrementFeatureObservationDuration( |
| SiteCharacteristicsFeatureProto* feature_proto, |
| base::TimeDelta extra_observation_duration) { |
| if (!feature_proto->has_use_timestamp() || |
| InternalRepresentationToTimeDelta(feature_proto->use_timestamp()) |
| .is_zero()) { |
| feature_proto->set_observation_duration(TimeDeltaToInternalRepresentation( |
| InternalRepresentationToTimeDelta( |
| feature_proto->observation_duration()) + |
| extra_observation_duration)); |
| } |
| } |
| |
| // static: |
| void LocalSiteCharacteristicsDataImpl:: |
| InitSiteCharacteristicsFeatureProtoWithDefaultValues( |
| SiteCharacteristicsFeatureProto* proto) { |
| DCHECK_NE(nullptr, proto); |
| static const auto zero_interval = |
| LocalSiteCharacteristicsDataImpl::TimeDeltaToInternalRepresentation( |
| base::TimeDelta()); |
| proto->set_observation_duration(zero_interval); |
| proto->set_use_timestamp(zero_interval); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::InitWithDefaultValues( |
| bool only_init_uninitialized_fields) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Initialize the feature elements with the default value, this is required |
| // because some fields might otherwise never be initialized. |
| for (auto* iter : GetAllFeaturesFromProto(&site_characteristics_)) { |
| if (!only_init_uninitialized_fields || !iter->IsInitialized()) |
| InitSiteCharacteristicsFeatureProtoWithDefaultValues(iter); |
| } |
| |
| if (!only_init_uninitialized_fields || |
| !site_characteristics_.has_last_loaded()) { |
| site_characteristics_.set_last_loaded( |
| LocalSiteCharacteristicsDataImpl::TimeDeltaToInternalRepresentation( |
| base::TimeDelta())); |
| } |
| } |
| |
| void LocalSiteCharacteristicsDataImpl:: |
| ClearObservationsAndInvalidateReadOperation() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Invalidate the weak pointer that have been served, this will ensure that |
| // this object doesn't get initialized from the database after being cleared. |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| // Reset all the observations. |
| InitWithDefaultValues(false); |
| |
| // Set the last loaded time to the current time if there's some loaded |
| // instances of this site. |
| if (IsLoaded()) { |
| site_characteristics_.set_last_loaded( |
| TimeDeltaToInternalRepresentation(GetTickDeltaSinceEpoch())); |
| } |
| |
| // This object is now in a valid state and can be written in the database. |
| fully_initialized_ = true; |
| } |
| |
| SiteFeatureUsage LocalSiteCharacteristicsDataImpl::GetFeatureUsage( |
| const SiteCharacteristicsFeatureProto& feature_proto, |
| const base::TimeDelta min_obs_time) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| UMA_HISTOGRAM_BOOLEAN( |
| "ResourceCoordinator.LocalDB.ReadHasCompletedBeforeQuery", |
| fully_initialized_); |
| |
| if (!feature_proto.IsInitialized()) |
| return SiteFeatureUsage::kSiteFeatureUsageUnknown; |
| |
| // Checks if this feature has already been observed. |
| // TODO(sebmarchand): Check the timestamp and reset features that haven't been |
| // observed in a long time, https://crbug.com/826446. |
| if (!InternalRepresentationToTimeDelta(feature_proto.use_timestamp()) |
| .is_zero()) { |
| return SiteFeatureUsage::kSiteFeatureInUse; |
| } |
| |
| if (FeatureObservationDuration(feature_proto) >= min_obs_time) |
| return SiteFeatureUsage::kSiteFeatureNotInUse; |
| |
| return SiteFeatureUsage::kSiteFeatureUsageUnknown; |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::NotifyFeatureUsage( |
| SiteCharacteristicsFeatureProto* feature_proto, |
| const char* feature_name) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(IsLoaded()); |
| DCHECK_GT(loaded_tabs_in_background_count_, 0U); |
| |
| // Report the observation time if this is the first time this feature is |
| // observed. |
| if (feature_proto->observation_duration() != 0) { |
| base::UmaHistogramCustomTimes( |
| base::StringPrintf( |
| "ResourceCoordinator.LocalDB.ObservationTimeBeforeFirstUse.%s", |
| feature_name), |
| InternalRepresentationToTimeDelta( |
| feature_proto->observation_duration()), |
| base::TimeDelta::FromSeconds(1), base::TimeDelta::FromDays(1), 100); |
| } |
| |
| feature_proto->set_use_timestamp( |
| TimeDeltaToInternalRepresentation(GetTickDeltaSinceEpoch())); |
| feature_proto->set_observation_duration( |
| LocalSiteCharacteristicsDataImpl::TimeDeltaToInternalRepresentation( |
| base::TimeDelta())); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::OnInitCallback( |
| base::Optional<SiteCharacteristicsProto> db_site_characteristics) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Check if the initialization has succeeded. |
| if (db_site_characteristics) { |
| // If so, iterates over all the features and initialize them. |
| auto this_features = GetAllFeaturesFromProto(&site_characteristics_); |
| auto db_features = |
| GetAllFeaturesFromProto(&db_site_characteristics.value()); |
| auto this_features_iter = this_features.begin(); |
| auto db_features_iter = db_features.begin(); |
| for (; this_features_iter != this_features.end() && |
| db_features_iter != db_features.end(); |
| ++this_features_iter, ++db_features_iter) { |
| // If the |use_timestamp| field is set for the in-memory entry for this |
| // feature then there's nothing to do, otherwise update it with the values |
| // from the database. |
| if (!(*this_features_iter)->has_use_timestamp()) { |
| if ((*db_features_iter)->has_use_timestamp()) { |
| // Keep the use timestamp from the database, if any. |
| (*this_features_iter) |
| ->set_use_timestamp((*db_features_iter)->use_timestamp()); |
| (*this_features_iter) |
| ->set_observation_duration( |
| LocalSiteCharacteristicsDataImpl:: |
| TimeDeltaToInternalRepresentation(base::TimeDelta())); |
| } else { |
| // Else, add the observation duration from the database to the |
| // in-memory observation duration. |
| if (!(*this_features_iter)->has_observation_duration()) { |
| (*this_features_iter) |
| ->set_observation_duration( |
| LocalSiteCharacteristicsDataImpl:: |
| TimeDeltaToInternalRepresentation(base::TimeDelta())); |
| } |
| IncrementFeatureObservationDuration( |
| (*this_features_iter), |
| InternalRepresentationToTimeDelta( |
| (*db_features_iter)->observation_duration())); |
| } |
| } |
| } |
| // Only update the last loaded field if we haven't updated it since the |
| // creation of this object. |
| if (!site_characteristics_.has_last_loaded()) { |
| site_characteristics_.set_last_loaded( |
| db_site_characteristics->last_loaded()); |
| } |
| } else { |
| // Init all the fields that haven't been initialized with a default value. |
| InitWithDefaultValues(true /* only_init_uninitialized_fields */); |
| } |
| |
| fully_initialized_ = true; |
| DCHECK(site_characteristics_.IsInitialized()); |
| } |
| |
| void LocalSiteCharacteristicsDataImpl::DecrementNumLoadedBackgroundTabs() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_GT(loaded_tabs_in_background_count_, 0U); |
| loaded_tabs_in_background_count_--; |
| // Only update the observation durations if there's no more backgounded |
| // instance of this origin. |
| if (loaded_tabs_in_background_count_ > 0U) |
| return; |
| |
| DCHECK(!background_session_begin_.is_null()); |
| base::TimeDelta extra_observation_duration = |
| NowTicks() - background_session_begin_; |
| |
| // Update the observation duration fields. |
| for (auto* iter : GetAllFeaturesFromProto(&site_characteristics_)) |
| IncrementFeatureObservationDuration(iter, extra_observation_duration); |
| } |
| |
| } // namespace internal |
| } // namespace resource_coordinator |