| // Copyright 2019 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 <math.h> |
| #include <algorithm> |
| |
| #include <net/nqe/network_congestion_analyzer.h> |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "net/nqe/network_quality_estimator.h" |
| #include "net/url_request/url_request.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| // The threshold for the observed peak queueing delay in milliseconds. |
| // A peak queueing delay is HIGH if it exceeds this threshold. The value is the |
| // 98th percentile value of the peak queueing delay observed by all requests. |
| static constexpr int64_t kHighQueueingDelayMsec = 5000; |
| |
| // The minimal time interval between two consecutive empty queue observations |
| // when the number of in-flight requests is relatively low (i.e. 2). This time |
| // interval is required so that a new measurement period could start. |
| static constexpr int64_t kMinEmptyQueueObservingTimeMsec = 1500; |
| |
| // The min and max values for the peak queueing delay level. |
| static constexpr size_t kQueueingDelayLevelMinVal = 1; |
| static constexpr size_t kQueueingDelayLevelMaxVal = 10; |
| |
| // The array of thresholds for bucketizing a peak queueing delay sample. |
| constexpr base::TimeDelta kQueueingDelayBucketThresholds[] = { |
| base::TimeDelta::FromMilliseconds(0), |
| base::TimeDelta::FromMilliseconds(30), |
| base::TimeDelta::FromMilliseconds(60), |
| base::TimeDelta::FromMilliseconds(120), |
| base::TimeDelta::FromMilliseconds(250), |
| base::TimeDelta::FromMilliseconds(500), |
| base::TimeDelta::FromMilliseconds(1000), |
| base::TimeDelta::FromMilliseconds(2000), |
| base::TimeDelta::FromMilliseconds(4000), |
| base::TimeDelta::FromMilliseconds(8000)}; |
| |
| // The array of thresholds for determining whether a queueing delay sample is |
| // low under different effective connection types (ECTs). Based on the initial |
| // measurement, the queueing delay shows different distributions under different |
| // ECTs. For example, a 300-msec queueing delay is low in a 2G connection, and |
| // indicates the network queue is empty. However, the delay is the 90th |
| // percentile value on a 4G connection, and indicates many packets are in the |
| // network queue. These thresholds are the 33rd percentile values from these |
| // delay distributions. A default value (400 msec) is used when the ECT is |
| // UNKNOWN or OFFLINE. |
| constexpr base::TimeDelta |
| kLowQueueingDelayThresholds[EFFECTIVE_CONNECTION_TYPE_LAST] = { |
| base::TimeDelta::FromMilliseconds(400), |
| base::TimeDelta::FromMilliseconds(400), |
| base::TimeDelta::FromMilliseconds(400), |
| base::TimeDelta::FromMilliseconds(400), |
| base::TimeDelta::FromMilliseconds(40), |
| base::TimeDelta::FromMilliseconds(15)}; |
| |
| // The min and max values for the count of in-flight requests in |
| // |count_inflight_requests_to_queueing_delay_| cache. This range covers more |
| // than 95% of cases under all types of connection types. |
| static constexpr size_t kMinCountOfRequests = 1u; |
| static constexpr size_t kMaxCountOfRequests = 30u; |
| |
| // The max number of samples that can be hold in a bucket in the |
| // |count_inflight_requests_to_queueing_delay_| cache. |
| static constexpr size_t kMaxCountOfSamplesPerBucket = 10u; |
| |
| // The min value for a count-delay mapping sample to be reasonable enough to be |
| // inserted into the cache. |
| static constexpr size_t kMinScoreForValidSamples = 50; |
| |
| } // namespace |
| |
| namespace nqe { |
| |
| namespace internal { |
| |
| NetworkCongestionAnalyzer::NetworkCongestionAnalyzer( |
| NetworkQualityEstimator* network_quality_estimator, |
| const base::TickClock* tick_clock) |
| : network_quality_estimator_(network_quality_estimator), |
| tick_clock_(tick_clock), |
| recent_active_hosts_count_(0u), |
| count_inflight_requests_for_peak_queueing_delay_(0u), |
| peak_count_inflight_requests_measurement_period_(0u), |
| count_peak_queueing_delay_mapping_sample_(0u) { |
| DCHECK(tick_clock_); |
| DCHECK(network_quality_estimator_); |
| DCHECK_EQ(kQueueingDelayLevelMaxVal, |
| base::size(kQueueingDelayBucketThresholds)); |
| } |
| |
| NetworkCongestionAnalyzer::~NetworkCongestionAnalyzer() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| size_t NetworkCongestionAnalyzer::GetActiveHostsCount() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return recent_active_hosts_count_; |
| } |
| |
| void NetworkCongestionAnalyzer::NotifyStartTransaction( |
| const URLRequest& request) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Starts tracking the peak queueing delay after |request| starts. |
| TrackPeakQueueingDelayBegin(&request); |
| } |
| |
| void NetworkCongestionAnalyzer::NotifyRequestCompleted( |
| const URLRequest& request) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Ends tracking of the peak queueing delay. |
| base::Optional<base::TimeDelta> peak_observed_delay = |
| TrackPeakQueueingDelayEnd(&request); |
| if (peak_observed_delay.has_value()) { |
| // Records the peak queueing delay keyed by the request priority. |
| base::UmaHistogramMediumTimes( |
| "ResourceScheduler.PeakObservedQueueingDelay.Priority" + |
| base::NumberToString(request.priority()), |
| peak_observed_delay.value()); |
| |
| // Records the peak queueing delay for all types of requests. |
| UMA_HISTOGRAM_MEDIUM_TIMES("ResourceScheduler.PeakObservedQueueingDelay", |
| peak_observed_delay.value()); |
| } |
| } |
| |
| void NetworkCongestionAnalyzer::TrackPeakQueueingDelayBegin( |
| const URLRequest* request) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Returns if |request| has already been tracked. |
| if (request_peak_delay_.find(request) != request_peak_delay_.end()) |
| return; |
| |
| request_peak_delay_[request] = base::nullopt; |
| } |
| |
| base::Optional<base::TimeDelta> |
| NetworkCongestionAnalyzer::TrackPeakQueueingDelayEnd( |
| const URLRequest* request) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto request_delay = request_peak_delay_.find(request); |
| if (request_delay == request_peak_delay_.end()) |
| return base::nullopt; |
| |
| base::Optional<base::TimeDelta> peak_delay = request_delay->second; |
| request_peak_delay_.erase(request_delay); |
| return peak_delay; |
| } |
| |
| // net::EffectiveConnectionTypeObserver: |
| void NetworkCongestionAnalyzer::OnEffectiveConnectionTypeChanged( |
| net::EffectiveConnectionType effective_connection_type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (effective_connection_type_ == effective_connection_type) |
| return; |
| |
| effective_connection_type_ = effective_connection_type; |
| count_inflight_requests_to_queueing_delay_.clear(); |
| count_peak_queueing_delay_mapping_sample_ = 0; |
| } |
| |
| void NetworkCongestionAnalyzer::ComputeRecentQueueingDelay( |
| const std::map<nqe::internal::IPHash, nqe::internal::CanonicalStats>& |
| recent_rtt_stats, |
| const std::map<nqe::internal::IPHash, nqe::internal::CanonicalStats>& |
| historical_rtt_stats, |
| const int32_t downlink_kbps) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Updates downlink throughput if a new valid observation comes. |
| if (downlink_kbps != nqe::internal::INVALID_RTT_THROUGHPUT) |
| set_recent_downlink_throughput_kbps(downlink_kbps); |
| if (recent_rtt_stats.empty()) |
| return; |
| |
| int32_t delay_sample_sum = 0; |
| recent_active_hosts_count_ = 0u; |
| for (const auto& host_stats : recent_rtt_stats) { |
| nqe::internal::IPHash host = host_stats.first; |
| // Skip hosts that do not have historical statistics. |
| if (historical_rtt_stats.find(host) == historical_rtt_stats.end()) |
| continue; |
| |
| // Skip hosts that have one or fewer RTT samples or do not have the min |
| // value. They cannot provide an effective queueing delay sample. |
| if (historical_rtt_stats.at(host).observation_count <= 1 || |
| historical_rtt_stats.at(host).canonical_pcts.find(kStatVal0p) == |
| historical_rtt_stats.at(host).canonical_pcts.end()) |
| continue; |
| |
| ++recent_active_hosts_count_; |
| delay_sample_sum += |
| recent_rtt_stats.at(host).most_recent_val - |
| historical_rtt_stats.at(host).canonical_pcts.at(kStatVal0p); |
| } |
| |
| if (recent_active_hosts_count_ == 0u) |
| return; |
| |
| DCHECK_LT(0u, recent_active_hosts_count_); |
| |
| int32_t delay_ms = |
| delay_sample_sum / static_cast<int>(recent_active_hosts_count_); |
| recent_queueing_delay_ = base::TimeDelta::FromMilliseconds(delay_ms); |
| |
| // Updates the peak queueing delay for all tracked in-flight requests. |
| for (auto& it : request_peak_delay_) { |
| if (it.second.has_value()) { |
| it.second = std::max(it.second.value(), recent_queueing_delay_); |
| } else { |
| it.second = recent_queueing_delay_; |
| } |
| } |
| |
| if (recent_downlink_per_packet_time_ms_ != base::nullopt) { |
| recent_queue_length_ = static_cast<float>(delay_ms) / |
| recent_downlink_per_packet_time_ms_.value(); |
| } |
| } |
| |
| size_t NetworkCongestionAnalyzer::ComputePeakQueueingDelayLevel( |
| const base::TimeDelta& peak_queueing_delay) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_LE(base::TimeDelta(), peak_queueing_delay); |
| |
| // The range of queueing delay buckets includes all non-negative values. Thus, |
| // the non-negative peak queueing delay must be found in one of these buckets. |
| size_t level = kQueueingDelayLevelMaxVal; |
| while (level > kQueueingDelayLevelMinVal) { |
| // Stops searching if the peak queueing delay falls in the current bucket. |
| if (peak_queueing_delay >= kQueueingDelayBucketThresholds[level - 1]) |
| break; |
| |
| --level; |
| } |
| // The queueing delay level is from 1 (LOWEST) to 10 (HIGHEST). |
| DCHECK_LE(kQueueingDelayLevelMinVal, level); |
| DCHECK_GE(kQueueingDelayLevelMaxVal, level); |
| return level; |
| } |
| |
| bool NetworkCongestionAnalyzer::IsQueueingDelayLow( |
| const base::TimeDelta& delay) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| net::EffectiveConnectionType current_ect = |
| network_quality_estimator_->GetEffectiveConnectionType(); |
| return delay <= kLowQueueingDelayThresholds[current_ect]; |
| } |
| |
| bool NetworkCongestionAnalyzer::ShouldStartNewMeasurement( |
| const base::TimeDelta& delay, |
| size_t count_inflight_requests) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // The queue is not empty if either the queueing delay is high or the number |
| // of in-flight requests is high. |
| if (!IsQueueingDelayLow(delay) || count_inflight_requests >= 3) { |
| observing_empty_queue_timestamp_ = base::nullopt; |
| return false; |
| } |
| |
| // Starts a new measurement period immediately if there is very few number of |
| // in-flight requests. |
| if (count_inflight_requests <= 1) { |
| observing_empty_queue_timestamp_ = base::nullopt; |
| return true; |
| } |
| |
| base::TimeTicks now = tick_clock_->NowTicks(); |
| // Requires a sufficient time interval between consecutive empty queue |
| // observations to claim the queue is empty. |
| if (observing_empty_queue_timestamp_.has_value()) { |
| if (now - observing_empty_queue_timestamp_.value() >= |
| base::TimeDelta::FromMilliseconds(kMinEmptyQueueObservingTimeMsec)) { |
| observing_empty_queue_timestamp_ = base::nullopt; |
| return true; |
| } |
| } else { |
| observing_empty_queue_timestamp_ = now; |
| } |
| return false; |
| } |
| |
| void NetworkCongestionAnalyzer::UpdatePeakDelayMapping( |
| const base::TimeDelta& delay, |
| size_t count_inflight_requests) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Discards an abnormal observation. This high queueing delay is likely |
| // caused by retransmission packets from a previous measurement period. |
| if (delay >= base::TimeDelta::FromSeconds(20)) |
| return; |
| |
| if (ShouldStartNewMeasurement(delay, count_inflight_requests)) { |
| FinalizeCurrentMeasurementPeriod(); |
| |
| // Resets the tracked data for the new measurement period. |
| peak_queueing_delay_ = delay; |
| count_inflight_requests_for_peak_queueing_delay_ = count_inflight_requests; |
| peak_count_inflight_requests_measurement_period_ = count_inflight_requests; |
| } else { |
| // This is the logic to update the tracking data. |
| // First, updates the pending peak count of in-flight requests if a higher |
| // number of in-flight requests is observed. |
| // Second, updates the peak queueing delay and the peak count of inflight |
| // requests if a higher queueing delay is observed. The new peak queueing |
| // delay should be mapped to the peak count of in-flight requests that are |
| // observed before within this measurement period. |
| peak_count_inflight_requests_measurement_period_ = |
| std::max(peak_count_inflight_requests_measurement_period_, |
| count_inflight_requests); |
| |
| if (delay > peak_queueing_delay_) { |
| // Updates the peak queueing delay and the count of in-flight requests |
| // that are responsible for the delay. |
| peak_queueing_delay_ = delay; |
| count_inflight_requests_for_peak_queueing_delay_ = |
| peak_count_inflight_requests_measurement_period_; |
| } |
| } |
| } |
| |
| void NetworkCongestionAnalyzer::FinalizeCurrentMeasurementPeriod() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Does nothing if the peak count of in-flight requests is less than 3. |
| if (peak_count_inflight_requests_measurement_period_ < 3) |
| return; |
| |
| // Exports the tracked mapping data from the current measurement period. |
| // Updates the count of in-flight requests that would likely cause a high |
| // network queueing delay. |
| if (peak_queueing_delay_ >= |
| base::TimeDelta::FromMilliseconds(kHighQueueingDelayMsec)) { |
| count_inflight_requests_causing_high_delay_ = |
| count_inflight_requests_for_peak_queueing_delay_; |
| } |
| |
| UpdateRequestsCountAndPeakQueueingDelayMapping(); |
| } |
| |
| void NetworkCongestionAnalyzer:: |
| UpdateRequestsCountAndPeakQueueingDelayMapping() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| size_t truncated_count = |
| std::min(std::max(count_inflight_requests_for_peak_queueing_delay_, |
| kMinCountOfRequests), |
| kMaxCountOfRequests); |
| |
| DCHECK_LE(kMinCountOfRequests, truncated_count); |
| DCHECK_GE(kMaxCountOfRequests, truncated_count); |
| |
| base::Optional<size_t> mapping_score = |
| ComputePeakDelayMappingSampleScore(truncated_count, peak_queueing_delay_); |
| |
| // Discards the mapping sample if there are at least 10 samples in the cache |
| // and its score is less than the threshold. The purpose is to make the |
| // majority of cached samples reasonable so that they can be used to evaluate |
| // whether a new sample is valid or not. |
| if (count_peak_queueing_delay_mapping_sample_ >= 10 && |
| mapping_score.value_or(0) < kMinScoreForValidSamples) { |
| return; |
| } |
| |
| AddRequestsCountAndPeakQueueingDelaySample(truncated_count, |
| peak_queueing_delay_); |
| } |
| |
| base::Optional<size_t> |
| NetworkCongestionAnalyzer::ComputePeakDelayMappingSampleScore( |
| const size_t count_inflight_requests, |
| const base::TimeDelta& peak_queueing_delay) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (count_inflight_requests < kMinCountOfRequests || |
| count_inflight_requests > kMaxCountOfRequests) { |
| return base::nullopt; |
| } |
| |
| if (count_peak_queueing_delay_mapping_sample_ < 5) |
| return base::nullopt; |
| |
| size_t count_positive_samples = 0; |
| size_t delay_level = ComputePeakQueueingDelayLevel(peak_queueing_delay); |
| DCHECK_LE(1u, delay_level); |
| |
| for (const auto& cached_entry : count_inflight_requests_to_queueing_delay_) { |
| if (cached_entry.first < count_inflight_requests) { |
| for (const auto& it : cached_entry.second) { |
| if (it < peak_queueing_delay) |
| ++count_positive_samples; |
| } |
| } else if (cached_entry.first == count_inflight_requests) { |
| for (const auto& it : cached_entry.second) { |
| size_t it_delay_level = ComputePeakQueueingDelayLevel(it); |
| // Two samples are considered near if the difference in queueing delay |
| // levels is small. The absolute time difference is small for samples |
| // whose queueing delay level is from 1 to 5 (max val=500 msec). The two |
| // samples are also considered near if the absolute time difference is |
| // less than the 500 msec threshold. |
| if (it_delay_level <= delay_level + 1 || |
| it_delay_level >= delay_level - 1 || |
| std::abs(it.InMilliseconds() - |
| peak_queueing_delay.InMilliseconds()) <= 500) { |
| ++count_positive_samples; |
| } |
| } |
| } else { |
| for (const auto& it : cached_entry.second) { |
| if (it > peak_queueing_delay) |
| ++count_positive_samples; |
| } |
| } |
| } |
| |
| return count_positive_samples * 100 / |
| count_peak_queueing_delay_mapping_sample_; |
| } |
| |
| void NetworkCongestionAnalyzer::AddRequestsCountAndPeakQueueingDelaySample( |
| const size_t count_inflight_requests, |
| const base::TimeDelta& peak_queueing_delay) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (count_inflight_requests < kMinCountOfRequests || |
| count_inflight_requests > kMaxCountOfRequests) { |
| return; |
| } |
| |
| auto count_map_it = |
| count_inflight_requests_to_queueing_delay_.find(count_inflight_requests); |
| // Creates a new entry if no matching record is found. |
| if (count_map_it == count_inflight_requests_to_queueing_delay_.end()) { |
| count_inflight_requests_to_queueing_delay_[count_inflight_requests] |
| .push_front(peak_queueing_delay); |
| ++count_peak_queueing_delay_mapping_sample_; |
| } else { |
| count_map_it->second.push_front(peak_queueing_delay); |
| ++count_peak_queueing_delay_mapping_sample_; |
| |
| if (count_map_it->second.size() > kMaxCountOfSamplesPerBucket) { |
| count_map_it->second.pop_back(); |
| --count_peak_queueing_delay_mapping_sample_; |
| } |
| } |
| } |
| |
| void NetworkCongestionAnalyzer::set_recent_downlink_throughput_kbps( |
| const int32_t downlink_kbps) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| recent_downlink_throughput_kbps_ = downlink_kbps; |
| // Time in msec to transmit one TCP packet (1500 Bytes). |
| // |recent_downlink_per_packet_time_ms_| = 1500 * 8 / |
| // |recent_downlink_throughput_kbps_|. |
| recent_downlink_per_packet_time_ms_ = 12000 / downlink_kbps; |
| } |
| |
| } // namespace internal |
| |
| } // namespace nqe |
| |
| } // namespace net |