| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net; |
| |
| import android.app.Activity; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Debug; |
| |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.PathUtils; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.base.task.TaskTraits; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.HttpURLConnection; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Runs networking benchmarks and saves results to a file. */ |
| public class CronetPerfTestActivity extends Activity { |
| private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_perf_test"; |
| // Benchmark configuration passed down from host via Intent data. |
| // Call getConfig*(key) to extract individual configuration values. |
| private Uri mConfig; |
| |
| // Functions that retrieve individual benchmark configuration values. |
| private String getConfigString(String key) { |
| return mConfig.getQueryParameter(key); |
| } |
| |
| private int getConfigInt(String key) { |
| return Integer.parseInt(mConfig.getQueryParameter(key)); |
| } |
| |
| private boolean getConfigBoolean(String key) { |
| return Boolean.parseBoolean(mConfig.getQueryParameter(key)); |
| } |
| |
| private enum Mode { |
| SYSTEM_HUC, // Benchmark system HttpURLConnection |
| CRONET_HUC, // Benchmark Cronet's HttpURLConnection |
| CRONET_ASYNC, // Benchmark Cronet's asynchronous API |
| } |
| |
| private enum Direction { |
| UP, // Benchmark upload (i.e. POST) |
| DOWN, // Benchmark download (i.e. GET) |
| } |
| |
| private enum Size { |
| LARGE, // Large benchmark |
| SMALL, // Small benchmark |
| } |
| |
| private enum Protocol { |
| HTTP, |
| QUIC, |
| } |
| |
| // Put together a benchmark configuration into a benchmark name. |
| // Make it fixed length for more readable tables. |
| // Benchmark names are written to the JSON output file and slurped up by Telemetry on the host. |
| private static String buildBenchmarkName( |
| Mode mode, Direction direction, Protocol protocol, int concurrency, int iterations) { |
| String name = direction == Direction.UP ? "Up___" : "Down_"; |
| switch (protocol) { |
| case HTTP: |
| name += "H_"; |
| break; |
| case QUIC: |
| name += "Q_"; |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown protocol: " + protocol); |
| } |
| name += iterations + "_" + concurrency + "_"; |
| switch (mode) { |
| case SYSTEM_HUC: |
| name += "SystemHUC__"; |
| break; |
| case CRONET_HUC: |
| name += "CronetHUC__"; |
| break; |
| case CRONET_ASYNC: |
| name += "CronetAsync"; |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown mode: " + mode); |
| } |
| return name; |
| } |
| |
| // Responsible for running one particular benchmark and timing it. |
| private class Benchmark { |
| private final Mode mMode; |
| private final Direction mDirection; |
| private final Protocol mProtocol; |
| private final URL mUrl; |
| private final String mName; |
| private final CronetEngine mCronetEngine; |
| // Size in bytes of content being uploaded or downloaded. |
| private final int mLength; |
| // How many requests to execute. |
| private final int mIterations; |
| // How many requests to execute in parallel at any one time. |
| private final int mConcurrency; |
| // Dictionary of benchmark names mapped to times to complete the benchmarks. |
| private final JSONObject mResults; |
| // How large a buffer to use for passing content, in bytes. |
| private final int mBufferSize; |
| // Cached copy of getConfigBoolean("CRONET_ASYNC_USE_NETWORK_THREAD") for faster access. |
| private final boolean mUseNetworkThread; |
| |
| private long mStartTimeMs = -1; |
| private long mStopTimeMs = -1; |
| |
| /** |
| * Create a new benchmark to run. Sets up various configuration settings. |
| * @param mode The API to benchmark. |
| * @param direction The transfer direction to benchmark (i.e. upload or download). |
| * @param size The size of the transfers to benchmark (i.e. large or small). |
| * @param protocol The transfer protocol to benchmark (i.e. HTTP or QUIC). |
| * @param concurrency The number of transfers to perform concurrently. |
| * @param results Mapping of benchmark names to time required to run the benchmark in ms. |
| * When the benchmark completes this is updated with the result. |
| */ |
| public Benchmark( |
| Mode mode, |
| Direction direction, |
| Size size, |
| Protocol protocol, |
| int concurrency, |
| JSONObject results) { |
| mMode = mode; |
| mDirection = direction; |
| mProtocol = protocol; |
| final String resource; |
| switch (size) { |
| case SMALL: |
| resource = getConfigString("SMALL_RESOURCE"); |
| mIterations = getConfigInt("SMALL_ITERATIONS"); |
| mLength = getConfigInt("SMALL_RESOURCE_SIZE"); |
| break; |
| case LARGE: |
| // When measuring a large upload, only download a small amount so download time |
| // isn't significant. |
| resource = |
| getConfigString( |
| direction == Direction.UP |
| ? "SMALL_RESOURCE" |
| : "LARGE_RESOURCE"); |
| mIterations = getConfigInt("LARGE_ITERATIONS"); |
| mLength = getConfigInt("LARGE_RESOURCE_SIZE"); |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown size: " + size); |
| } |
| final String scheme; |
| final String host; |
| final int port; |
| switch (protocol) { |
| case HTTP: |
| scheme = "http"; |
| host = getConfigString("HOST_IP"); |
| port = getConfigInt("HTTP_PORT"); |
| break; |
| case QUIC: |
| scheme = "https"; |
| host = getConfigString("HOST"); |
| port = getConfigInt("QUIC_PORT"); |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown protocol: " + protocol); |
| } |
| try { |
| mUrl = new URL(scheme, host, port, resource); |
| } catch (MalformedURLException e) { |
| throw new IllegalArgumentException( |
| "Bad URL: " + host + ":" + port + "/" + resource); |
| } |
| final ExperimentalCronetEngine.Builder cronetEngineBuilder = |
| new ExperimentalCronetEngine.Builder(CronetPerfTestActivity.this); |
| System.loadLibrary("cronet_tests"); |
| if (mProtocol == Protocol.QUIC) { |
| cronetEngineBuilder.enableQuic(true); |
| cronetEngineBuilder.addQuicHint(host, port, port); |
| CronetTestUtil.setMockCertVerifierForTesting( |
| cronetEngineBuilder, |
| MockCertVerifier.createMockCertVerifier( |
| new String[] {getConfigString("QUIC_CERT_FILE")}, true)); |
| } |
| |
| try { |
| JSONObject hostResolverParams = |
| CronetTestUtil.generateHostResolverRules(getConfigString("HOST_IP")); |
| JSONObject experimentalOptions = |
| new JSONObject().put("HostResolverRules", hostResolverParams); |
| cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); |
| } catch (JSONException e) { |
| throw new IllegalStateException("JSON failed: " + e); |
| } |
| mCronetEngine = cronetEngineBuilder.build(); |
| mName = buildBenchmarkName(mode, direction, protocol, concurrency, mIterations); |
| mConcurrency = concurrency; |
| mResults = results; |
| mBufferSize = |
| mLength > getConfigInt("MAX_BUFFER_SIZE") |
| ? getConfigInt("MAX_BUFFER_SIZE") |
| : mLength; |
| mUseNetworkThread = getConfigBoolean("CRONET_ASYNC_USE_NETWORK_THREAD"); |
| } |
| |
| private void startTimer() { |
| mStartTimeMs = System.currentTimeMillis(); |
| } |
| |
| private void stopTimer() { |
| mStopTimeMs = System.currentTimeMillis(); |
| } |
| |
| private void reportResult() { |
| if (mStartTimeMs == -1 || mStopTimeMs == -1) { |
| throw new IllegalStateException("startTimer() or stopTimer() not called"); |
| } |
| try { |
| mResults.put(mName, mStopTimeMs - mStartTimeMs); |
| } catch (JSONException e) { |
| System.out.println("Failed to write JSON result for " + mName); |
| } |
| } |
| |
| private void startLogging() { |
| if (getConfigBoolean("CAPTURE_NETLOG")) { |
| mCronetEngine.startNetLogToFile( |
| getFilesDir().getPath() + "/" + mName + ".json", false); |
| } |
| if (getConfigBoolean("CAPTURE_TRACE")) { |
| Debug.startMethodTracing(getFilesDir().getPath() + "/" + mName + ".trace"); |
| } else if (getConfigBoolean("CAPTURE_SAMPLED_TRACE")) { |
| Debug.startMethodTracingSampling( |
| getFilesDir().getPath() + "/" + mName + ".trace", 8000000, 10); |
| } |
| } |
| |
| private void stopLogging() { |
| if (getConfigBoolean("CAPTURE_NETLOG")) { |
| mCronetEngine.stopNetLog(); |
| } |
| if (getConfigBoolean("CAPTURE_TRACE") || getConfigBoolean("CAPTURE_SAMPLED_TRACE")) { |
| Debug.stopMethodTracing(); |
| } |
| } |
| |
| /** |
| * Transfer {@code mLength} bytes through HttpURLConnection in {@code mDirection} direction. |
| * @param urlConnection The HttpURLConnection to use for transfer. |
| * @param buffer A buffer of length |mBufferSize| to use for transfer. |
| * @return {@code true} if transfer completed successfully. |
| */ |
| private boolean exerciseHttpURLConnection(URLConnection urlConnection, byte[] buffer) |
| throws IOException { |
| final HttpURLConnection connection = (HttpURLConnection) urlConnection; |
| try { |
| int bytesTransfered = 0; |
| if (mDirection == Direction.DOWN) { |
| final InputStream inputStream = connection.getInputStream(); |
| while (true) { |
| final int bytesRead = inputStream.read(buffer, 0, mBufferSize); |
| if (bytesRead == -1) { |
| break; |
| } else { |
| bytesTransfered += bytesRead; |
| } |
| } |
| } else { |
| connection.setDoOutput(true); |
| connection.setRequestMethod("POST"); |
| connection.setRequestProperty("Content-Length", Integer.toString(mLength)); |
| final OutputStream outputStream = connection.getOutputStream(); |
| for (int remaining = mLength; remaining > 0; remaining -= mBufferSize) { |
| outputStream.write(buffer, 0, Math.min(remaining, mBufferSize)); |
| } |
| bytesTransfered = mLength; |
| } |
| return connection.getResponseCode() == 200 && bytesTransfered == mLength; |
| } finally { |
| connection.disconnect(); |
| } |
| } |
| |
| // GET or POST to one particular URL using URL.openConnection() |
| private class SystemHttpURLConnectionFetchTask implements Callable<Boolean> { |
| private final byte[] mBuffer = new byte[mBufferSize]; |
| |
| @Override |
| public Boolean call() { |
| try { |
| return exerciseHttpURLConnection(mUrl.openConnection(), mBuffer); |
| } catch (IOException e) { |
| System.out.println("System HttpURLConnection failed with " + e); |
| return false; |
| } |
| } |
| } |
| |
| // GET or POST to one particular URL using Cronet HttpURLConnection API |
| private class CronetHttpURLConnectionFetchTask implements Callable<Boolean> { |
| private final byte[] mBuffer = new byte[mBufferSize]; |
| |
| @Override |
| public Boolean call() { |
| try { |
| return exerciseHttpURLConnection(mCronetEngine.openConnection(mUrl), mBuffer); |
| } catch (IOException e) { |
| System.out.println("Cronet HttpURLConnection failed with " + e); |
| return false; |
| } |
| } |
| } |
| |
| // GET or POST to one particular URL using Cronet's asynchronous API |
| private class CronetAsyncFetchTask implements Callable<Boolean> { |
| // A message-queue for asynchronous tasks to post back to. |
| private final LinkedBlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(); |
| private final WorkQueueExecutor mWorkQueueExecutor = new WorkQueueExecutor(); |
| |
| private int mRemainingRequests; |
| private int mConcurrentFetchersDone; |
| private boolean mFailed; |
| |
| CronetAsyncFetchTask() { |
| mRemainingRequests = mIterations; |
| mConcurrentFetchersDone = 0; |
| mFailed = false; |
| } |
| |
| private void initiateRequest(final ByteBuffer buffer) { |
| if (mRemainingRequests == 0) { |
| mConcurrentFetchersDone++; |
| if (mUseNetworkThread) { |
| // Post empty task so message loop exit condition is retested. |
| postToWorkQueue( |
| new Runnable() { |
| @Override |
| public void run() {} |
| }); |
| } |
| return; |
| } |
| mRemainingRequests--; |
| final Runnable completionCallback = |
| new Runnable() { |
| @Override |
| public void run() { |
| initiateRequest(buffer); |
| } |
| }; |
| final UrlRequest.Builder builder = |
| mCronetEngine.newUrlRequestBuilder( |
| mUrl.toString(), |
| new Callback(buffer, completionCallback), |
| mWorkQueueExecutor); |
| if (mDirection == Direction.UP) { |
| builder.setUploadDataProvider(new Uploader(buffer), mWorkQueueExecutor); |
| builder.addHeader("Content-Type", "application/octet-stream"); |
| } |
| builder.build().start(); |
| } |
| |
| private class Uploader extends UploadDataProvider { |
| private final ByteBuffer mBuffer; |
| private int mRemainingBytes; |
| |
| Uploader(ByteBuffer buffer) { |
| mBuffer = buffer; |
| mRemainingBytes = mLength; |
| } |
| |
| @Override |
| public long getLength() { |
| return mLength; |
| } |
| |
| @Override |
| public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) { |
| mBuffer.clear(); |
| // Don't post more than |mLength|. |
| if (mRemainingBytes < mBuffer.limit()) { |
| mBuffer.limit(mRemainingBytes); |
| } |
| // Don't overflow |byteBuffer|. |
| if (byteBuffer.remaining() < mBuffer.limit()) { |
| mBuffer.limit(byteBuffer.remaining()); |
| } |
| byteBuffer.put(mBuffer); |
| mRemainingBytes -= mBuffer.position(); |
| uploadDataSink.onReadSucceeded(false); |
| } |
| |
| @Override |
| public void rewind(UploadDataSink uploadDataSink) { |
| uploadDataSink.onRewindError(new Exception("no rewinding")); |
| } |
| } |
| |
| private class Callback extends UrlRequest.Callback { |
| private final ByteBuffer mBuffer; |
| private final Runnable mCompletionCallback; |
| private int mBytesReceived; |
| |
| Callback(ByteBuffer buffer, Runnable completionCallback) { |
| mBuffer = buffer; |
| mCompletionCallback = completionCallback; |
| } |
| |
| @Override |
| public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { |
| mBuffer.clear(); |
| request.read(mBuffer); |
| } |
| |
| @Override |
| public void onRedirectReceived( |
| UrlRequest request, UrlResponseInfo info, String newLocationUrl) { |
| request.followRedirect(); |
| } |
| |
| @Override |
| public void onReadCompleted( |
| UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { |
| mBytesReceived += byteBuffer.position(); |
| mBuffer.clear(); |
| request.read(mBuffer); |
| } |
| |
| @Override |
| public void onSucceeded(UrlRequest request, UrlResponseInfo info) { |
| if (info.getHttpStatusCode() != 200 || mBytesReceived != mLength) { |
| System.out.println( |
| "Failed: response code: " |
| + info.getHttpStatusCode() |
| + " bytes: " |
| + mBytesReceived); |
| mFailed = true; |
| } |
| mCompletionCallback.run(); |
| } |
| |
| @Override |
| public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException e) { |
| System.out.println("Async request failed with " + e); |
| mFailed = true; |
| } |
| } |
| |
| private void postToWorkQueue(Runnable task) { |
| try { |
| mWorkQueue.put(task); |
| } catch (InterruptedException e) { |
| mFailed = true; |
| } |
| } |
| |
| private class WorkQueueExecutor implements Executor { |
| @Override |
| public void execute(Runnable task) { |
| if (mUseNetworkThread) { |
| task.run(); |
| } else { |
| postToWorkQueue(task); |
| } |
| } |
| } |
| |
| @Override |
| public Boolean call() { |
| // Initiate concurrent requests. |
| for (int i = 0; i < mConcurrency; i++) { |
| initiateRequest(ByteBuffer.allocateDirect(mBufferSize)); |
| } |
| // Wait for all jobs to finish. |
| try { |
| while (mConcurrentFetchersDone != mConcurrency && !mFailed) { |
| mWorkQueue.take().run(); |
| } |
| } catch (InterruptedException e) { |
| System.out.println("Async tasks failed with " + e); |
| mFailed = true; |
| } |
| return !mFailed; |
| } |
| } |
| |
| /** Executes the benchmark, times how long it takes, and records time in |mResults|. */ |
| public void run() { |
| final ExecutorService executor = Executors.newFixedThreadPool(mConcurrency); |
| final List<Callable<Boolean>> tasks = new ArrayList<>(mIterations); |
| startLogging(); |
| // Prepare list of tasks to run. |
| switch (mMode) { |
| case SYSTEM_HUC: |
| for (int i = 0; i < mIterations; i++) { |
| tasks.add(new SystemHttpURLConnectionFetchTask()); |
| } |
| break; |
| case CRONET_HUC: |
| { |
| for (int i = 0; i < mIterations; i++) { |
| tasks.add(new CronetHttpURLConnectionFetchTask()); |
| } |
| break; |
| } |
| case CRONET_ASYNC: |
| tasks.add(new CronetAsyncFetchTask()); |
| break; |
| default: |
| throw new IllegalArgumentException("Unknown mode: " + mMode); |
| } |
| // Execute tasks. |
| boolean success = true; |
| List<Future<Boolean>> futures = new ArrayList<>(); |
| try { |
| startTimer(); |
| // If possible execute directly to lessen impact of thread-pool overhead. |
| if (tasks.size() == 1 || mConcurrency == 1) { |
| for (int i = 0; i < tasks.size(); i++) { |
| if (!tasks.get(i).call()) { |
| success = false; |
| } |
| } |
| } else { |
| futures = executor.invokeAll(tasks); |
| executor.shutdown(); |
| executor.awaitTermination(240, TimeUnit.SECONDS); |
| } |
| stopTimer(); |
| for (Future<Boolean> future : futures) { |
| if (!future.isDone() || !future.get()) { |
| success = false; |
| break; |
| } |
| } |
| } catch (Exception e) { |
| System.out.println("Batch execution failed with " + e); |
| success = false; |
| } |
| stopLogging(); |
| if (success) { |
| reportResult(); |
| } |
| } |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| // Initializing application context here due to lack of custom CronetPerfTestApplication. |
| ContextUtils.initApplicationContext(getApplicationContext()); |
| PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); |
| mConfig = getIntent().getData(); |
| // Execute benchmarks on another thread to avoid networking on main thread. |
| |
| PostTask.postTask( |
| TaskTraits.USER_BLOCKING, |
| () -> { |
| JSONObject results = new JSONObject(); |
| for (Mode mode : Mode.values()) { |
| for (Direction direction : Direction.values()) { |
| for (Protocol protocol : Protocol.values()) { |
| if (protocol == Protocol.QUIC && mode == Mode.SYSTEM_HUC) { |
| // Unsupported; skip. |
| continue; |
| } |
| // Run large and small benchmarks one at a time to test |
| // single-threaded use. |
| // Also run them four at a time to see how they benefit from |
| // concurrency. |
| // The value four was chosen as many devices are now quad-core. |
| new Benchmark(mode, direction, Size.LARGE, protocol, 1, results) |
| .run(); |
| new Benchmark(mode, direction, Size.LARGE, protocol, 4, results) |
| .run(); |
| new Benchmark(mode, direction, Size.SMALL, protocol, 1, results) |
| .run(); |
| new Benchmark(mode, direction, Size.SMALL, protocol, 4, results) |
| .run(); |
| // Large benchmarks are generally bandwidth bound and unaffected by |
| // per-request overhead. Small benchmarks are not, so test at |
| // further increased concurrency to see if further benefit is |
| // possible. |
| new Benchmark(mode, direction, Size.SMALL, protocol, 8, results) |
| .run(); |
| } |
| } |
| } |
| final File outputFile = new File(getConfigString("RESULTS_FILE")); |
| final File doneFile = new File(getConfigString("DONE_FILE")); |
| // If DONE_FILE exists, something is horribly wrong, produce no results to |
| // convey this. |
| if (doneFile.exists()) { |
| results = new JSONObject(); |
| } |
| // Write out results to RESULTS_FILE, then create DONE_FILE. |
| FileOutputStream outputFileStream = null; |
| FileOutputStream doneFileStream = null; |
| try { |
| outputFileStream = new FileOutputStream(outputFile); |
| outputFileStream.write(results.toString().getBytes()); |
| outputFileStream.close(); |
| doneFileStream = new FileOutputStream(doneFile); |
| doneFileStream.close(); |
| } catch (Exception e) { |
| System.out.println("Failed write results file: " + e); |
| } finally { |
| try { |
| if (outputFileStream != null) { |
| outputFileStream.close(); |
| } |
| if (doneFileStream != null) { |
| doneFileStream.close(); |
| } |
| } catch (IOException e) { |
| System.out.println("Failed to close output file: " + e); |
| } |
| } |
| finish(); |
| }); |
| } |
| } |