From 9bbdf3a9245e83dfb2a2f20afce7fd1dc41f95fb Mon Sep 17 00:00:00 2001 From: jwilson Date: Fri, 31 Jan 2014 22:24:05 -0500 Subject: [PATCH] Quick and dirty benchmark. Sample output: OkHttp [gzip, chunked, HTTP_11] bodyByteCount=1048576 headerCount=20 threadCount=10 Requests per second: 121.0 Requests per second: 304.6 Requests per second: 379.1 Requests per second: 386.9 Requests per second: 369.2 Requests per second: 390.8 Requests per second: 368.8 Requests per second: 325.2 Requests per second: 409.4 Requests per second: 389.1 --- benchmarks/pom.xml | 36 +++ .../squareup/okhttp/benchmarks/Benchmark.java | 238 ++++++++++++++++++ .../benchmarks/HttpURLConnectionRequest.java | 60 +++++ .../okhttp/mockwebserver/MockWebServer.java | 13 +- pom.xml | 1 + 5 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 benchmarks/pom.xml create mode 100644 benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java create mode 100644 benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpURLConnectionRequest.java diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml new file mode 100644 index 000000000000..e606d7f84d12 --- /dev/null +++ b/benchmarks/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + + com.squareup.okhttp + parent + 2.0.0-SNAPSHOT + + + benchmarks + Benchmarks + + + + com.squareup.okhttp + okhttp + ${project.version} + + + com.squareup.okhttp + mockwebserver + ${project.version} + + + org.bouncycastle + bcprov-jdk15on + + + org.mortbay.jetty.npn + npn-boot + provided + + + diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java new file mode 100644 index 000000000000..7637a3ba2482 --- /dev/null +++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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.squareup.okhttp.benchmarks; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Protocol; +import com.squareup.okhttp.internal.SslContextBuilder; +import com.squareup.okhttp.mockwebserver.Dispatcher; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; + +/** + * This benchmark is fake, but may be useful for certain relative comparisons. + * It uses a local connection to a MockWebServer to measure how many identical + * requests per second can be carried over a fixed number of threads. + */ +public class Benchmark { + private static final int NUM_REPORTS = 10; + private final Random random = new Random(0); + + /** Which client to run.*/ + // TODO: implement additional candidates for other HTTP client libraries. + Candidate candidate = new OkHttp(); + + /** How many concurrent threads to execute. */ + int threadCount = 10; + + /** True to use TLS. */ + // TODO: compare different ciphers? + boolean tls = false; + + /** True to use gzip content-encoding for the response body. */ + boolean gzip = true; + + /** Don't combine chunked with SPDY_3 or HTTP_2; that's not allowed. */ + boolean chunked = true; + + /** The size of the HTTP response body, in uncompressed bytes. */ + int bodyByteCount = 1024 * 1024; + + /** How many additional headers were included, beyond the built-in ones. */ + int headerCount = 20; + + /** Which ALPN/NPN protocols are in use. Only useful with TLS. */ + List protocols = Arrays.asList(Protocol.HTTP_11); + + public static void main(String[] args) throws IOException { + new Benchmark().run(); + } + + public void run() throws IOException { + ThreadPoolExecutor executor = new ThreadPoolExecutor(threadCount, threadCount, + 1, TimeUnit.SECONDS, new LinkedBlockingQueue()); + + System.out.println(toString()); + + // Prepare the client & server + candidate.prepare(); + MockWebServer server = startServer(); + String url = server.getUrl("/").toString(); + + int targetBacklog = 10; + int requestCount = 0; + long reportStart = System.nanoTime(); + long reportPeriod = TimeUnit.SECONDS.toNanos(1); + int reports = 0; + + // Run until we've printed enough reports. + while (reports < NUM_REPORTS) { + // Print a report if we haven't recently. + long now = System.nanoTime(); + double reportDuration = now - reportStart; + if (reportDuration > reportPeriod) { + double requestsPerSecond = requestCount / reportDuration * TimeUnit.SECONDS.toNanos(1); + System.out.println(String.format("Requests per second: %.1f", requestsPerSecond)); + requestCount = 0; + reportStart = now; + reports++; + } + + // Fill the job queue with work. + while (executor.getQueue().size() < targetBacklog) { + executor.execute(candidate.request(url)); + requestCount++; + } + + // The job queue is full. Take a break. + sleep(10); + } + } + + @Override public String toString() { + List modifiers = new ArrayList(); + if (tls) modifiers.add("tls"); + if (gzip) modifiers.add("gzip"); + if (chunked) modifiers.add("chunked"); + modifiers.addAll(protocols); + + return String.format("%s %s\n" + + "bodyByteCount=%s headerCount=%s threadCount=%s", + candidate.getClass().getSimpleName(), modifiers, + bodyByteCount, headerCount, threadCount); + } + + private void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException ignored) { + } + } + + private MockWebServer startServer() throws IOException { + Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING); + MockWebServer server = new MockWebServer(); + + if (tls) { + SSLContext sslContext = SslContextBuilder.localhost(); + server.useHttps(sslContext.getSocketFactory(), false); + server.setNpnEnabled(true); + } + + final MockResponse response = newResponse(); + server.setDispatcher(new Dispatcher() { + @Override public MockResponse dispatch(RecordedRequest request) { + return response; + } + }); + + server.play(); + return server; + } + + private MockResponse newResponse() throws IOException { + byte[] body = new byte[bodyByteCount]; + random.nextBytes(body); + + MockResponse result = new MockResponse(); + + if (gzip) { + body = gzip(body); + result.addHeader("Content-Encoding: gzip"); + } + + if (chunked) { + result.setChunkedBody(body, 1024); + } else { + result.setBody(body); + } + + for (int i = 0; i < headerCount; i++) { + result.addHeader(randomString(12), randomString(20)); + } + + return result; + } + + private String randomString(int length) { + String alphabet = "-abcdefghijklmnopqrstuvwxyz"; + char[] result = new char[length]; + for (int i = 0; i < length; i++) { + result[i] = alphabet.charAt(random.nextInt(alphabet.length())); + } + return new String(result); + } + + /** Returns a gzipped copy of {@code bytes}. */ + private byte[] gzip(byte[] bytes) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + OutputStream gzippedOut = new GZIPOutputStream(bytesOut); + gzippedOut.write(bytes); + gzippedOut.close(); + return bytesOut.toByteArray(); + } + + interface Candidate { + void prepare(); + Runnable request(String url); + } + + class OkHttp implements Candidate { + private OkHttpClient client; + + @Override public void prepare() { + client = new OkHttpClient(); + client.setProtocols(protocols); + + URL.setURLStreamHandlerFactory(client); + + if (tls) { + SSLContext sslContext = SslContextBuilder.localhost(); + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override public boolean verify(String s, SSLSession session) { + return true; + } + }; + client.setSslSocketFactory(socketFactory); + client.setHostnameVerifier(hostnameVerifier); + } + } + + @Override public Runnable request(String url) { + return new HttpURLConnectionRequest(url); + } + } +} diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpURLConnectionRequest.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpURLConnectionRequest.java new file mode 100644 index 000000000000..b1eb99eceda1 --- /dev/null +++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/HttpURLConnectionRequest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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.squareup.okhttp.benchmarks; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +public class HttpURLConnectionRequest implements Runnable { + private static final boolean VERBOSE = false; + private final URL url; + + public HttpURLConnectionRequest(String url) { + try { + this.url = new URL(url); + } catch (MalformedURLException e) { + throw new AssertionError(); + } + } + + public void run() { + byte[] buffer = new byte[1024]; + long start = System.nanoTime(); + try { + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + InputStream in = urlConnection.getInputStream(); + + // Discard the response body. + int total = 0; + for (int count; (count = in.read(buffer)) != -1; ) { + total += count; + } + in.close(); + long finish = System.nanoTime(); + + if (VERBOSE) { + System.out.println(String.format("Transferred % 8d bytes in %4d ms", + total, TimeUnit.NANOSECONDS.toMillis(finish - start))); + } + } catch (IOException e) { + System.out.println("Failed: " + e); + } + } +} diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java index 4ca28bab4cc8..90e2925e625a 100644 --- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java +++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java @@ -18,10 +18,10 @@ package com.squareup.okhttp.mockwebserver; import com.squareup.okhttp.Protocol; -import com.squareup.okhttp.internal.bytes.ByteString; import com.squareup.okhttp.internal.NamedRunnable; import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.Util; +import com.squareup.okhttp.internal.bytes.ByteString; import com.squareup.okhttp.internal.spdy.Header; import com.squareup.okhttp.internal.spdy.IncomingStreamHandler; import com.squareup.okhttp.internal.spdy.SpdyConnection; @@ -72,7 +72,6 @@ * replays them upon request in sequence. */ public final class MockWebServer { - private static final X509TrustManager UNTRUSTED_TRUST_MANAGER = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { @@ -381,7 +380,9 @@ private boolean processOneRequest(Socket socket, InputStream in, OutputStream ou } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_OUTPUT_AT_END) { socket.shutdownOutput(); } - logger.info("Received request: " + request + " and responded: " + response); + if (logger.isLoggable(Level.INFO)) { + logger.info("Received request: " + request + " and responded: " + response); + } sequenceNumber++; return true; } @@ -611,8 +612,10 @@ private SpdySocketHandler(Socket socket, Protocol protocol) { throw new AssertionError(e); } writeResponse(stream, response); - logger.info("Received request: " + request + " and responded: " + response - + " protocol is " + protocol.name.utf8()); + if (logger.isLoggable(Level.INFO)) { + logger.info("Received request: " + request + " and responded: " + response + + " protocol is " + protocol.name.utf8()); + } } private RecordedRequest readRequest(SpdyStream stream) throws IOException { diff --git a/pom.xml b/pom.xml index a4ab7ea59988..aea71da07add 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ okhttp-protocols mockwebserver samples + benchmarks