1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.car.usb.aoap.host;
17 
18 import android.content.Context;
19 import android.hardware.usb.UsbConstants;
20 import android.hardware.usb.UsbDevice;
21 import android.hardware.usb.UsbDeviceConnection;
22 import android.hardware.usb.UsbEndpoint;
23 import android.hardware.usb.UsbInterface;
24 import android.hardware.usb.UsbRequest;
25 import android.os.SystemClock;
26 import android.text.format.Formatter;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.GuardedBy;
30 
31 import java.nio.ByteBuffer;
32 import java.nio.ByteOrder;
33 import java.util.Random;
34 
35 /** Controller that measures USB AOAP transfer speed. */
36 class SpeedMeasurementController extends Thread {
37     private static final String TAG = SpeedMeasurementController.class.getSimpleName();
38 
39     interface SpeedMeasurementControllerCallback {
testStarted(int mode, int bufferSize)40         void testStarted(int mode, int bufferSize);
testFinished(int mode, int bufferSize)41         void testFinished(int mode, int bufferSize);
testSuiteFinished()42         void testSuiteFinished();
testResult(int mode, String update)43         void testResult(int mode, String update);
44     }
45 
46     public static final int TEST_MODE_SYNC = 1;
47     public static final int TEST_MODE_ASYNC = 2;
48 
49     private static final int TEST_DATA_SIZE = 100 * 1024 * 1024; // 100MB
50     private static final int TEST_DATA_1_BATCH_SIZE = 15000;
51     private static final int TEST_DATA_2_BATCH_SIZE = 1500;
52     private static final int USB_TIMEOUT_MS = 1000; // 1s
53     private static final int TEST_MAX_TIME_MS = 200000; // 200s
54     private static final int ASYNC_MAX_OUTSTANDING_REQUESTS = 5;
55 
56     private static final ByteOrder ORDER = ByteOrder.BIG_ENDIAN;
57 
58     private final UsbDevice mDevice;
59     private final UsbDeviceConnection mUsbConnection;
60     private final SpeedMeasurementControllerCallback mCallback;
61     private final Context mContext;
62 
63     public static Random sRandom = new Random(SystemClock.uptimeMillis());
64 
SpeedMeasurementController(Context context, UsbDevice device, UsbDeviceConnection conn, SpeedMeasurementControllerCallback callback)65     SpeedMeasurementController(Context context,
66             UsbDevice device, UsbDeviceConnection conn,
67             SpeedMeasurementControllerCallback callback) {
68         if (TEST_DATA_SIZE % TEST_DATA_1_BATCH_SIZE == 0) {
69             throw new AssertionError("TEST_DATA_SIZE mod TEST_DATA_BIG_BATCH_SIZE must not be 0");
70         }
71         if (TEST_DATA_SIZE % TEST_DATA_2_BATCH_SIZE == 0) {
72             throw new AssertionError("TEST_DATA_SIZE mod TEST_DATA_SMALL_BATCH_SIZE must not be 0");
73         }
74         mContext = context;
75         mDevice = device;
76         mUsbConnection = conn;
77         mCallback = callback;
78     }
79 
release()80     protected void release() {
81         if (mUsbConnection != null) {
82             mUsbConnection.close();
83         }
84     }
85 
86     /**
87      * {@inheritDoc}
88      *
89      * Test runs two type of USB host->phone write tests:
90      * <ul>
91      * <li> Synchronous write with usage of UsbDeviceConnection.bulkTransfer. </li>
92      * <li> Aynchronous write with usage of UsbRequest and UsbDeviceConnection.requestWait. </li>
93      * </ul>
94      * Each test scenario also runs with different buffer size.
95      */
96     @Override
run()97     public void run() {
98         Log.v(TAG, "Running sync test with buffer size #1");
99         runSyncTest(TEST_DATA_1_BATCH_SIZE);
100         Log.v(TAG, "Running sync test with buffer size #2");
101         runSyncTest(TEST_DATA_2_BATCH_SIZE);
102         Log.v(TAG, "Running async test with buffer size #1");
103         runAsyncTest(TEST_DATA_1_BATCH_SIZE);
104         Log.v(TAG, "Running async test with buffer size #2");
105         runAsyncTest(TEST_DATA_2_BATCH_SIZE);
106         Log.v(TAG, "Done running tests");
107         release();
108         mCallback.testSuiteFinished();
109     }
110 
runTest(BaseWriterThread writer, ReaderThread reader, int mode, int bufferSize)111     private void runTest(BaseWriterThread writer, ReaderThread reader, int mode, int bufferSize) {
112         mCallback.testStarted(mode, bufferSize);
113         writer.start();
114         reader.start();
115         try {
116             writer.join(TEST_MAX_TIME_MS);
117         } catch (InterruptedException e) {}
118         try {
119             reader.join(TEST_MAX_TIME_MS);
120         } catch (InterruptedException e) {}
121         if (reader.isAlive()) {
122             reader.requestToQuit();
123             try {
124                 reader.join(USB_TIMEOUT_MS);
125             } catch (InterruptedException e) {}
126             if (reader.isAlive()) {
127                 throw new RuntimeException("ReaderSyncThread still alive");
128             }
129         }
130         if (writer.isAlive()) {
131             writer.requestToQuit();
132             try {
133                 writer.join(USB_TIMEOUT_MS);
134             } catch (InterruptedException e) {}
135             if (writer.isAlive()) {
136                 throw new RuntimeException("WriterSyncThread still alive");
137             }
138         }
139         mCallback.testFinished(mode, bufferSize);
140         mCallback.testResult(
141                 mode,
142                 "Buffer size: " + bufferSize + " bytes. Speed " + writer.getSpeed());
143     }
144 
runSyncTest(int bufferSize)145     private void runSyncTest(int bufferSize) {
146         ReaderThread readerSync = new ReaderThread(mDevice, mUsbConnection, TEST_MODE_SYNC);
147         WriterSyncThread writerSync = new WriterSyncThread(mDevice, mUsbConnection, bufferSize);
148         runTest(writerSync, readerSync, TEST_MODE_SYNC, bufferSize);
149     }
150 
runAsyncTest(int bufferSize)151     private void runAsyncTest(int bufferSize) {
152         ReaderThread readerAsync = new ReaderThread(mDevice, mUsbConnection, TEST_MODE_ASYNC);
153         WriterAsyncThread writerAsync = new WriterAsyncThread(mDevice, mUsbConnection, bufferSize);
154         runTest(writerAsync, readerAsync, TEST_MODE_ASYNC, bufferSize);
155     }
156 
157     private class ReaderThread extends Thread {
158 
159         private final UsbDevice mDevice;
160         private final UsbDeviceConnection mUsbConnection;
161         private final int mMode;
162         private final UsbEndpoint mBulkIn;
163         private final byte[] mBuffer = new byte[16384];
164 
165         private final Object mLock = new Object();
166 
167         @GuardedBy("mLock")
168         private boolean mShouldQuit;
169 
ReaderThread(UsbDevice device, UsbDeviceConnection conn, int testMode)170         private ReaderThread(UsbDevice device, UsbDeviceConnection conn, int testMode) {
171             super("AOAP reader");
172             mDevice = device;
173             mUsbConnection = conn;
174             mMode = testMode;
175             UsbInterface iface = mDevice.getInterface(0);
176             // Setup bulk endpoints.
177             UsbEndpoint bulkIn = null;
178             UsbEndpoint bulkOut = null;
179             for (int i = 0; i < iface.getEndpointCount(); i++) {
180                 UsbEndpoint ep = iface.getEndpoint(i);
181                 if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
182                     if (bulkIn == null) {
183                         bulkIn = ep;
184                     }
185                 } else {
186                     if (bulkOut == null) {
187                         bulkOut = ep;
188                     }
189                 }
190             }
191             if (bulkIn == null || bulkOut == null) {
192                 throw new IllegalStateException("Unable to find bulk endpoints");
193             }
194             mBulkIn = bulkIn;
195         }
196 
requestToQuit()197         public void requestToQuit() {
198             synchronized (mLock) {
199                 mShouldQuit = true;
200             }
201         }
202 
shouldQuit()203         private boolean shouldQuit() {
204             synchronized (mLock) {
205                 return mShouldQuit;
206             }
207         }
208 
209         @Override
run()210         public void run() {
211             while (!shouldQuit()) {
212                 int read = mUsbConnection.bulkTransfer(
213                         mBulkIn, mBuffer, mBuffer.length, USB_TIMEOUT_MS);
214                 if (read > 0) {
215                     Log.v(TAG, "Read " + read + " bytes");
216                     break;
217                 }
218             }
219         }
220     }
221 
222     private abstract class BaseWriterThread extends Thread {
223 
224         protected final UsbDevice mDevice;
225         protected final int mBufferSize;
226         protected final UsbDeviceConnection mUsbConnection;
227         protected final UsbEndpoint mBulkOut;
228 
229         private final Object mLock = new Object();
230 
231         @GuardedBy("mLock")
232         protected boolean mShouldQuit;
233 
234         @GuardedBy("mLock")
235         protected long mSpeed;
236 
BaseWriterThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize)237         private BaseWriterThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize) {
238             super("AOAP writer");
239             mDevice = device;
240             mUsbConnection = conn;
241             mBufferSize = bufferSize;
242             UsbInterface iface = mDevice.getInterface(0);
243             // Setup bulk endpoints.
244             UsbEndpoint bulkIn = null;
245             UsbEndpoint bulkOut = null;
246             for (int i = 0; i < iface.getEndpointCount(); i++) {
247                 UsbEndpoint ep = iface.getEndpoint(i);
248                 if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
249                     if (bulkIn == null) {
250                         bulkIn = ep;
251                     }
252                 } else {
253                     if (bulkOut == null) {
254                         bulkOut = ep;
255                     }
256                 }
257             }
258             if (bulkIn == null || bulkOut == null) {
259                 throw new IllegalStateException("Unable to find bulk endpoints");
260             }
261             mBulkOut = bulkOut;
262         }
263 
requestToQuit()264         public void requestToQuit() {
265             synchronized (mLock) {
266                 mShouldQuit = true;
267             }
268         }
269 
shouldQuit()270         protected boolean shouldQuit() {
271             synchronized (mLock) {
272                 return mShouldQuit;
273             }
274         }
275 
getSpeed()276         public String getSpeed() {
277             synchronized (mLock) {
278                 return Formatter.formatFileSize(mContext, mSpeed) + "/s";
279             }
280         }
281 
setSpeed(long speed)282         protected void setSpeed(long speed) {
283             synchronized (mLock) {
284                 // Speed is set in bytes/ms. Convert it to bytes/s.
285                 mSpeed = speed * 1000;
286             }
287         }
288 
intToByte(int value)289         protected byte[] intToByte(int value) {
290             return ByteBuffer.allocate(4).order(ORDER).putInt(value).array();
291         }
292 
293     }
294 
295     private class WriterSyncThread extends BaseWriterThread {
WriterSyncThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize)296         private WriterSyncThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize) {
297             super(device, conn, bufferSize);
298         }
299 
writeBufferSize()300         private boolean writeBufferSize() {
301             byte[] bufferSizeArray = intToByte(mBufferSize);
302             int sentBytes = mUsbConnection.bulkTransfer(
303                     mBulkOut,
304                     bufferSizeArray,
305                     bufferSizeArray.length,
306                     USB_TIMEOUT_MS);
307             if (sentBytes < 0) {
308                 Log.e(TAG, "Failed to write data");
309                 return false;
310             }
311             return true;
312         }
313 
314         @Override
run()315         public void run() {
316             int bytesToSend = TEST_DATA_SIZE;
317             if (!writeBufferSize()) {
318                 return;
319             }
320             byte[] buffer = new byte[mBufferSize];
321             sRandom.nextBytes(buffer);
322 
323             long timeStart = System.currentTimeMillis();
324             while (bytesToSend > 0 && !shouldQuit()) {
325                 int sentBytes = mUsbConnection.bulkTransfer(
326                         mBulkOut,
327                         buffer,
328                         (bytesToSend > buffer.length ? buffer.length : bytesToSend),
329                         USB_TIMEOUT_MS);
330                 if (sentBytes < 0) {
331                     Log.e(TAG, "Failed to write data/");
332                     return;
333                 } else {
334                     bytesToSend -= sentBytes;
335                 }
336             }
337             setSpeed(TEST_DATA_SIZE / (System.currentTimeMillis() - timeStart));
338         }
339     }
340 
341     private class WriterAsyncThread extends BaseWriterThread {
WriterAsyncThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize)342         private WriterAsyncThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize) {
343             super(device, conn, bufferSize);
344         }
345 
drainRequests(int numRequests)346         private boolean drainRequests(int numRequests) {
347             while (numRequests > 0) {
348                 UsbRequest req = mUsbConnection.requestWait();
349                 if (req == null) {
350                     Log.e(TAG, "Error while requestWait");
351                     return false;
352                 }
353                 req.close();
354                 numRequests--;
355             }
356             return true;
357         }
358 
writeBufferSize()359         private boolean writeBufferSize() {
360             byte[] bufferSizeArray = intToByte(mBufferSize);
361             UsbRequest sendRequest = getNewRequest();
362             if (sendRequest == null) {
363                 return false;
364             }
365 
366             ByteBuffer bufferToSend = ByteBuffer.wrap(bufferSizeArray, 0, bufferSizeArray.length);
367             boolean queued = sendRequest.queue(bufferToSend, bufferSizeArray.length);
368 
369             if (!queued) {
370                 Log.e(TAG, "Failed to queue request");
371                 return false;
372             }
373 
374             UsbRequest req = mUsbConnection.requestWait();
375             if (req == null) {
376                 Log.e(TAG, "Error while waiting for request to complete.");
377                 return false;
378             }
379             req.close();
380             return true;
381         }
382 
getNewRequest()383         private UsbRequest getNewRequest() {
384             UsbRequest request = new UsbRequest();
385             if (!request.initialize(mUsbConnection, mBulkOut)) {
386                 Log.e(TAG, "Failed to init");
387                 return null;
388             }
389             return request;
390         }
391 
392         @Override
run()393         public void run() {
394             int bytesToSend = TEST_DATA_SIZE;
395             if (!writeBufferSize()) {
396                 return;
397             }
398             int numRequests = 0;
399             byte[] buffer = new byte[mBufferSize];
400             sRandom.nextBytes(buffer);
401 
402             long timeStart = System.currentTimeMillis();
403             while (bytesToSend > 0 && !shouldQuit()) {
404                 numRequests++;
405                 UsbRequest sendRequest = getNewRequest();
406                 if (sendRequest == null) {
407                     return;
408                 }
409 
410                 int bufferSize = (bytesToSend > buffer.length ? buffer.length : bytesToSend);
411                 ByteBuffer bufferToSend = ByteBuffer.wrap(buffer, 0, bufferSize);
412                 boolean queued = sendRequest.queue(bufferToSend, bufferSize);
413                 if (queued) {
414                     bytesToSend -= buffer.length;
415                 } else {
416                     Log.e(TAG, "Failed to queue more data");
417                     return;
418                 }
419 
420                 if (numRequests == ASYNC_MAX_OUTSTANDING_REQUESTS) {
421                     UsbRequest req = mUsbConnection.requestWait();
422                     if (req == null) {
423                         Log.e(TAG, "Error while waiting for request to complete.");
424                         return;
425                     }
426                     req.close();
427                     numRequests--;
428                 }
429             }
430 
431             if (!drainRequests(numRequests)) {
432                 return;
433             }
434             setSpeed(TEST_DATA_SIZE / (System.currentTimeMillis() - timeStart));
435             Log.d(TAG, "Wrote all the data. Exiting thread");
436         }
437     }
438 }
439