// Copyright 2015 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 "components/data_usage/android/traffic_stats_amortizer.h" #include // For std::min. #include // For std::modf. #include #include #include "base/location.h" #include "base/metrics/field_trial.h" #include "base/metrics/histogram_base.h" #include "base/metrics/histogram_macros.h" #include "base/strings/string_number_conversions.h" #include "base/time/default_tick_clock.h" #include "base/timer/timer.h" #include "components/data_usage/core/data_use.h" #include "components/variations/variations_associated_data.h" #include "net/android/traffic_stats.h" namespace data_usage { namespace android { namespace { // Convenience typedef. typedef std::vector, DataUseAmortizer::AmortizationCompleteCallback>> DataUseBuffer; // Name of the field trial. const char kExternalDataUseObserverFieldTrial[] = "ExternalDataUseObserver"; // The delay between receiving DataUse and querying TrafficStats byte counts for // amortization. const int64_t kDefaultTrafficStatsQueryDelayMs = 50; // The longest amount of time that an amortization run can be delayed for. const int64_t kDefaultMaxAmortizationDelayMs = 500; // The maximum allowed size of the DataUse buffer. If the buffer ever exceeds // this size, then DataUse will be amortized immediately and the buffer will be // flushed. const size_t kDefaultMaxDataUseBufferSize = 128; base::TimeDelta GetTrafficStatsQueryDelay() { int64_t duration_ms = kDefaultTrafficStatsQueryDelayMs; std::string variation_value = variations::GetVariationParamValue( kExternalDataUseObserverFieldTrial, "traffic_stats_query_delay_ms"); if (!variation_value.empty() && base::StringToInt64(variation_value, &duration_ms) && duration_ms >= 0) { return base::TimeDelta::FromMilliseconds(duration_ms); } return base::TimeDelta::FromMilliseconds(kDefaultTrafficStatsQueryDelayMs); } base::TimeDelta GetMaxAmortizationDelay() { int64_t duration_ms = kDefaultMaxAmortizationDelayMs; std::string variation_value = variations::GetVariationParamValue( kExternalDataUseObserverFieldTrial, "max_amortization_delay_ms"); if (!variation_value.empty() && base::StringToInt64(variation_value, &duration_ms) && duration_ms >= 0) { return base::TimeDelta::FromMilliseconds(duration_ms); } return base::TimeDelta::FromMilliseconds(kDefaultMaxAmortizationDelayMs); } size_t GetMaxDataUseBufferSize() { size_t max_buffer_size = kDefaultMaxDataUseBufferSize; std::string variation_value = variations::GetVariationParamValue( kExternalDataUseObserverFieldTrial, "max_data_use_buffer_size"); if (!variation_value.empty() && base::StringToSizeT(variation_value, &max_buffer_size)) { return max_buffer_size; } return kDefaultMaxDataUseBufferSize; } // Returns |byte_count| as a histogram sample capped at the maximum histogram // sample value that's suitable for being recorded without overflowing. base::HistogramBase::Sample GetByteCountAsHistogramSample(int64_t byte_count) { DCHECK_GE(byte_count, 0); if (byte_count >= base::HistogramBase::kSampleType_MAX) { // Return kSampleType_MAX - 1 because it's invalid to record // kSampleType_MAX, which would cause a CHECK to fail in the histogram code. return base::HistogramBase::kSampleType_MAX - 1; } return static_cast(byte_count); } // Scales |bytes| by |ratio|, using |remainder| to hold the running rounding // error. |bytes| must be non-negative, and multiplying |bytes| by |ratio| must // yield a number that's representable within the bounds of a non-negative // int64_t. int64_t ScaleByteCount(int64_t bytes, double ratio, double* remainder) { DCHECK_GE(bytes, 0); DCHECK_GE(ratio, 0.0); DCHECK_LE(ratio, static_cast(INT64_MAX)); DCHECK_GE(*remainder, 0.0); DCHECK_LT(*remainder, 1.0); double intpart; *remainder = std::modf(static_cast(bytes) * ratio + (*remainder), &intpart); DCHECK_GE(intpart, 0.0); DCHECK_LE(intpart, static_cast(INT64_MAX)); DCHECK_GE(*remainder, 0.0); DCHECK_LT(*remainder, 1.0); // Due to floating point error, casting the double |intpart| to an int64_t // could cause it to overflow, even though it's already been checked to be // less than the double representation of INT64_MAX. If this happens, cap the // scaled value at INT64_MAX. uint64_t scaled_bytes = std::min(static_cast(intpart), static_cast(INT64_MAX)); return static_cast(scaled_bytes); } // Amortizes the difference between |desired_post_amortization_total| and // |pre_amortization_total| into each of the DataUse objects in // |data_use_sequence| by scaling the byte counts determined by the // |get_byte_count_fn| function (e.g. tx_bytes, rx_bytes) for each DataUse // appropriately. |pre_amortization_total| must not be 0. void AmortizeByteCountSequence(DataUseBuffer* data_use_sequence, int64_t* (*get_byte_count_fn)(DataUse*), int64_t pre_amortization_total, int64_t desired_post_amortization_total) { DCHECK_GT(pre_amortization_total, 0); DCHECK_GE(desired_post_amortization_total, 0); const double ratio = static_cast(desired_post_amortization_total) / static_cast(pre_amortization_total); double remainder = 0.0; for (auto& data_use_buffer_pair : *data_use_sequence) { int64_t* byte_count = get_byte_count_fn(data_use_buffer_pair.first.get()); *byte_count = ScaleByteCount(*byte_count, ratio, &remainder); } } int64_t* GetTxBytes(DataUse* data_use) { return &data_use->tx_bytes; } int64_t* GetRxBytes(DataUse* data_use) { return &data_use->rx_bytes; } // Returns the total transmitted bytes contained in |data_use_sequence|. int64_t GetTotalTxBytes(const DataUseBuffer& data_use_sequence) { int64_t sum = 0; for (const auto& data_use_buffer_pair : data_use_sequence) sum += data_use_buffer_pair.first->tx_bytes; return sum; } // Returns the total received bytes contained in |data_use_sequence|. int64_t GetTotalRxBytes(const DataUseBuffer& data_use_sequence) { int64_t sum = 0; for (const auto& data_use_buffer_pair : data_use_sequence) sum += data_use_buffer_pair.first->rx_bytes; return sum; } void RecordConcurrentTabsHistogram(const DataUseBuffer& data_use_buffer) { std::set unique_tabs; for (const auto& data_use_buffer_pair : data_use_buffer) unique_tabs.insert(data_use_buffer_pair.first->tab_id); UMA_HISTOGRAM_COUNTS_100("TrafficStatsAmortizer.ConcurrentTabs", unique_tabs.size()); } } // namespace TrafficStatsAmortizer::TrafficStatsAmortizer() : TrafficStatsAmortizer( scoped_ptr(new base::DefaultTickClock()), scoped_ptr(new base::Timer(false, false)), GetTrafficStatsQueryDelay(), GetMaxAmortizationDelay(), GetMaxDataUseBufferSize()) {} TrafficStatsAmortizer::~TrafficStatsAmortizer() {} void TrafficStatsAmortizer::AmortizeDataUse( scoped_ptr data_use, const AmortizationCompleteCallback& callback) { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(!callback.is_null()); int64_t tx_bytes = data_use->tx_bytes, rx_bytes = data_use->rx_bytes; // As an optimization, combine consecutive buffered DataUse objects that are // identical except for byte counts and have the same callback. if (!buffered_data_use_.empty() && buffered_data_use_.back().first->CanCombineWith(*data_use) && buffered_data_use_.back().second.Equals(callback)) { buffered_data_use_.back().first->tx_bytes += data_use->tx_bytes; buffered_data_use_.back().first->rx_bytes += data_use->rx_bytes; } else { buffered_data_use_.push_back( std::pair, AmortizationCompleteCallback>( std::move(data_use), callback)); } AddPreAmortizationBytes(tx_bytes, rx_bytes); } void TrafficStatsAmortizer::OnExtraBytes(int64_t extra_tx_bytes, int64_t extra_rx_bytes) { DCHECK(thread_checker_.CalledOnValidThread()); AddPreAmortizationBytes(extra_tx_bytes, extra_rx_bytes); } base::WeakPtr TrafficStatsAmortizer::GetWeakPtr() { DCHECK(thread_checker_.CalledOnValidThread()); return weak_ptr_factory_.GetWeakPtr(); } TrafficStatsAmortizer::TrafficStatsAmortizer( scoped_ptr tick_clock, scoped_ptr traffic_stats_query_timer, const base::TimeDelta& traffic_stats_query_delay, const base::TimeDelta& max_amortization_delay, size_t max_data_use_buffer_size) : tick_clock_(std::move(tick_clock)), traffic_stats_query_timer_(std::move(traffic_stats_query_timer)), traffic_stats_query_delay_(traffic_stats_query_delay), max_amortization_delay_(max_amortization_delay), max_data_use_buffer_size_(max_data_use_buffer_size), is_amortization_in_progress_(false), are_last_amortization_traffic_stats_available_(false), last_amortization_traffic_stats_tx_bytes_(-1), last_amortization_traffic_stats_rx_bytes_(-1), pre_amortization_tx_bytes_(0), pre_amortization_rx_bytes_(0), weak_ptr_factory_(this) {} bool TrafficStatsAmortizer::QueryTrafficStats(int64_t* tx_bytes, int64_t* rx_bytes) const { DCHECK(thread_checker_.CalledOnValidThread()); return net::android::traffic_stats::GetCurrentUidTxBytes(tx_bytes) && net::android::traffic_stats::GetCurrentUidRxBytes(rx_bytes); } void TrafficStatsAmortizer::AddPreAmortizationBytes(int64_t tx_bytes, int64_t rx_bytes) { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK_GE(tx_bytes, 0); DCHECK_GE(rx_bytes, 0); base::TimeTicks now_ticks = tick_clock_->NowTicks(); if (!is_amortization_in_progress_) { is_amortization_in_progress_ = true; current_amortization_run_start_time_ = now_ticks; } pre_amortization_tx_bytes_ += tx_bytes; pre_amortization_rx_bytes_ += rx_bytes; if (buffered_data_use_.size() > max_data_use_buffer_size_) { // Enforce a maximum limit on the size of |buffered_data_use_| to avoid // hogging memory. Note that this will likely cause the post-amortization // byte counts calculated here to be less accurate than if the amortizer // waited to perform amortization. traffic_stats_query_timer_->Stop(); AmortizeNow(); return; } // Cap any amortization delay to |max_amortization_delay_|. Note that if // |max_amortization_delay_| comes earlier, then this will likely cause the // post-amortization byte counts calculated here to be less accurate than if // the amortizer waited to perform amortization. base::TimeDelta query_delay = std::min( traffic_stats_query_delay_, current_amortization_run_start_time_ + max_amortization_delay_ - now_ticks); // Set the timer to query TrafficStats and amortize after a delay, so that // it's more likely that TrafficStats will be queried when the network is // idle. If the timer was already set, then this overrides the previous delay. traffic_stats_query_timer_->Start( FROM_HERE, query_delay, base::Bind(&TrafficStatsAmortizer::AmortizeNow, GetWeakPtr())); } void TrafficStatsAmortizer::AmortizeNow() { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(is_amortization_in_progress_); if (!buffered_data_use_.empty()) { // Record histograms for the pre-amortization byte counts of the DataUse // objects. UMA_HISTOGRAM_COUNTS( "TrafficStatsAmortizer.PreAmortizationRunDataUseBytes.Tx", GetByteCountAsHistogramSample(GetTotalTxBytes(buffered_data_use_))); UMA_HISTOGRAM_COUNTS( "TrafficStatsAmortizer.PreAmortizationRunDataUseBytes.Rx", GetByteCountAsHistogramSample(GetTotalRxBytes(buffered_data_use_))); } int64_t current_traffic_stats_tx_bytes = -1; int64_t current_traffic_stats_rx_bytes = -1; bool are_current_traffic_stats_available = QueryTrafficStats( ¤t_traffic_stats_tx_bytes, ¤t_traffic_stats_rx_bytes); if (are_current_traffic_stats_available && are_last_amortization_traffic_stats_available_ && !buffered_data_use_.empty()) { // These TrafficStats byte counts are guaranteed to increase monotonically // since device boot. DCHECK_GE(current_traffic_stats_tx_bytes, last_amortization_traffic_stats_tx_bytes_); DCHECK_GE(current_traffic_stats_rx_bytes, last_amortization_traffic_stats_rx_bytes_); // Only attempt to amortize network overhead from TrafficStats if any of // those bytes are reflected in the pre-amortization byte totals. Otherwise, // that network overhead will be amortized in a later amortization run. if (pre_amortization_tx_bytes_ != 0) { AmortizeByteCountSequence(&buffered_data_use_, &GetTxBytes, pre_amortization_tx_bytes_, current_traffic_stats_tx_bytes - last_amortization_traffic_stats_tx_bytes_); } if (pre_amortization_rx_bytes_ != 0) { AmortizeByteCountSequence(&buffered_data_use_, &GetRxBytes, pre_amortization_rx_bytes_, current_traffic_stats_rx_bytes - last_amortization_traffic_stats_rx_bytes_); } } if (!buffered_data_use_.empty()) { // Record histograms for the post-amortization byte counts of the DataUse // objects. UMA_HISTOGRAM_COUNTS( "TrafficStatsAmortizer.PostAmortizationRunDataUseBytes.Tx", GetByteCountAsHistogramSample(GetTotalTxBytes(buffered_data_use_))); UMA_HISTOGRAM_COUNTS( "TrafficStatsAmortizer.PostAmortizationRunDataUseBytes.Rx", GetByteCountAsHistogramSample(GetTotalRxBytes(buffered_data_use_))); RecordConcurrentTabsHistogram(buffered_data_use_); } UMA_HISTOGRAM_TIMES( "TrafficStatsAmortizer.AmortizationDelay", tick_clock_->NowTicks() - current_amortization_run_start_time_); UMA_HISTOGRAM_COUNTS_1000("TrafficStatsAmortizer.BufferSizeOnFlush", buffered_data_use_.size()); // Reset state now that the amortization run has finished. is_amortization_in_progress_ = false; current_amortization_run_start_time_ = base::TimeTicks(); // Don't update the previous amortization run's TrafficStats byte counts if // none of the bytes since then are reflected in the pre-amortization byte // totals. This way, the overhead that wasn't handled in this amortization run // can be handled in a later amortization run that actually has bytes in that // direction. This mitigates the problem of losing TrafficStats overhead bytes // on slow networks due to TrafficStats seeing the bytes much earlier than the // network stack reports them, or vice versa. if (!are_last_amortization_traffic_stats_available_ || pre_amortization_tx_bytes_ != 0) { last_amortization_traffic_stats_tx_bytes_ = current_traffic_stats_tx_bytes; } if (!are_last_amortization_traffic_stats_available_ || pre_amortization_rx_bytes_ != 0) { last_amortization_traffic_stats_rx_bytes_ = current_traffic_stats_rx_bytes; } are_last_amortization_traffic_stats_available_ = are_current_traffic_stats_available; pre_amortization_tx_bytes_ = 0; pre_amortization_rx_bytes_ = 0; DataUseBuffer data_use_sequence; data_use_sequence.swap(buffered_data_use_); // Pass post-amortization DataUse objects to their respective callbacks. for (auto& data_use_buffer_pair : data_use_sequence) data_use_buffer_pair.second.Run(std::move(data_use_buffer_pair.first)); } } // namespace android } // namespace data_usage