blob: 0e0f7cebc4e70e6e41aa147525caa87c940177bd [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.car.usb.aoap.host;
import android.content.Context;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbRequest;
import android.os.SystemClock;
import android.text.format.Formatter;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Random;
/** Controller that measures USB AOAP transfer speed. */
class SpeedMeasurementController extends Thread {
private static final String TAG = SpeedMeasurementController.class.getSimpleName();
interface SpeedMeasurementControllerCallback {
void testStarted(int mode, int bufferSize);
void testFinished(int mode, int bufferSize);
void testSuiteFinished();
void testResult(int mode, String update);
}
public static final int TEST_MODE_SYNC = 1;
public static final int TEST_MODE_ASYNC = 2;
private static final int TEST_DATA_SIZE = 100 * 1024 * 1024; // 100MB
private static final int TEST_DATA_1_BATCH_SIZE = 15000;
private static final int TEST_DATA_2_BATCH_SIZE = 1500;
private static final int USB_TIMEOUT_MS = 1000; // 1s
private static final int TEST_MAX_TIME_MS = 200000; // 200s
private static final int ASYNC_MAX_OUTSTANDING_REQUESTS = 5;
private static final ByteOrder ORDER = ByteOrder.BIG_ENDIAN;
private final UsbDevice mDevice;
private final UsbDeviceConnection mUsbConnection;
private final SpeedMeasurementControllerCallback mCallback;
private final Context mContext;
public static Random sRandom = new Random(SystemClock.uptimeMillis());
SpeedMeasurementController(Context context,
UsbDevice device, UsbDeviceConnection conn,
SpeedMeasurementControllerCallback callback) {
if (TEST_DATA_SIZE % TEST_DATA_1_BATCH_SIZE == 0) {
throw new AssertionError("TEST_DATA_SIZE mod TEST_DATA_BIG_BATCH_SIZE must not be 0");
}
if (TEST_DATA_SIZE % TEST_DATA_2_BATCH_SIZE == 0) {
throw new AssertionError("TEST_DATA_SIZE mod TEST_DATA_SMALL_BATCH_SIZE must not be 0");
}
mContext = context;
mDevice = device;
mUsbConnection = conn;
mCallback = callback;
}
protected void release() {
if (mUsbConnection != null) {
mUsbConnection.close();
}
}
/**
* {@inheritDoc}
*
* Test runs two type of USB host->phone write tests:
* <ul>
* <li> Synchronous write with usage of UsbDeviceConnection.bulkTransfer. </li>
* <li> Aynchronous write with usage of UsbRequest and UsbDeviceConnection.requestWait. </li>
* </ul>
* Each test scenario also runs with different buffer size.
*/
@Override
public void run() {
Log.v(TAG, "Running sync test with buffer size #1");
runSyncTest(TEST_DATA_1_BATCH_SIZE);
Log.v(TAG, "Running sync test with buffer size #2");
runSyncTest(TEST_DATA_2_BATCH_SIZE);
Log.v(TAG, "Running async test with buffer size #1");
runAsyncTest(TEST_DATA_1_BATCH_SIZE);
Log.v(TAG, "Running async test with buffer size #2");
runAsyncTest(TEST_DATA_2_BATCH_SIZE);
Log.v(TAG, "Done running tests");
release();
mCallback.testSuiteFinished();
}
private void runTest(BaseWriterThread writer, ReaderThread reader, int mode, int bufferSize) {
mCallback.testStarted(mode, bufferSize);
writer.start();
reader.start();
try {
writer.join(TEST_MAX_TIME_MS);
} catch (InterruptedException e) {}
try {
reader.join(TEST_MAX_TIME_MS);
} catch (InterruptedException e) {}
if (reader.isAlive()) {
reader.requestToQuit();
try {
reader.join(USB_TIMEOUT_MS);
} catch (InterruptedException e) {}
if (reader.isAlive()) {
throw new RuntimeException("ReaderSyncThread still alive");
}
}
if (writer.isAlive()) {
writer.requestToQuit();
try {
writer.join(USB_TIMEOUT_MS);
} catch (InterruptedException e) {}
if (writer.isAlive()) {
throw new RuntimeException("WriterSyncThread still alive");
}
}
mCallback.testFinished(mode, bufferSize);
mCallback.testResult(
mode,
"Buffer size: " + bufferSize + " bytes. Speed " + writer.getSpeed());
}
private void runSyncTest(int bufferSize) {
ReaderThread readerSync = new ReaderThread(mDevice, mUsbConnection, TEST_MODE_SYNC);
WriterSyncThread writerSync = new WriterSyncThread(mDevice, mUsbConnection, bufferSize);
runTest(writerSync, readerSync, TEST_MODE_SYNC, bufferSize);
}
private void runAsyncTest(int bufferSize) {
ReaderThread readerAsync = new ReaderThread(mDevice, mUsbConnection, TEST_MODE_ASYNC);
WriterAsyncThread writerAsync = new WriterAsyncThread(mDevice, mUsbConnection, bufferSize);
runTest(writerAsync, readerAsync, TEST_MODE_ASYNC, bufferSize);
}
private class ReaderThread extends Thread {
private boolean mShouldQuit = false;
private final UsbDevice mDevice;
private final UsbDeviceConnection mUsbConnection;
private final int mMode;
private final UsbEndpoint mBulkIn;
private final byte[] mBuffer = new byte[16384];
private ReaderThread(UsbDevice device, UsbDeviceConnection conn, int testMode) {
super("AOAP reader");
mDevice = device;
mUsbConnection = conn;
mMode = testMode;
UsbInterface iface = mDevice.getInterface(0);
// Setup bulk endpoints.
UsbEndpoint bulkIn = null;
UsbEndpoint bulkOut = null;
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint ep = iface.getEndpoint(i);
if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
if (bulkIn == null) {
bulkIn = ep;
}
} else {
if (bulkOut == null) {
bulkOut = ep;
}
}
}
if (bulkIn == null || bulkOut == null) {
throw new IllegalStateException("Unable to find bulk endpoints");
}
mBulkIn = bulkIn;
}
public synchronized void requestToQuit() {
mShouldQuit = true;
}
private synchronized boolean shouldQuit() {
return mShouldQuit;
}
@Override
public void run() {
while (!shouldQuit()) {
int read = mUsbConnection.bulkTransfer(
mBulkIn, mBuffer, mBuffer.length, USB_TIMEOUT_MS);
if (read > 0) {
Log.v(TAG, "Read " + read + " bytes");
break;
}
}
}
}
private abstract class BaseWriterThread extends Thread {
protected boolean mShouldQuit = false;
protected long mSpeed;
protected final UsbDevice mDevice;
protected final int mBufferSize;
protected final UsbDeviceConnection mUsbConnection;
protected final UsbEndpoint mBulkOut;
private BaseWriterThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize) {
super("AOAP writer");
mDevice = device;
mUsbConnection = conn;
mBufferSize = bufferSize;
UsbInterface iface = mDevice.getInterface(0);
// Setup bulk endpoints.
UsbEndpoint bulkIn = null;
UsbEndpoint bulkOut = null;
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint ep = iface.getEndpoint(i);
if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
if (bulkIn == null) {
bulkIn = ep;
}
} else {
if (bulkOut == null) {
bulkOut = ep;
}
}
}
if (bulkIn == null || bulkOut == null) {
throw new IllegalStateException("Unable to find bulk endpoints");
}
mBulkOut = bulkOut;
}
public synchronized void requestToQuit() {
mShouldQuit = true;
}
protected synchronized boolean shouldQuit() {
return mShouldQuit;
}
public synchronized String getSpeed() {
return Formatter.formatFileSize(mContext, mSpeed) + "/s";
}
protected synchronized void setSpeed(long speed) {
// Speed is set in bytes/ms. Convert it to bytes/s.
mSpeed = speed * 1000;
}
protected byte[] intToByte(int value) {
return ByteBuffer.allocate(4).order(ORDER).putInt(value).array();
}
}
private class WriterSyncThread extends BaseWriterThread {
private WriterSyncThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize) {
super(device, conn, bufferSize);
}
private boolean writeBufferSize() {
byte[] bufferSizeArray = intToByte(mBufferSize);
int sentBytes = mUsbConnection.bulkTransfer(
mBulkOut,
bufferSizeArray,
bufferSizeArray.length,
USB_TIMEOUT_MS);
if (sentBytes < 0) {
Log.e(TAG, "Failed to write data");
return false;
}
return true;
}
@Override
public void run() {
int bytesToSend = TEST_DATA_SIZE;
if (!writeBufferSize()) {
return;
}
byte[] buffer = new byte[mBufferSize];
sRandom.nextBytes(buffer);
long timeStart = System.currentTimeMillis();
while (bytesToSend > 0 && !shouldQuit()) {
int sentBytes = mUsbConnection.bulkTransfer(
mBulkOut,
buffer,
(bytesToSend > buffer.length ? buffer.length : bytesToSend),
USB_TIMEOUT_MS);
if (sentBytes < 0) {
Log.e(TAG, "Failed to write data/");
return;
} else {
bytesToSend -= sentBytes;
}
}
setSpeed(TEST_DATA_SIZE / (System.currentTimeMillis() - timeStart));
}
}
private class WriterAsyncThread extends BaseWriterThread {
private WriterAsyncThread(UsbDevice device, UsbDeviceConnection conn, int bufferSize) {
super(device, conn, bufferSize);
}
private boolean drainRequests(int numRequests) {
while (numRequests > 0) {
UsbRequest req = mUsbConnection.requestWait();
if (req == null) {
Log.e(TAG, "Error while requestWait");
return false;
}
req.close();
numRequests--;
}
return true;
}
private boolean writeBufferSize() {
byte[] bufferSizeArray = intToByte(mBufferSize);
UsbRequest sendRequest = getNewRequest();
if (sendRequest == null) {
return false;
}
ByteBuffer bufferToSend = ByteBuffer.wrap(bufferSizeArray, 0, bufferSizeArray.length);
boolean queued = sendRequest.queue(bufferToSend, bufferSizeArray.length);
if (!queued) {
Log.e(TAG, "Failed to queue request");
return false;
}
UsbRequest req = mUsbConnection.requestWait();
if (req == null) {
Log.e(TAG, "Error while waiting for request to complete.");
return false;
}
req.close();
return true;
}
private UsbRequest getNewRequest() {
UsbRequest request = new UsbRequest();
if (!request.initialize(mUsbConnection, mBulkOut)) {
Log.e(TAG, "Failed to init");
return null;
}
return request;
}
@Override
public void run() {
int bytesToSend = TEST_DATA_SIZE;
if (!writeBufferSize()) {
return;
}
int numRequests = 0;
byte[] buffer = new byte[mBufferSize];
sRandom.nextBytes(buffer);
long timeStart = System.currentTimeMillis();
while (bytesToSend > 0 && !shouldQuit()) {
numRequests++;
UsbRequest sendRequest = getNewRequest();
if (sendRequest == null) {
return;
}
int bufferSize = (bytesToSend > buffer.length ? buffer.length : bytesToSend);
ByteBuffer bufferToSend = ByteBuffer.wrap(buffer, 0, bufferSize);
boolean queued = sendRequest.queue(bufferToSend, bufferSize);
if (queued) {
bytesToSend -= buffer.length;
} else {
Log.e(TAG, "Failed to queue more data");
return;
}
if (numRequests == ASYNC_MAX_OUTSTANDING_REQUESTS) {
UsbRequest req = mUsbConnection.requestWait();
if (req == null) {
Log.e(TAG, "Error while waiting for request to complete.");
return;
}
req.close();
numRequests--;
}
}
if (!drainRequests(numRequests)) {
return;
}
setSpeed(TEST_DATA_SIZE / (System.currentTimeMillis() - timeStart));
Log.d(TAG, "Wrote all the data. Exiting thread");
}
}
}