• 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 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