diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 000000000000..59f571fc5548
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,8 @@
+OkHttp Benchmarks
+=======================================
+
+This module allows you to test the performance of HTTP clients.
+
+### Running
+ 1. If you made modifications to `com.squareup.okhttp.benchmarks.Benchmark` run `mvn compile`.
+ 2. Run `mvn exec:exec` to launch a new JVM, which will execute the benchmark.
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
new file mode 100644
index 000000000000..fb67f50eeb43
--- /dev/null
+++ b/benchmarks/pom.xml
@@ -0,0 +1,92 @@
+
+
+
+ 4.0.0
+
+
+ com.squareup.okhttp
+ parent
+ 2.0.0-SNAPSHOT
+
+
+ benchmarks
+ Benchmarks
+
+
+
+ com.google.caliper
+ caliper
+ 1.0-beta-1
+
+
+ com.squareup.okhttp
+ okhttp
+ ${project.version}
+
+
+ com.squareup.okhttp
+ mockwebserver
+ ${project.version}
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+
+
+ org.mortbay.jetty.npn
+ npn-boot
+ provided
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ io.netty
+ netty-transport
+ 4.0.15.Final
+
+
+ io.netty
+ netty-handler
+ 4.0.15.Final
+
+
+ io.netty
+ netty-codec-http
+ 4.0.15.Final
+
+
+
+ com.jcraft
+ jzlib
+ 1.1.2
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+
+ java
+
+
+
+
+ java
+
+ -Xms512m
+ -Xmx512m
+ -Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar
+ -classpath
+
+ com.squareup.okhttp.benchmarks.Benchmark
+
+
+
+
+
+
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
new file mode 100644
index 000000000000..cb8e719111c4
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/ApacheHttpClient.java
@@ -0,0 +1,85 @@
+/*
+ * 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.internal.SslContextBuilder;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.GZIPInputStream;
+import javax.net.ssl.SSLContext;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.PoolingClientConnectionManager;
+
+/** Benchmark Apache HTTP client. */
+class ApacheHttpClient extends SynchronousHttpClient {
+ private static final boolean VERBOSE = false;
+
+ private HttpClient client;
+
+ @Override public void prepare(Benchmark benchmark) {
+ super.prepare(benchmark);
+ ClientConnectionManager connectionManager = new PoolingClientConnectionManager();
+ if (benchmark.tls) {
+ SSLContext sslContext = SslContextBuilder.localhost();
+ connectionManager.getSchemeRegistry().register(
+ new Scheme("https", 443, new SSLSocketFactory(sslContext)));
+ }
+ client = new DefaultHttpClient(connectionManager);
+ }
+
+ @Override public Runnable request(URL url) {
+ return new ApacheHttpClientRequest(url);
+ }
+
+ class ApacheHttpClientRequest implements Runnable {
+ private final URL url;
+
+ public ApacheHttpClientRequest(URL url) {
+ this.url = url;
+ }
+
+ public void run() {
+ long start = System.nanoTime();
+ try {
+ HttpResponse response = client.execute(new HttpGet(url.toString()));
+ InputStream in = response.getEntity().getContent();
+ Header contentEncoding = response.getFirstHeader("Content-Encoding");
+ if (contentEncoding != null && contentEncoding.getValue().equals("gzip")) {
+ in = new GZIPInputStream(in);
+ }
+
+ long total = readAllAndClose(in);
+ 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/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..151128d6c055
--- /dev/null
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
@@ -0,0 +1,223 @@
+/*
+ * 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.google.caliper.Param;
+import com.google.caliper.model.ArbitraryMeasurement;
+import com.google.caliper.runner.CaliperMain;
+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.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.SSLContext;
+
+/**
+ * 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 extends com.google.caliper.Benchmark {
+ private static final int NUM_REPORTS = 10;
+ private static final boolean VERBOSE = false;
+
+ private final Random random = new Random(0);
+
+ /** Which client to run.*/
+ @Param
+ Client client;
+
+ /** How many concurrent requests to execute. */
+ @Param({ "1", "10" })
+ int concurrencyLevel;
+
+ /** How many requests to enqueue to await threads to execute them. */
+ @Param({ "10" })
+ int targetBacklog;
+
+ /** True to use TLS. */
+ // TODO: compare different ciphers?
+ @Param
+ boolean tls;
+
+ /** True to use gzip content-encoding for the response body. */
+ @Param
+ boolean gzip;
+
+ /** Don't combine chunked with SPDY_3 or HTTP_2; that's not allowed. */
+ @Param
+ boolean chunked;
+
+ /** The size of the HTTP response body, in uncompressed bytes. */
+ @Param({ "128", "1048576" })
+ int bodyByteCount;
+
+ /** How many additional headers were included, beyond the built-in ones. */
+ @Param({ "0", "20" })
+ int headerCount;
+
+ /** Which ALPN/NPN protocols are in use. Only useful with TLS. */
+ List protocols = Arrays.asList(Protocol.HTTP_11);
+
+ public static void main(String[] args) {
+ List allArgs = new ArrayList();
+ allArgs.add("--instrument");
+ allArgs.add("arbitrary");
+ allArgs.addAll(Arrays.asList(args));
+
+ CaliperMain.main(Benchmark.class, allArgs.toArray(new String[allArgs.size()]));
+ }
+
+ @ArbitraryMeasurement(description = "requests per second")
+ public double run() throws Exception {
+ if (VERBOSE) System.out.println(toString());
+ HttpClient httpClient = client.create();
+
+ // Prepare the client & server
+ httpClient.prepare(this);
+ MockWebServer server = startServer();
+ URL url = server.getUrl("/");
+
+ int requestCount = 0;
+ long reportStart = System.nanoTime();
+ long reportPeriod = TimeUnit.SECONDS.toNanos(1);
+ int reports = 0;
+ double best = 0.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);
+ if (VERBOSE) {
+ System.out.println(String.format("Requests per second: %.1f", requestsPerSecond));
+ }
+ best = Math.max(best, requestsPerSecond);
+ requestCount = 0;
+ reportStart = now;
+ reports++;
+ }
+
+ // Fill the job queue with work.
+ while (httpClient.acceptingJobs()) {
+ httpClient.enqueue(url);
+ requestCount++;
+ }
+
+ // The job queue is full. Take a break.
+ sleep(1);
+ }
+
+ return best;
+ }
+
+ @Override public String toString() {
+ List