// Copyright 2016 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 "net/nqe/throughput_analyzer.h" #include #include #include #include #include #include #include "base/bind.h" #include "base/bind_helpers.h" #include "base/containers/circular_deque.h" #include "base/location.h" #include "base/macros.h" #include "base/run_loop.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_number_conversions.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/simple_test_tick_clock.h" #include "base/test/test_timeouts.h" #include "base/threading/platform_thread.h" #include "base/threading/thread_task_runner_handle.h" #include "base/time/default_tick_clock.h" #include "net/base/features.h" #include "net/base/isolation_info.h" #include "net/dns/mock_host_resolver.h" #include "net/log/test_net_log.h" #include "net/nqe/network_quality_estimator.h" #include "net/nqe/network_quality_estimator_params.h" #include "net/nqe/network_quality_estimator_test_util.h" #include "net/nqe/network_quality_estimator_util.h" #include "net/test/test_with_task_environment.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "net/url_request/url_request.h" #include "net/url_request/url_request_test_util.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" #include "url/origin.h" namespace net { namespace nqe { namespace { class TestThroughputAnalyzer : public internal::ThroughputAnalyzer { public: TestThroughputAnalyzer(NetworkQualityEstimator* network_quality_estimator, NetworkQualityEstimatorParams* params, const base::TickClock* tick_clock) : internal::ThroughputAnalyzer( network_quality_estimator, params, base::ThreadTaskRunnerHandle::Get(), base::BindRepeating( &TestThroughputAnalyzer::OnNewThroughputObservationAvailable, base::Unretained(this)), tick_clock, std::make_unique()->bound()), throughput_observations_received_(0), bits_received_(0) {} ~TestThroughputAnalyzer() override = default; int32_t throughput_observations_received() const { return throughput_observations_received_; } void OnNewThroughputObservationAvailable(int32_t downstream_kbps) { throughput_observations_received_++; } int64_t GetBitsReceived() const override { return bits_received_; } void IncrementBitsReceived(int64_t additional_bits_received) { bits_received_ += additional_bits_received; } // Uses a mock resolver to force example.com to resolve to a public IP // address. void AddIPAddressResolution(TestURLRequestContext* context) { scoped_refptr rules = base::MakeRefCounted(nullptr); // example.com resolves to a public IP address. rules->AddRule("example.com", "27.0.0.3"); // local.com resolves to a private IP address. rules->AddRule("local.com", "127.0.0.1"); mock_host_resolver_.set_rules(rules.get()); mock_host_resolver_.LoadIntoCache(HostPortPair("example.com", 80), NetworkIsolationKey(), base::nullopt); mock_host_resolver_.LoadIntoCache(HostPortPair("local.com", 80), NetworkIsolationKey(), base::nullopt); context->set_host_resolver(&mock_host_resolver_); } using internal::ThroughputAnalyzer::CountActiveInFlightRequests; using internal::ThroughputAnalyzer:: disable_throughput_measurements_for_testing; using internal::ThroughputAnalyzer::EraseHangingRequests; using internal::ThroughputAnalyzer::IsHangingWindow; private: int throughput_observations_received_; int64_t bits_received_; MockCachingHostResolver mock_host_resolver_; DISALLOW_COPY_AND_ASSIGN(TestThroughputAnalyzer); }; using ThroughputAnalyzerTest = TestWithTaskEnvironment; TEST_F(ThroughputAnalyzerTest, MaximumRequests) { const struct TestCase { GURL url; bool is_local; } kTestCases[] = { {GURL("http://127.0.0.1/test.html"), true /* is_local */}, {GURL("http://example.com/test.html"), false /* is_local */}, {GURL("http://local.com/test.html"), true /* is_local */}, }; for (const auto& test_case : kTestCases) { const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; std::map variation_params; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestDelegate test_delegate; TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); ASSERT_FALSE( throughput_analyzer.disable_throughput_measurements_for_testing()); base::circular_deque> requests; // Start more requests than the maximum number of requests that can be held // in the memory. EXPECT_EQ(test_case.is_local, nqe::internal::IsPrivateHostForTesting( context.host_resolver(), HostPortPair::FromURL(test_case.url), NetworkIsolationKey())); for (size_t i = 0; i < 1000; ++i) { std::unique_ptr request( context.CreateRequest(test_case.url, DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); throughput_analyzer.NotifyStartTransaction(*(request.get())); requests.push_back(std::move(request)); } // Too many local requests should cause the |throughput_analyzer| to disable // throughput measurements. EXPECT_NE(test_case.is_local, throughput_analyzer.IsCurrentlyTrackingThroughput()); } } // Make sure that the NetworkIsolationKey is respected when resolving a host // from the cache. TEST_F(ThroughputAnalyzerTest, MaximumRequestsWithNetworkIsolationKey) { const url::Origin kOrigin = url::Origin::Create(GURL("https://foo.test/")); const net::NetworkIsolationKey kNetworkIsolationKey(kOrigin, kOrigin); const GURL kUrl = GURL("http://foo.test/test.html"); base::test::ScopedFeatureList feature_list; feature_list.InitAndEnableFeature( features::kSplitHostCacheByNetworkIsolationKey); for (bool use_network_isolation_key : {false, true}) { const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; std::map variation_params; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestDelegate test_delegate; TestURLRequestContext context; MockCachingHostResolver mock_host_resolver; context.set_host_resolver(&mock_host_resolver); // Add an entry to the host cache mapping kUrl to non-local IP when using an // empty NetworkIsolationKey. scoped_refptr rules = base::MakeRefCounted(nullptr); rules->AddRule(kUrl.host(), "1.2.3.4"); mock_host_resolver.set_rules(rules.get()); mock_host_resolver.LoadIntoCache(HostPortPair::FromURL(kUrl), NetworkIsolationKey(), base::nullopt); // Add an entry to the host cache mapping kUrl to local IP when using // kNetworkIsolationKey. rules = base::MakeRefCounted(nullptr); rules->AddRule(kUrl.host(), "127.0.0.1"); mock_host_resolver.set_rules(rules.get()); mock_host_resolver.LoadIntoCache(HostPortPair::FromURL(kUrl), kNetworkIsolationKey, base::nullopt); ASSERT_FALSE( throughput_analyzer.disable_throughput_measurements_for_testing()); base::circular_deque> requests; // Start more requests than the maximum number of requests that can be held // in the memory. EXPECT_EQ(use_network_isolation_key, nqe::internal::IsPrivateHostForTesting( context.host_resolver(), HostPortPair::FromURL(kUrl), use_network_isolation_key ? kNetworkIsolationKey : NetworkIsolationKey())); for (size_t i = 0; i < 1000; ++i) { std::unique_ptr request( context.CreateRequest(kUrl, DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); if (use_network_isolation_key) request->set_isolation_info(IsolationInfo::CreatePartial( IsolationInfo::RedirectMode::kUpdateNothing, kNetworkIsolationKey)); throughput_analyzer.NotifyStartTransaction(*(request.get())); requests.push_back(std::move(request)); } // Too many local requests should cause the |throughput_analyzer| to disable // throughput measurements. EXPECT_NE(use_network_isolation_key, throughput_analyzer.IsCurrentlyTrackingThroughput()); } } // Tests that the throughput observation is taken only if there are sufficient // number of requests in-flight. TEST_F(ThroughputAnalyzerTest, TestMinRequestsForThroughputSample) { const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; std::map variation_params; variation_params["throughput_hanging_requests_cwnd_size_multiplier"] = "-1"; NetworkQualityEstimatorParams params(variation_params); // Set HTTP RTT to a large value so that the throughput observation window // is not detected as hanging. In practice, this would be provided by // |network_quality_estimator| based on the recent observations. network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromSeconds(100)); for (size_t num_requests = 1; num_requests <= params.throughput_min_requests_in_flight() + 1; ++num_requests) { TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); std::vector> requests_not_local; std::vector not_local_test_delegates(num_requests); for (size_t i = 0; i < num_requests; ++i) { // We don't care about completion, except for the first one (see below). not_local_test_delegates[i].set_on_complete(base::DoNothing()); std::unique_ptr request_not_local(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, ¬_local_test_delegates[i], TRAFFIC_ANNOTATION_FOR_TESTS)); request_not_local->Start(); requests_not_local.push_back(std::move(request_not_local)); } not_local_test_delegates[0].RunUntilComplete(); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); for (size_t i = 0; i < requests_not_local.size(); ++i) { throughput_analyzer.NotifyStartTransaction(*requests_not_local.at(i)); } // Increment the bytes received count to emulate the bytes received for // |request_local| and |requests_not_local|. throughput_analyzer.IncrementBitsReceived(100 * 1000 * 8); for (size_t i = 0; i < requests_not_local.size(); ++i) { throughput_analyzer.NotifyRequestCompleted(*requests_not_local.at(i)); } base::RunLoop().RunUntilIdle(); int expected_throughput_observations = num_requests >= params.throughput_min_requests_in_flight() ? 1 : 0; EXPECT_EQ(expected_throughput_observations, throughput_analyzer.throughput_observations_received()); } } // Tests that the hanging requests are dropped from the |requests_|, and // throughput observation window is ended. TEST_F(ThroughputAnalyzerTest, TestHangingRequests) { static const struct { int hanging_request_duration_http_rtt_multiplier; base::TimeDelta http_rtt; base::TimeDelta requests_hang_duration; bool expect_throughput_observation; } tests[] = { { // |requests_hang_duration| is less than 5 times the HTTP RTT. // Requests should not be marked as hanging. 5, base::TimeDelta::FromMilliseconds(1000), base::TimeDelta::FromMilliseconds(3000), true, }, { // |requests_hang_duration| is more than 5 times the HTTP RTT. // Requests should be marked as hanging. 5, base::TimeDelta::FromMilliseconds(200), base::TimeDelta::FromMilliseconds(3000), false, }, { // |requests_hang_duration| is less than // |hanging_request_min_duration_msec|. Requests should not be marked // as hanging. 1, base::TimeDelta::FromMilliseconds(100), base::TimeDelta::FromMilliseconds(100), true, }, { // |requests_hang_duration| is more than // |hanging_request_min_duration_msec|. Requests should be marked as // hanging. 1, base::TimeDelta::FromMilliseconds(2000), base::TimeDelta::FromMilliseconds(3100), false, }, { // |requests_hang_duration| is less than 5 times the HTTP RTT. // Requests should not be marked as hanging. 5, base::TimeDelta::FromSeconds(2), base::TimeDelta::FromSeconds(1), true, }, { // HTTP RTT is unavailable. Requests should not be marked as hanging. 5, base::TimeDelta::FromSeconds(-1), base::TimeDelta::FromSeconds(-1), true, }, }; for (const auto& test : tests) { base::HistogramTester histogram_tester; const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; if (test.http_rtt >= base::TimeDelta()) network_quality_estimator.SetStartTimeNullHttpRtt(test.http_rtt); std::map variation_params; // Set the transport RTT multiplier to a large value so that the hanging // request decision is made only on the basis of the HTTP RTT. variation_params ["hanging_request_http_rtt_upper_bound_transport_rtt_multiplier"] = "10000"; variation_params["throughput_hanging_requests_cwnd_size_multiplier"] = "-1"; variation_params["hanging_request_duration_http_rtt_multiplier"] = base::NumberToString(test.hanging_request_duration_http_rtt_multiplier); NetworkQualityEstimatorParams params(variation_params); const size_t num_requests = params.throughput_min_requests_in_flight(); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); std::vector> requests_not_local; std::vector not_local_test_delegates(num_requests); for (size_t i = 0; i < num_requests; ++i) { // We don't care about completion, except for the first one (see below). not_local_test_delegates[i].set_on_complete(base::DoNothing()); std::unique_ptr request_not_local(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, ¬_local_test_delegates[i], TRAFFIC_ANNOTATION_FOR_TESTS)); request_not_local->Start(); requests_not_local.push_back(std::move(request_not_local)); } not_local_test_delegates[0].RunUntilComplete(); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); for (size_t i = 0; i < num_requests; ++i) { throughput_analyzer.NotifyStartTransaction(*requests_not_local.at(i)); } // Increment the bytes received count to emulate the bytes received for // |request_local| and |requests_not_local|. throughput_analyzer.IncrementBitsReceived(100 * 1000 * 8); // Mark in-flight requests as hanging requests (if specified in the test // params). if (test.requests_hang_duration >= base::TimeDelta()) base::PlatformThread::Sleep(test.requests_hang_duration); EXPECT_EQ(num_requests, throughput_analyzer.CountActiveInFlightRequests()); for (size_t i = 0; i < num_requests; ++i) { throughput_analyzer.NotifyRequestCompleted(*requests_not_local.at(i)); if (!test.expect_throughput_observation) { // All in-flight requests should be marked as hanging, and thus should // be deleted from the set of in-flight requests. EXPECT_EQ(0u, throughput_analyzer.CountActiveInFlightRequests()); } else { // One request should be deleted at one time. EXPECT_EQ(requests_not_local.size() - i - 1, throughput_analyzer.CountActiveInFlightRequests()); } } base::RunLoop().RunUntilIdle(); EXPECT_EQ(test.expect_throughput_observation, throughput_analyzer.throughput_observations_received() > 0); } } // Tests that the check for hanging requests is done at most once per second. TEST_F(ThroughputAnalyzerTest, TestHangingRequestsCheckedOnlyPeriodically) { base::SimpleTestTickClock tick_clock; TestNetworkQualityEstimator network_quality_estimator; network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromSeconds(1)); std::map variation_params; variation_params["hanging_request_duration_http_rtt_multiplier"] = "5"; variation_params["hanging_request_min_duration_msec"] = "2000"; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, &tick_clock); TestDelegate test_delegate; TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); std::vector> requests_not_local; for (size_t i = 0; i < 2; ++i) { std::unique_ptr request_not_local(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); request_not_local->Start(); requests_not_local.push_back(std::move(request_not_local)); } std::unique_ptr some_other_request(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); test_delegate.RunUntilComplete(); // First request starts at t=1. The second request starts at t=2. The first // request would be marked as hanging at t=6, and the second request at t=7 // seconds. for (size_t i = 0; i < 2; ++i) { tick_clock.Advance(base::TimeDelta::FromMilliseconds(1000)); throughput_analyzer.NotifyStartTransaction(*requests_not_local.at(i)); } EXPECT_EQ(2u, throughput_analyzer.CountActiveInFlightRequests()); tick_clock.Advance(base::TimeDelta::FromMilliseconds(3500)); // Current time is t = 5.5 seconds. throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(2u, throughput_analyzer.CountActiveInFlightRequests()); tick_clock.Advance(base::TimeDelta::FromMilliseconds(1000)); // Current time is t = 6.5 seconds. One request should be marked as hanging. throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); // Current time is t = 6.5 seconds. Calling NotifyBytesRead again should not // run the hanging request checker since the last check was at t=6.5 seconds. throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); tick_clock.Advance(base::TimeDelta::FromMilliseconds(600)); // Current time is t = 7.1 seconds. Calling NotifyBytesRead again should not // run the hanging request checker since the last check was at t=6.5 seconds // (less than 1 second ago). throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); tick_clock.Advance(base::TimeDelta::FromMilliseconds(400)); // Current time is t = 7.5 seconds. Calling NotifyBytesRead again should run // the hanging request checker since the last check was at t=6.5 seconds (at // least 1 second ago). throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(0u, throughput_analyzer.CountActiveInFlightRequests()); } // Tests that the last received time for a request is updated when data is // received for that request. TEST_F(ThroughputAnalyzerTest, TestLastReceivedTimeIsUpdated) { base::SimpleTestTickClock tick_clock; TestNetworkQualityEstimator network_quality_estimator; network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromSeconds(1)); std::map variation_params; variation_params["hanging_request_duration_http_rtt_multiplier"] = "5"; variation_params["hanging_request_min_duration_msec"] = "2000"; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, &tick_clock); TestDelegate test_delegate; TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); std::unique_ptr request_not_local(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); request_not_local->Start(); test_delegate.RunUntilComplete(); std::unique_ptr some_other_request(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); // Start time for the request is t=0 second. The request will be marked as // hanging at t=5 seconds. throughput_analyzer.NotifyStartTransaction(*request_not_local); tick_clock.Advance(base::TimeDelta::FromMilliseconds(4000)); // Current time is t=4.0 seconds. throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); // The request will be marked as hanging at t=9 seconds. throughput_analyzer.NotifyBytesRead(*request_not_local); tick_clock.Advance(base::TimeDelta::FromMilliseconds(4000)); // Current time is t=8 seconds. throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); tick_clock.Advance(base::TimeDelta::FromMilliseconds(2000)); // Current time is t=10 seconds. throughput_analyzer.EraseHangingRequests(*some_other_request); EXPECT_EQ(0u, throughput_analyzer.CountActiveInFlightRequests()); } // Test that a request that has been hanging for a long time is deleted // immediately when EraseHangingRequests is called even if the last hanging // request check was done recently. TEST_F(ThroughputAnalyzerTest, TestRequestDeletedImmediately) { base::SimpleTestTickClock tick_clock; TestNetworkQualityEstimator network_quality_estimator; network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromSeconds(1)); std::map variation_params; variation_params["hanging_request_duration_http_rtt_multiplier"] = "2"; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, &tick_clock); TestDelegate test_delegate; TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); std::unique_ptr request_not_local(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS)); request_not_local->Start(); test_delegate.RunUntilComplete(); // Start time for the request is t=0 second. The request will be marked as // hanging at t=2 seconds. throughput_analyzer.NotifyStartTransaction(*request_not_local); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); tick_clock.Advance(base::TimeDelta::FromMilliseconds(2900)); // Current time is t=2.9 seconds. throughput_analyzer.EraseHangingRequests(*request_not_local); EXPECT_EQ(1u, throughput_analyzer.CountActiveInFlightRequests()); // |request_not_local| should be deleted since it has been idle for 2.4 // seconds. tick_clock.Advance(base::TimeDelta::FromMilliseconds(500)); throughput_analyzer.NotifyBytesRead(*request_not_local); EXPECT_EQ(0u, throughput_analyzer.CountActiveInFlightRequests()); } // Tests if the throughput observation is taken correctly when local and network // requests overlap. TEST_F(ThroughputAnalyzerTest, TestThroughputWithMultipleRequestsOverlap) { static const struct { bool start_local_request; bool local_request_completes_first; bool expect_throughput_observation; } tests[] = { { false, false, true, }, { true, false, false, }, { true, true, true, }, }; for (const auto& test : tests) { const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; // Localhost requests are not allowed for estimation purposes. std::map variation_params; variation_params["throughput_hanging_requests_cwnd_size_multiplier"] = "-1"; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestDelegate local_delegate; local_delegate.set_on_complete(base::DoNothing()); TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); std::unique_ptr request_local; std::vector> requests_not_local; std::vector not_local_test_delegates( params.throughput_min_requests_in_flight()); for (size_t i = 0; i < params.throughput_min_requests_in_flight(); ++i) { // We don't care about completion, except for the first one (see below). not_local_test_delegates[i].set_on_complete(base::DoNothing()); std::unique_ptr request_not_local(context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, ¬_local_test_delegates[i], TRAFFIC_ANNOTATION_FOR_TESTS)); request_not_local->Start(); requests_not_local.push_back(std::move(request_not_local)); } if (test.start_local_request) { request_local = context.CreateRequest(GURL("http://127.0.0.1/echo.html"), DEFAULT_PRIORITY, &local_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); request_local->Start(); } // Wait until the first not-local request completes. not_local_test_delegates[0].RunUntilComplete(); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); // If |test.start_local_request| is true, then |request_local| starts // before |request_not_local|, and ends after |request_not_local|. Thus, // network quality estimator should not get a chance to record throughput // observation from |request_not_local| because of ongoing local request // at all times. if (test.start_local_request) throughput_analyzer.NotifyStartTransaction(*request_local); for (size_t i = 0; i < requests_not_local.size(); ++i) { throughput_analyzer.NotifyStartTransaction(*requests_not_local.at(i)); } if (test.local_request_completes_first) { ASSERT_TRUE(test.start_local_request); throughput_analyzer.NotifyRequestCompleted(*request_local); } // Increment the bytes received count to emulate the bytes received for // |request_local| and |requests_not_local|. throughput_analyzer.IncrementBitsReceived(100 * 1000 * 8); for (size_t i = 0; i < requests_not_local.size(); ++i) { throughput_analyzer.NotifyRequestCompleted(*requests_not_local.at(i)); } if (test.start_local_request && !test.local_request_completes_first) throughput_analyzer.NotifyRequestCompleted(*request_local); // Pump the message loop to let analyzer tasks get processed. base::RunLoop().RunUntilIdle(); int expected_throughput_observations = test.expect_throughput_observation ? 1 : 0; EXPECT_EQ(expected_throughput_observations, throughput_analyzer.throughput_observations_received()); } } // Tests if the throughput observation is taken correctly when two network // requests overlap. TEST_F(ThroughputAnalyzerTest, TestThroughputWithNetworkRequestsOverlap) { static const struct { size_t throughput_min_requests_in_flight; size_t number_requests_in_flight; int64_t increment_bits; bool expect_throughput_observation; } tests[] = { { 1, 2, 100 * 1000 * 8, true, }, { 3, 1, 100 * 1000 * 8, false, }, { 3, 2, 100 * 1000 * 8, false, }, { 3, 3, 100 * 1000 * 8, true, }, { 3, 4, 100 * 1000 * 8, true, }, { 1, 2, 1, false, }, }; for (const auto& test : tests) { const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; // Localhost requests are not allowed for estimation purposes. std::map variation_params; variation_params["throughput_min_requests_in_flight"] = base::NumberToString(test.throughput_min_requests_in_flight); variation_params["throughput_hanging_requests_cwnd_size_multiplier"] = "-1"; NetworkQualityEstimatorParams params(variation_params); // Set HTTP RTT to a large value so that the throughput observation window // is not detected as hanging. In practice, this would be provided by // |network_quality_estimator| based on the recent observations. network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromSeconds(100)); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); std::vector> requests_in_flight; std::vector in_flight_test_delegates( test.number_requests_in_flight); for (size_t i = 0; i < test.number_requests_in_flight; ++i) { // We don't care about completion, except for the first one (see below). in_flight_test_delegates[i].set_on_complete(base::DoNothing()); std::unique_ptr request_network_1 = context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &in_flight_test_delegates[i], TRAFFIC_ANNOTATION_FOR_TESTS); requests_in_flight.push_back(std::move(request_network_1)); requests_in_flight.back()->Start(); } in_flight_test_delegates[0].RunUntilComplete(); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); for (size_t i = 0; i < test.number_requests_in_flight; ++i) { URLRequest* request = requests_in_flight.at(i).get(); throughput_analyzer.NotifyStartTransaction(*request); } // Increment the bytes received count to emulate the bytes received for // |request_network_1| and |request_network_2|. throughput_analyzer.IncrementBitsReceived(test.increment_bits); for (size_t i = 0; i < test.number_requests_in_flight; ++i) { URLRequest* request = requests_in_flight.at(i).get(); throughput_analyzer.NotifyRequestCompleted(*request); } base::RunLoop().RunUntilIdle(); // Only one observation should be taken since two requests overlap. if (test.expect_throughput_observation) { EXPECT_EQ(1, throughput_analyzer.throughput_observations_received()); } else { EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); } } } // Tests if the throughput observation is taken correctly when the start and end // of network requests overlap, and the minimum number of in flight requests // when taking an observation is more than 1. TEST_F(ThroughputAnalyzerTest, TestThroughputWithMultipleNetworkRequests) { const base::test::ScopedRunLoopTimeout increased_run_timeout( FROM_HERE, TestTimeouts::action_max_timeout()); const base::TickClock* tick_clock = base::DefaultTickClock::GetInstance(); TestNetworkQualityEstimator network_quality_estimator; std::map variation_params; variation_params["throughput_min_requests_in_flight"] = "3"; variation_params["throughput_hanging_requests_cwnd_size_multiplier"] = "-1"; NetworkQualityEstimatorParams params(variation_params); // Set HTTP RTT to a large value so that the throughput observation window // is not detected as hanging. In practice, this would be provided by // |network_quality_estimator| based on the recent observations. network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromSeconds(100)); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, tick_clock); TestDelegate test_delegate; TestURLRequestContext context; throughput_analyzer.AddIPAddressResolution(&context); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); std::unique_ptr request_1 = context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); std::unique_ptr request_2 = context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); std::unique_ptr request_3 = context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); std::unique_ptr request_4 = context.CreateRequest( GURL("http://example.com/echo.html"), DEFAULT_PRIORITY, &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS); request_1->Start(); request_2->Start(); request_3->Start(); request_4->Start(); // We dispatched four requests, so wait for four completions. for (int i = 0; i < 4; ++i) test_delegate.RunUntilComplete(); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); throughput_analyzer.NotifyStartTransaction(*(request_1.get())); throughput_analyzer.NotifyStartTransaction(*(request_2.get())); const size_t increment_bits = 100 * 1000 * 8; // Increment the bytes received count to emulate the bytes received for // |request_1| and |request_2|. throughput_analyzer.IncrementBitsReceived(increment_bits); throughput_analyzer.NotifyRequestCompleted(*(request_1.get())); base::RunLoop().RunUntilIdle(); // No observation should be taken since only 1 request is in flight. EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); throughput_analyzer.NotifyStartTransaction(*(request_3.get())); throughput_analyzer.NotifyStartTransaction(*(request_4.get())); EXPECT_EQ(0, throughput_analyzer.throughput_observations_received()); // 3 requests are in flight which is at least as many as the minimum number of // in flight requests required. An observation should be taken. throughput_analyzer.IncrementBitsReceived(increment_bits); // Only one observation should be taken since two requests overlap. throughput_analyzer.NotifyRequestCompleted(*(request_2.get())); base::RunLoop().RunUntilIdle(); EXPECT_EQ(1, throughput_analyzer.throughput_observations_received()); throughput_analyzer.NotifyRequestCompleted(*(request_3.get())); throughput_analyzer.NotifyRequestCompleted(*(request_4.get())); EXPECT_EQ(1, throughput_analyzer.throughput_observations_received()); } TEST_F(ThroughputAnalyzerTest, TestHangingWindow) { static constexpr size_t kCwndSizeKilobytes = 10 * 1.5; static constexpr size_t kCwndSizeBits = kCwndSizeKilobytes * 1000 * 8; base::SimpleTestTickClock tick_clock; TestNetworkQualityEstimator network_quality_estimator; int64_t http_rtt_msec = 1000; network_quality_estimator.SetStartTimeNullHttpRtt( base::TimeDelta::FromMilliseconds(http_rtt_msec)); std::map variation_params; variation_params["throughput_hanging_requests_cwnd_size_multiplier"] = "1"; NetworkQualityEstimatorParams params(variation_params); TestThroughputAnalyzer throughput_analyzer(&network_quality_estimator, ¶ms, &tick_clock); const struct { size_t bits_received; base::TimeDelta window_duration; bool expected_hanging; } tests[] = { {100, base::TimeDelta::FromMilliseconds(http_rtt_msec), true}, {kCwndSizeBits - 1, base::TimeDelta::FromMilliseconds(http_rtt_msec), true}, {kCwndSizeBits + 1, base::TimeDelta::FromMilliseconds(http_rtt_msec), false}, {2 * (kCwndSizeBits - 1), base::TimeDelta::FromMilliseconds(http_rtt_msec * 2), true}, {2 * (kCwndSizeBits + 1), base::TimeDelta::FromMilliseconds(http_rtt_msec * 2), false}, {kCwndSizeBits / 2 - 1, base::TimeDelta::FromMilliseconds(http_rtt_msec / 2), true}, {kCwndSizeBits / 2 + 1, base::TimeDelta::FromMilliseconds(http_rtt_msec / 2), false}, }; for (const auto& test : tests) { base::HistogramTester histogram_tester; double kbps = test.bits_received / test.window_duration.InMillisecondsF(); EXPECT_EQ(test.expected_hanging, throughput_analyzer.IsHangingWindow(test.bits_received, test.window_duration, kbps)); if (test.expected_hanging) { histogram_tester.ExpectUniqueSample("NQE.ThroughputObservation.Hanging", kbps, 1); histogram_tester.ExpectTotalCount("NQE.ThroughputObservation.NotHanging", 0); } else { histogram_tester.ExpectTotalCount("NQE.ThroughputObservation.Hanging", 0); histogram_tester.ExpectUniqueSample( "NQE.ThroughputObservation.NotHanging", kbps, 1); } } } } // namespace } // namespace nqe } // namespace net