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