1 // Copyright 2017 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 package org.chromium.net; 6 7 import static com.google.common.truth.Truth.assertThat; 8 9 import static org.junit.Assert.assertThrows; 10 11 import static org.chromium.net.CronetTestRule.getTestStorage; 12 13 import android.os.StrictMode; 14 15 import androidx.test.ext.junit.runners.AndroidJUnit4; 16 import androidx.test.filters.SmallTest; 17 18 import org.json.JSONObject; 19 import org.junit.After; 20 import org.junit.Before; 21 import org.junit.Rule; 22 import org.junit.Test; 23 import org.junit.runner.RunWith; 24 25 import org.chromium.base.Log; 26 import org.chromium.base.metrics.UmaRecorderHolder; 27 import org.chromium.base.test.util.DoNotBatch; 28 import org.chromium.base.test.util.HistogramWatcher; 29 import org.chromium.net.CronetTestRule.CronetImplementation; 30 import org.chromium.net.CronetTestRule.IgnoreFor; 31 import org.chromium.net.MetricsTestUtil.TestExecutor; 32 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.FileNotFoundException; 36 import java.io.IOException; 37 import java.util.concurrent.Executor; 38 import java.util.concurrent.Executors; 39 import java.util.concurrent.ThreadFactory; 40 41 /** Test Network Quality Estimator. */ 42 @DoNotBatch(reason = "crbug/1459563") 43 @RunWith(AndroidJUnit4.class) 44 @IgnoreFor( 45 implementations = {CronetImplementation.FALLBACK, CronetImplementation.AOSP_PLATFORM}, 46 reason = "Fallback and AOSP implementations do not support network quality estimating") 47 public class NQETest { 48 private static final String TAG = NQETest.class.getSimpleName(); 49 50 @Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup(); 51 52 private String mUrl; 53 54 // Thread on which network quality listeners should be notified. 55 private Thread mNetworkQualityThread; 56 57 @Before setUp()58 public void setUp() throws Exception { 59 NativeTestServer.startNativeTestServer(mTestRule.getTestFramework().getContext()); 60 mUrl = NativeTestServer.getFileURL("/echo?status=200"); 61 } 62 63 @After tearDown()64 public void tearDown() throws Exception { 65 NativeTestServer.shutdownNativeTestServer(); 66 } 67 68 private class ExecutorThreadFactory implements ThreadFactory { 69 @Override newThread(final Runnable r)70 public Thread newThread(final Runnable r) { 71 mNetworkQualityThread = 72 new Thread( 73 new Runnable() { 74 @Override 75 public void run() { 76 StrictMode.ThreadPolicy threadPolicy = 77 StrictMode.getThreadPolicy(); 78 try { 79 StrictMode.setThreadPolicy( 80 new StrictMode.ThreadPolicy.Builder() 81 .detectNetwork() 82 .penaltyLog() 83 .penaltyDeath() 84 .build()); 85 r.run(); 86 } finally { 87 StrictMode.setThreadPolicy(threadPolicy); 88 } 89 } 90 }); 91 return mNetworkQualityThread; 92 } 93 } 94 95 @Test 96 @SmallTest testNotEnabled()97 public void testNotEnabled() throws Exception { 98 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().startEngine(); 99 Executor networkQualityExecutor = Executors.newSingleThreadExecutor(); 100 TestNetworkQualityRttListener rttListener = 101 new TestNetworkQualityRttListener(networkQualityExecutor); 102 TestNetworkQualityThroughputListener throughputListener = 103 new TestNetworkQualityThroughputListener(networkQualityExecutor); 104 assertThrows(IllegalStateException.class, () -> cronetEngine.addRttListener(rttListener)); 105 assertThrows( 106 IllegalStateException.class, 107 () -> cronetEngine.addThroughputListener(throughputListener)); 108 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 109 UrlRequest.Builder builder = 110 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 111 UrlRequest urlRequest = builder.build(); 112 113 urlRequest.start(); 114 callback.blockForDone(); 115 assertThat(rttListener.rttObservationCount()).isEqualTo(0); 116 assertThat(throughputListener.throughputObservationCount()).isEqualTo(0); 117 } 118 119 @Test 120 @SmallTest testListenerRemoved()121 public void testListenerRemoved() throws Exception { 122 mTestRule 123 .getTestFramework() 124 .applyEngineBuilderPatch((builder) -> builder.enableNetworkQualityEstimator(true)); 125 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().startEngine(); 126 127 TestExecutor networkQualityExecutor = new TestExecutor(); 128 TestNetworkQualityRttListener rttListener = 129 new TestNetworkQualityRttListener(networkQualityExecutor); 130 131 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, false); 132 133 cronetEngine.addRttListener(rttListener); 134 cronetEngine.removeRttListener(rttListener); 135 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 136 UrlRequest.Builder builder = 137 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 138 UrlRequest urlRequest = builder.build(); 139 urlRequest.start(); 140 callback.blockForDone(); 141 networkQualityExecutor.runAllTasks(); 142 assertThat(rttListener.rttObservationCount()).isEqualTo(0); 143 } 144 145 // Returns whether a file contains a particular string. prefsFileContainsString(String content)146 private boolean prefsFileContainsString(String content) throws IOException { 147 File file = 148 new File( 149 getTestStorage(mTestRule.getTestFramework().getContext()) 150 + "/prefs/local_prefs.json"); 151 FileInputStream fileInputStream = new FileInputStream(file); 152 byte[] data = new byte[(int) file.length()]; 153 fileInputStream.read(data); 154 fileInputStream.close(); 155 return new String(data, "UTF-8").contains(content); 156 } 157 158 @Test 159 @SmallTest testQuicDisabled()160 public void testQuicDisabled() throws Exception { 161 // Set up HistogramWatcher before starting CronetEngine. This is because the 162 // HistogramWatcher takes a snapshot of the starting sample count and uses the delta of this 163 // and the count at assertExpected() call time to confirm that new samples are logged. 164 UmaRecorderHolder.onLibraryLoaded(); // Hackish workaround to crbug.com/1338919 165 var writeCountHistogram = 166 HistogramWatcher.newBuilder() 167 .expectIntRecord("NQE.Prefs.WriteCount", 1) 168 .allowExtraRecordsForHistogramsAbove() 169 .build(); 170 var readCountHistogram = 171 HistogramWatcher.newBuilder() 172 .expectIntRecord("NQE.Prefs.ReadCount", 1) 173 .allowExtraRecordsForHistogramsAbove() 174 .build(); 175 assertThat(RttThroughputValues.INVALID_RTT_THROUGHPUT).isLessThan(0); 176 Executor listenersExecutor = Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 177 TestNetworkQualityRttListener rttListener = 178 new TestNetworkQualityRttListener(listenersExecutor); 179 TestNetworkQualityThroughputListener throughputListener = 180 new TestNetworkQualityThroughputListener(listenersExecutor); 181 mTestRule 182 .getTestFramework() 183 .applyEngineBuilderPatch( 184 (builder) -> { 185 builder.enableNetworkQualityEstimator(true) 186 .enableHttp2(true) 187 .enableQuic(false); 188 189 // The pref may not be written if the computed Effective Connection Type 190 // (ECT) matches the default ECT for the current connection type. 191 // Force the ECT to "Slow-2G". Since "Slow-2G" is not the default ECT 192 // for any connection type, this ensures that the pref is written to. 193 JSONObject nqeOptions = 194 new JSONObject() 195 .put("force_effective_connection_type", "Slow-2G"); 196 JSONObject experimentalOptions = 197 new JSONObject().put("NetworkQualityEstimator", nqeOptions); 198 199 builder.setExperimentalOptions(experimentalOptions.toString()); 200 builder.setStoragePath( 201 getTestStorage(mTestRule.getTestFramework().getContext())); 202 }); 203 204 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().startEngine(); 205 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); 206 207 cronetEngine.addRttListener(rttListener); 208 cronetEngine.addThroughputListener(throughputListener); 209 210 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 211 UrlRequest.Builder builder = 212 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 213 UrlRequest urlRequest = builder.build(); 214 urlRequest.start(); 215 callback.blockForDone(); 216 217 // Throughput observation is posted to the network quality estimator on the network thread 218 // after the UrlRequest is completed. The observations are then eventually posted to 219 // throughput listeners on the executor provided to network quality. 220 throughputListener.waitUntilFirstThroughputObservationReceived(); 221 222 // Wait for RTT observation (at the URL request layer) to be posted. 223 rttListener.waitUntilFirstUrlRequestRTTReceived(); 224 225 assertThat(throughputListener.throughputObservationCount()).isGreaterThan(0); 226 227 // Prefs must be read at startup. 228 readCountHistogram.assertExpected(); 229 230 // Check RTT observation count after throughput observation has been received. This ensures 231 // that executor has finished posting the RTT observation to the RTT listeners. 232 assertThat(rttListener.rttObservationCount()).isGreaterThan(0); 233 234 // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST 235 assertThat(rttListener.rttObservationCount(0)).isGreaterThan(0); 236 237 // NETWORK_QUALITY_OBSERVATION_SOURCE_TCP 238 assertThat(rttListener.rttObservationCount(1)).isGreaterThan(0); 239 240 // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC 241 assertThat(rttListener.rttObservationCount(2)).isEqualTo(0); 242 243 // Verify that the listeners were notified on the expected thread. 244 assertThat(rttListener.getThread()).isEqualTo(mNetworkQualityThread); 245 assertThat(throughputListener.getThread()).isEqualTo(mNetworkQualityThread); 246 247 // Verify that effective connection type callback is received and 248 // effective connection type is correctly set. 249 assertThat(cronetEngine.getEffectiveConnectionType()) 250 .isNotEqualTo(EffectiveConnectionType.TYPE_UNKNOWN); 251 252 // Verify that the HTTP RTT, transport RTT and downstream throughput 253 // estimates are available. 254 assertThat(cronetEngine.getHttpRttMs()).isAtLeast(0); 255 assertThat(cronetEngine.getTransportRttMs()).isAtLeast(0); 256 assertThat(cronetEngine.getDownstreamThroughputKbps()).isAtLeast(0); 257 258 // Verify that the cached estimates were written to the prefs. 259 while (true) { 260 Log.i(TAG, "Still waiting for pref file update....."); 261 Thread.sleep(12000); 262 try { 263 if (prefsFileContainsString("network_qualities")) { 264 break; 265 } 266 } catch (FileNotFoundException e) { 267 // Ignored this exception since the file will only be created when updates are 268 // flushed to the disk. 269 } 270 } 271 assertThat(prefsFileContainsString("network_qualities")).isTrue(); 272 273 cronetEngine.shutdown(); 274 writeCountHistogram.assertExpected(); 275 } 276 277 @Test 278 @SmallTest testPrefsWriteRead()279 public void testPrefsWriteRead() throws Exception { 280 // When the loop is run for the first time, network quality is written to the disk. The 281 // test verifies that in the next loop, the network quality is read back. 282 283 UmaRecorderHolder.onLibraryLoaded(); // Hackish workaround to crbug.com/1338919 284 for (int i = 0; i <= 1; ++i) { 285 // Set up HistogramWatcher before starting CronetEngine. This is because the 286 // HistogramWatcher takes a snapshot of the starting sample count and uses the delta of 287 // this and the count at assertExpected() call time to confirm that new samples are 288 // logged. 289 HistogramWatcher readCountHistogram = 290 HistogramWatcher.newBuilder() 291 .expectIntRecord("NQE.Prefs.ReadCount", 1) 292 .allowExtraRecordsForHistogramsAbove() 293 .build(); 294 295 // Stored network quality in the pref should be read in the second iteration. 296 HistogramWatcher readPrefsSizeHistogram; 297 if (i == 0) { 298 readPrefsSizeHistogram = 299 HistogramWatcher.newBuilder() 300 .expectIntRecord("NQE.Prefs.ReadSize", 0) 301 .build(); 302 } else { 303 readPrefsSizeHistogram = 304 HistogramWatcher.newBuilder() 305 .expectIntRecord("NQE.Prefs.ReadSize", 1) 306 .allowExtraRecordsForHistogramsAbove() 307 .build(); 308 } 309 310 // NETWORK_QUALITY_OBSERVATION_SOURCE_HTTP_CACHED_ESTIMATE: 3 311 HistogramWatcher cachedRttHistogram = 312 HistogramWatcher.newBuilder() 313 .expectIntRecord("NQE.RTT.ObservationSource", 3) 314 .allowExtraRecordsForHistogramsAbove() 315 .build(); 316 317 ExperimentalCronetEngine.Builder cronetEngineBuilder = 318 new ExperimentalCronetEngine.Builder(mTestRule.getTestFramework().getContext()); 319 assertThat(RttThroughputValues.INVALID_RTT_THROUGHPUT).isLessThan(0); 320 Executor listenersExecutor = 321 Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 322 TestNetworkQualityRttListener rttListener = 323 new TestNetworkQualityRttListener(listenersExecutor); 324 cronetEngineBuilder 325 .enableNetworkQualityEstimator(true) 326 .enableHttp2(true) 327 .enableQuic(false); 328 329 // The pref may not be written if the computed Effective Connection Type (ECT) matches 330 // the default ECT for the current connection type. Force the ECT to "Slow-2G". Since 331 // "Slow-2G" is not the default ECT for any connection type, this ensures that the pref 332 // is written to. 333 JSONObject nqeOptions = 334 new JSONObject().put("force_effective_connection_type", "Slow-2G"); 335 JSONObject experimentalOptions = 336 new JSONObject().put("NetworkQualityEstimator", nqeOptions); 337 338 cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); 339 340 cronetEngineBuilder.setStoragePath( 341 getTestStorage(mTestRule.getTestFramework().getContext())); 342 343 final ExperimentalCronetEngine cronetEngine = cronetEngineBuilder.build(); 344 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); 345 cronetEngine.addRttListener(rttListener); 346 347 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 348 UrlRequest.Builder builder = 349 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 350 UrlRequest urlRequest = builder.build(); 351 urlRequest.start(); 352 callback.blockForDone(); 353 354 // Wait for RTT observation (at the URL request layer) to be posted. 355 rttListener.waitUntilFirstUrlRequestRTTReceived(); 356 357 // Prefs must be read at startup. 358 readCountHistogram.assertExpected(); 359 360 // Check RTT observation count after throughput observation has been received. This 361 // ensures that executor has finished posting the RTT observation to the RTT 362 // listeners. 363 assertThat(rttListener.rttObservationCount()).isGreaterThan(0); 364 365 // Verify that effective connection type callback is received and 366 // effective connection type is correctly set. 367 assertThat(cronetEngine.getEffectiveConnectionType()) 368 .isNotEqualTo(EffectiveConnectionType.TYPE_UNKNOWN); 369 370 cronetEngine.shutdown(); 371 372 if (i == 0) { 373 // Verify that the cached estimates were written to the prefs. 374 assertThat(prefsFileContainsString("network_qualities")).isTrue(); 375 } 376 377 readPrefsSizeHistogram.assertExpected(); 378 if (i > 0) { 379 cachedRttHistogram.assertExpected(); 380 } 381 } 382 } 383 384 @Test 385 @SmallTest testQuicDisabledWithParams()386 public void testQuicDisabledWithParams() throws Exception { 387 Executor listenersExecutor = Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 388 TestNetworkQualityRttListener rttListener = 389 new TestNetworkQualityRttListener(listenersExecutor); 390 TestNetworkQualityThroughputListener throughputListener = 391 new TestNetworkQualityThroughputListener(listenersExecutor); 392 393 mTestRule 394 .getTestFramework() 395 .applyEngineBuilderPatch( 396 (builder) -> { 397 // Force the effective connection type to "2G". 398 JSONObject nqeOptions = 399 new JSONObject() 400 .put("force_effective_connection_type", "Slow-2G"); 401 // Add one more extra param two times to ensure robustness. 402 nqeOptions.put("some_other_param_1", "value1"); 403 nqeOptions.put("some_other_param_2", "value2"); 404 JSONObject experimentalOptions = 405 new JSONObject().put("NetworkQualityEstimator", nqeOptions); 406 experimentalOptions.put("SomeOtherFieldTrialName", new JSONObject()); 407 408 builder.enableNetworkQualityEstimator(true) 409 .enableHttp2(true) 410 .enableQuic(false); 411 builder.setExperimentalOptions(experimentalOptions.toString()); 412 }); 413 414 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().startEngine(); 415 416 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, false); 417 418 cronetEngine.addRttListener(rttListener); 419 cronetEngine.addThroughputListener(throughputListener); 420 421 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 422 UrlRequest.Builder builder = 423 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 424 UrlRequest urlRequest = builder.build(); 425 urlRequest.start(); 426 callback.blockForDone(); 427 428 // Throughput observation is posted to the network quality estimator on the network thread 429 // after the UrlRequest is completed. The observations are then eventually posted to 430 // throughput listeners on the executor provided to network quality. 431 throughputListener.waitUntilFirstThroughputObservationReceived(); 432 433 // Wait for RTT observation (at the URL request layer) to be posted. 434 rttListener.waitUntilFirstUrlRequestRTTReceived(); 435 436 assertThat(throughputListener.throughputObservationCount()).isGreaterThan(0); 437 438 // Check RTT observation count after throughput observation has been received. This ensures 439 // that executor has finished posting the RTT observation to the RTT listeners. 440 assertThat(rttListener.rttObservationCount()).isGreaterThan(0); 441 442 // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST 443 assertThat(rttListener.rttObservationCount(0)).isGreaterThan(0); 444 445 // NETWORK_QUALITY_OBSERVATION_SOURCE_TCP 446 assertThat(rttListener.rttObservationCount(1)).isGreaterThan(0); 447 448 // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC 449 assertThat(rttListener.rttObservationCount(2)).isEqualTo(0); 450 451 // Verify that the listeners were notified on the expected thread. 452 assertThat(rttListener.getThread()).isEqualTo(mNetworkQualityThread); 453 assertThat(throughputListener.getThread()).isEqualTo(mNetworkQualityThread); 454 455 // Verify that effective connection type callback is received and effective connection type 456 // is correctly set to the forced value. This also verifies that the configuration params 457 // from Cronet embedders were correctly read by NetworkQualityEstimator. 458 assertThat(cronetEngine.getEffectiveConnectionType()) 459 .isEqualTo(EffectiveConnectionType.TYPE_SLOW_2G); 460 } 461 } 462