• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/nqe/throughput_analyzer.h"
6 
7 #include <cmath>
8 
9 #include "base/functional/bind.h"
10 #include "base/location.h"
11 #include "base/metrics/histogram_macros.h"
12 #include "base/task/single_thread_task_runner.h"
13 #include "base/time/tick_clock.h"
14 #include "net/base/host_port_pair.h"
15 #include "net/base/network_activity_monitor.h"
16 #include "net/base/url_util.h"
17 #include "net/nqe/network_quality_estimator.h"
18 #include "net/nqe/network_quality_estimator_params.h"
19 #include "net/nqe/network_quality_estimator_util.h"
20 #include "net/url_request/url_request.h"
21 #include "net/url_request/url_request_context.h"
22 
23 namespace net {
24 
25 class HostResolver;
26 
27 namespace {
28 
29 // Maximum number of accuracy degrading requests, and requests that do not
30 // degrade accuracy held in the memory.
31 static const size_t kMaxRequestsSize = 300;
32 
33 // Returns true if the request should be discarded because it does not provide
34 // meaningful observation.
ShouldDiscardRequest(const URLRequest & request)35 bool ShouldDiscardRequest(const URLRequest& request) {
36   return request.method() != "GET";
37 }
38 
39 }  // namespace
40 
41 namespace nqe::internal {
42 // The default content size of a HTML response body. It is set to the median
43 // HTML response content size, i.e. 1.8kB.
44 constexpr int64_t kDefaultContentSizeBytes = 1800;
45 
ThroughputAnalyzer(const NetworkQualityEstimator * network_quality_estimator,const NetworkQualityEstimatorParams * params,scoped_refptr<base::SingleThreadTaskRunner> task_runner,ThroughputObservationCallback throughput_observation_callback,const base::TickClock * tick_clock,const NetLogWithSource & net_log)46 ThroughputAnalyzer::ThroughputAnalyzer(
47     const NetworkQualityEstimator* network_quality_estimator,
48     const NetworkQualityEstimatorParams* params,
49     scoped_refptr<base::SingleThreadTaskRunner> task_runner,
50     ThroughputObservationCallback throughput_observation_callback,
51     const base::TickClock* tick_clock,
52     const NetLogWithSource& net_log)
53     : network_quality_estimator_(network_quality_estimator),
54       params_(params),
55       task_runner_(task_runner),
56       throughput_observation_callback_(throughput_observation_callback),
57       tick_clock_(tick_clock),
58       last_connection_change_(tick_clock_->NowTicks()),
59       window_start_time_(base::TimeTicks()),
60       net_log_(net_log) {
61   DCHECK(tick_clock_);
62   DCHECK(network_quality_estimator_);
63   DCHECK(params_);
64   DCHECK(task_runner_);
65   DCHECK(!IsCurrentlyTrackingThroughput());
66 }
67 
~ThroughputAnalyzer()68 ThroughputAnalyzer::~ThroughputAnalyzer() {
69   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
70 }
71 
MaybeStartThroughputObservationWindow()72 void ThroughputAnalyzer::MaybeStartThroughputObservationWindow() {
73   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
74 
75   if (disable_throughput_measurements_)
76     return;
77 
78   // Throughput observation window can be started only if no accuracy degrading
79   // requests are currently active, the observation window is not already
80   // started, and there is at least one active request that does not degrade
81   // throughput computation accuracy.
82   if (accuracy_degrading_requests_.size() > 0 ||
83       IsCurrentlyTrackingThroughput() ||
84       requests_.size() < params_->throughput_min_requests_in_flight()) {
85     return;
86   }
87   window_start_time_ = tick_clock_->NowTicks();
88   bits_received_at_window_start_ = GetBitsReceived();
89 }
90 
EndThroughputObservationWindow()91 void ThroughputAnalyzer::EndThroughputObservationWindow() {
92   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
93 
94   // Mark the throughput observation window as stopped by resetting the window
95   // parameters.
96   window_start_time_ = base::TimeTicks();
97   bits_received_at_window_start_ = 0;
98   DCHECK(!IsCurrentlyTrackingThroughput());
99 }
100 
IsCurrentlyTrackingThroughput() const101 bool ThroughputAnalyzer::IsCurrentlyTrackingThroughput() const {
102   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
103 
104   if (window_start_time_.is_null())
105     return false;
106 
107   // If the throughput observation window is running, then at least one request
108   // that does not degrade throughput computation accuracy should be active.
109   DCHECK_GT(requests_.size(), 0U);
110 
111   // If the throughput observation window is running, then no accuracy degrading
112   // requests should be currently active.
113   DCHECK_EQ(0U, accuracy_degrading_requests_.size());
114 
115   DCHECK_LE(params_->throughput_min_requests_in_flight(), requests_.size());
116 
117   return true;
118 }
119 
SetTickClockForTesting(const base::TickClock * tick_clock)120 void ThroughputAnalyzer::SetTickClockForTesting(
121     const base::TickClock* tick_clock) {
122   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
123   tick_clock_ = tick_clock;
124   DCHECK(tick_clock_);
125 }
126 
UpdateResponseContentSize(const URLRequest * request,int64_t response_size)127 void ThroughputAnalyzer::UpdateResponseContentSize(const URLRequest* request,
128                                                    int64_t response_size) {
129   DCHECK_LE(0, response_size);
130   // Updates the map and the counter. Subtracts the previous stored response
131   // content size if an old record exists in the map.
132   if (response_content_sizes_.find(request) != response_content_sizes_.end()) {
133     total_response_content_size_ +=
134         response_size - response_content_sizes_[request];
135   } else {
136     total_response_content_size_ += response_size;
137   }
138   response_content_sizes_[request] = response_size;
139 }
140 
NotifyStartTransaction(const URLRequest & request)141 void ThroughputAnalyzer::NotifyStartTransaction(const URLRequest& request) {
142   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
143 
144   UpdateResponseContentSize(&request, kDefaultContentSizeBytes);
145 
146   if (disable_throughput_measurements_)
147     return;
148 
149   const bool degrades_accuracy = DegradesAccuracy(request);
150   if (degrades_accuracy) {
151     accuracy_degrading_requests_.insert(&request);
152 
153     BoundRequestsSize();
154 
155     // Call EndThroughputObservationWindow since observations cannot be
156     // recorded in the presence of requests that degrade throughput computation
157     // accuracy.
158     EndThroughputObservationWindow();
159     DCHECK(!IsCurrentlyTrackingThroughput());
160     return;
161   } else if (ShouldDiscardRequest(request)) {
162     return;
163   }
164 
165   EraseHangingRequests(request);
166 
167   requests_[&request] = tick_clock_->NowTicks();
168   BoundRequestsSize();
169   MaybeStartThroughputObservationWindow();
170 }
171 
NotifyBytesRead(const URLRequest & request)172 void ThroughputAnalyzer::NotifyBytesRead(const URLRequest& request) {
173   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
174 
175   if (disable_throughput_measurements_)
176     return;
177 
178   EraseHangingRequests(request);
179 
180   if (requests_.erase(&request) == 0)
181     return;
182 
183   // Update the time when the bytes were received for |request|.
184   requests_[&request] = tick_clock_->NowTicks();
185 }
186 
NotifyRequestCompleted(const URLRequest & request)187 void ThroughputAnalyzer::NotifyRequestCompleted(const URLRequest& request) {
188   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
189 
190   // Remove the request from the inflight requests if it presents in the map.
191   if (response_content_sizes_.find(&request) != response_content_sizes_.end()) {
192     total_response_content_size_ -= response_content_sizes_[&request];
193     response_content_sizes_.erase(&request);
194   }
195 
196   if (disable_throughput_measurements_)
197     return;
198 
199   // Return early if the |request| is not present in the collections of
200   // requests. This may happen when a completed request is later destroyed.
201   if (requests_.find(&request) == requests_.end() &&
202       accuracy_degrading_requests_.find(&request) ==
203           accuracy_degrading_requests_.end()) {
204     return;
205   }
206 
207   EraseHangingRequests(request);
208 
209   int32_t downstream_kbps = -1;
210   if (MaybeGetThroughputObservation(&downstream_kbps)) {
211     // Notify the provided callback.
212     task_runner_->PostTask(
213         FROM_HERE,
214         base::BindOnce(throughput_observation_callback_, downstream_kbps));
215   }
216 
217   // Try to remove the request from either |accuracy_degrading_requests_| or
218   // |requests_|, since it is no longer active.
219   if (accuracy_degrading_requests_.erase(&request) == 1u) {
220     // Generally, |request| cannot be in both |accuracy_degrading_requests_|
221     // and |requests_| at the same time. However, in some cases, the same
222     // request may appear in both vectors. See https://crbug.com/849604 for
223     // more details.
224     // It's safe to delete |request| from |requests_| since (i)
225     // The observation window is currently not recording throughput, and (ii)
226     // |requests_| is a best effort guess of requests that are currently
227     // in-flight.
228     DCHECK(!IsCurrentlyTrackingThroughput());
229     requests_.erase(&request);
230 
231     // If a request that degraded the accuracy of throughput computation has
232     // completed, then it may be possible to start the tracking window.
233     MaybeStartThroughputObservationWindow();
234     return;
235   }
236 
237   if (requests_.erase(&request) == 1u) {
238     // If there is no network activity, stop tracking throughput to prevent
239     // recording of any observations.
240     if (requests_.size() < params_->throughput_min_requests_in_flight())
241       EndThroughputObservationWindow();
242     return;
243   }
244   MaybeStartThroughputObservationWindow();
245 }
246 
NotifyExpectedResponseContentSize(const URLRequest & request,int64_t expected_content_size)247 void ThroughputAnalyzer::NotifyExpectedResponseContentSize(
248     const URLRequest& request,
249     int64_t expected_content_size) {
250   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
251   // Updates when the value is valid.
252   if (expected_content_size >= 0) {
253     UpdateResponseContentSize(&request, expected_content_size);
254   }
255 }
256 
IsHangingWindow(int64_t bits_received,base::TimeDelta duration) const257 bool ThroughputAnalyzer::IsHangingWindow(int64_t bits_received,
258                                          base::TimeDelta duration) const {
259   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
260 
261   if (params_->throughput_hanging_requests_cwnd_size_multiplier() <= 0)
262     return false;
263 
264   if (params_->use_small_responses())
265     return false;
266 
267   if (!duration.is_positive())
268     return false;
269 
270   // Initial congestion window size for TCP connections.
271   static constexpr size_t kCwndSizeKilobytes = 10 * 1.5;
272   static constexpr size_t kCwndSizeBits = kCwndSizeKilobytes * 1000 * 8;
273 
274   // Scale the |duration| to one HTTP RTT, and compute the number of bits that
275   // would be received over a duration of one HTTP RTT.
276   size_t bits_received_over_one_http_rtt =
277       bits_received *
278       (network_quality_estimator_->GetHttpRTT().value_or(base::Seconds(10)) /
279        duration);
280 
281   // If |is_hanging| is true, it implies that less than
282   // kCwndSizeKilobytes were received over a period of 1 HTTP RTT. For a network
283   // that is not under-utilized, it is expected that at least |kCwndSizeBits|
284   // are received over a duration of 1 HTTP RTT.
285   bool is_hanging =
286       bits_received_over_one_http_rtt <
287       (kCwndSizeBits *
288        params_->throughput_hanging_requests_cwnd_size_multiplier());
289 
290   return is_hanging;
291 }
292 
MaybeGetThroughputObservation(int32_t * downstream_kbps)293 bool ThroughputAnalyzer::MaybeGetThroughputObservation(
294     int32_t* downstream_kbps) {
295   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
296   DCHECK(downstream_kbps);
297 
298   if (disable_throughput_measurements_)
299     return false;
300 
301   // Return early if the window that records downstream throughput is currently
302   // inactive because throughput observations can be taken only when the window
303   // is active.
304   if (!IsCurrentlyTrackingThroughput())
305     return false;
306 
307   DCHECK_GE(requests_.size(), params_->throughput_min_requests_in_flight());
308   DCHECK_EQ(0U, accuracy_degrading_requests_.size());
309 
310   base::TimeTicks now = tick_clock_->NowTicks();
311 
312   int64_t bits_received = GetBitsReceived() - bits_received_at_window_start_;
313   DCHECK_LE(window_start_time_, now);
314   DCHECK_LE(0, bits_received);
315   const base::TimeDelta duration = now - window_start_time_;
316 
317   // Ignore tiny/short transfers, which will not produce accurate rates. Skip
318   // the checks if |use_small_responses_| is true.
319   if (!params_->use_small_responses() &&
320       bits_received < params_->GetThroughputMinTransferSizeBits()) {
321     return false;
322   }
323 
324   double downstream_kbps_double = bits_received * duration.ToHz() / 1000;
325 
326   if (IsHangingWindow(bits_received, duration)) {
327     requests_.clear();
328     EndThroughputObservationWindow();
329     return false;
330   }
331 
332   // Round-up |downstream_kbps_double|.
333   *downstream_kbps = static_cast<int64_t>(std::ceil(downstream_kbps_double));
334   DCHECK(IsCurrentlyTrackingThroughput());
335 
336   // Stop the observation window since a throughput measurement has been taken.
337   EndThroughputObservationWindow();
338   DCHECK(!IsCurrentlyTrackingThroughput());
339 
340   // Maybe start the throughput observation window again so that another
341   // throughput measurement can be taken.
342   MaybeStartThroughputObservationWindow();
343   return true;
344 }
345 
OnConnectionTypeChanged()346 void ThroughputAnalyzer::OnConnectionTypeChanged() {
347   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
348 
349   // All the requests that were previously not degrading the througpput
350   // computation are now spanning a connection change event. These requests
351   // would now degrade the throughput computation accuracy. So, move them to
352   // |accuracy_degrading_requests_|.
353   for (const auto& request : requests_) {
354     accuracy_degrading_requests_.insert(request.first);
355   }
356   requests_.clear();
357   BoundRequestsSize();
358   EndThroughputObservationWindow();
359 
360   last_connection_change_ = tick_clock_->NowTicks();
361 }
362 
SetUseLocalHostRequestsForTesting(bool use_localhost_requests)363 void ThroughputAnalyzer::SetUseLocalHostRequestsForTesting(
364     bool use_localhost_requests) {
365   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
366   use_localhost_requests_for_tests_ = use_localhost_requests;
367 }
368 
GetBitsReceived() const369 int64_t ThroughputAnalyzer::GetBitsReceived() const {
370   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
371   return activity_monitor::GetBytesReceived() * 8;
372 }
373 
CountActiveInFlightRequests() const374 size_t ThroughputAnalyzer::CountActiveInFlightRequests() const {
375   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
376   return requests_.size();
377 }
378 
CountTotalInFlightRequests() const379 size_t ThroughputAnalyzer::CountTotalInFlightRequests() const {
380   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
381   return response_content_sizes_.size();
382 }
383 
CountTotalContentSizeBytes() const384 int64_t ThroughputAnalyzer::CountTotalContentSizeBytes() const {
385   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
386 
387   return total_response_content_size_;
388 }
389 
DegradesAccuracy(const URLRequest & request) const390 bool ThroughputAnalyzer::DegradesAccuracy(const URLRequest& request) const {
391   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
392 
393   bool private_network_request =
394       nqe::internal::IsRequestForPrivateHost(request, net_log_);
395 
396   return !(use_localhost_requests_for_tests_ || !private_network_request) ||
397          request.creation_time() < last_connection_change_;
398 }
399 
BoundRequestsSize()400 void ThroughputAnalyzer::BoundRequestsSize() {
401   if (accuracy_degrading_requests_.size() > kMaxRequestsSize) {
402     // Clear |accuracy_degrading_requests_| since its size has exceeded its
403     // capacity.
404     accuracy_degrading_requests_.clear();
405     // Disable throughput measurements since |this| has lost track of the
406     // accuracy degrading requests.
407     disable_throughput_measurements_ = true;
408 
409     // Reset other variables related to tracking since the tracking is now
410     // disabled.
411     EndThroughputObservationWindow();
412     DCHECK(!IsCurrentlyTrackingThroughput());
413     requests_.clear();
414 
415     // TODO(tbansal): crbug.com/609174 Add UMA to record how frequently this
416     // happens.
417   }
418 
419   if (requests_.size() > kMaxRequestsSize) {
420     // Clear |requests_| since its size has exceeded its capacity.
421     EndThroughputObservationWindow();
422     DCHECK(!IsCurrentlyTrackingThroughput());
423     requests_.clear();
424 
425     // TODO(tbansal): crbug.com/609174 Add UMA to record how frequently this
426     // happens.
427   }
428 }
429 
EraseHangingRequests(const URLRequest & request)430 void ThroughputAnalyzer::EraseHangingRequests(const URLRequest& request) {
431   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
432 
433   DCHECK_LT(0, params_->hanging_request_duration_http_rtt_multiplier());
434 
435   const base::TimeTicks now = tick_clock_->NowTicks();
436 
437   const base::TimeDelta http_rtt =
438       network_quality_estimator_->GetHttpRTT().value_or(base::Seconds(60));
439 
440   size_t count_request_erased = 0;
441   auto request_it = requests_.find(&request);
442   if (request_it != requests_.end()) {
443     base::TimeDelta time_since_last_received = now - request_it->second;
444 
445     if (time_since_last_received >=
446             params_->hanging_request_duration_http_rtt_multiplier() *
447                 http_rtt &&
448         time_since_last_received >= params_->hanging_request_min_duration()) {
449       count_request_erased++;
450       requests_.erase(request_it);
451     }
452   }
453 
454   if (now - last_hanging_request_check_ >= base::Seconds(1)) {
455     // Hanging request check is done at most once per second.
456     last_hanging_request_check_ = now;
457 
458     for (auto it = requests_.begin(); it != requests_.end();) {
459       base::TimeDelta time_since_last_received = now - it->second;
460 
461       if (time_since_last_received >=
462               params_->hanging_request_duration_http_rtt_multiplier() *
463                   http_rtt &&
464           time_since_last_received >= params_->hanging_request_min_duration()) {
465         count_request_erased++;
466         requests_.erase(it++);
467       } else {
468         ++it;
469       }
470     }
471   }
472 
473   if (count_request_erased > 0) {
474     // End the observation window since there is at least one hanging GET in
475     // flight, which may lead to inaccuracies in the throughput estimate
476     // computation.
477     EndThroughputObservationWindow();
478   }
479 }
480 
481 }  // namespace nqe::internal
482 
483 }  // namespace net
484