1 /*
2  * Copyright (C) 2014 Square, Inc.
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.squareup.okhttp.benchmarks;
17 
18 import com.google.caliper.Param;
19 import com.google.caliper.model.ArbitraryMeasurement;
20 import com.google.caliper.runner.CaliperMain;
21 import com.squareup.okhttp.HttpUrl;
22 import com.squareup.okhttp.Protocol;
23 import com.squareup.okhttp.internal.SslContextBuilder;
24 import com.squareup.okhttp.mockwebserver.Dispatcher;
25 import com.squareup.okhttp.mockwebserver.MockResponse;
26 import com.squareup.okhttp.mockwebserver.MockWebServer;
27 import com.squareup.okhttp.mockwebserver.RecordedRequest;
28 import java.io.IOException;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.List;
32 import java.util.Random;
33 import java.util.concurrent.TimeUnit;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36 import javax.net.ssl.SSLContext;
37 import okio.Buffer;
38 import okio.GzipSink;
39 
40 /**
41  * This benchmark is fake, but may be useful for certain relative comparisons.
42  * It uses a local connection to a MockWebServer to measure how many identical
43  * requests per second can be carried over a fixed number of threads.
44  */
45 public class Benchmark extends com.google.caliper.Benchmark {
46   private static final int NUM_REPORTS = 10;
47   private static final boolean VERBOSE = false;
48 
49   private final Random random = new Random(0);
50 
51   /** Which client to run.*/
52   @Param
53   Client client;
54 
55   /** How many concurrent requests to execute. */
56   @Param({ "1", "10" })
57   int concurrencyLevel;
58 
59   /** How many requests to enqueue to await threads to execute them. */
60   @Param({ "10" })
61   int targetBacklog;
62 
63   /** True to use TLS. */
64   // TODO: compare different ciphers?
65   @Param
66   boolean tls;
67 
68   /** True to use gzip content-encoding for the response body. */
69   @Param
70   boolean gzip;
71 
72   /** Don't combine chunked with SPDY_3 or HTTP_2; that's not allowed. */
73   @Param
74   boolean chunked;
75 
76   /** The size of the HTTP response body, in uncompressed bytes. */
77   @Param({ "128", "1048576" })
78   int bodyByteCount;
79 
80   /** How many additional headers were included, beyond the built-in ones. */
81   @Param({ "0", "20" })
82   int headerCount;
83 
84   /** Which ALPN protocols are in use. Only useful with TLS. */
85   List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1);
86 
main(String[] args)87   public static void main(String[] args) {
88     List<String> allArgs = new ArrayList<>();
89     allArgs.add("--instrument");
90     allArgs.add("arbitrary");
91     allArgs.addAll(Arrays.asList(args));
92 
93     CaliperMain.main(Benchmark.class, allArgs.toArray(new String[allArgs.size()]));
94   }
95 
96   @ArbitraryMeasurement(description = "requests per second")
run()97   public double run() throws Exception {
98     if (VERBOSE) System.out.println(toString());
99     HttpClient httpClient = client.create();
100 
101     // Prepare the client & server
102     httpClient.prepare(this);
103     MockWebServer server = startServer();
104     HttpUrl url = server.url("/");
105 
106     int requestCount = 0;
107     long reportStart = System.nanoTime();
108     long reportPeriod = TimeUnit.SECONDS.toNanos(1);
109     int reports = 0;
110     double best = 0.0;
111 
112     // Run until we've printed enough reports.
113     while (reports < NUM_REPORTS) {
114       // Print a report if we haven't recently.
115       long now = System.nanoTime();
116       double reportDuration = now - reportStart;
117       if (reportDuration > reportPeriod) {
118         double requestsPerSecond = requestCount / reportDuration * TimeUnit.SECONDS.toNanos(1);
119         if (VERBOSE) {
120           System.out.println(String.format("Requests per second: %.1f", requestsPerSecond));
121         }
122         best = Math.max(best, requestsPerSecond);
123         requestCount = 0;
124         reportStart = now;
125         reports++;
126       }
127 
128       // Fill the job queue with work.
129       while (httpClient.acceptingJobs()) {
130         httpClient.enqueue(url);
131         requestCount++;
132       }
133 
134       // The job queue is full. Take a break.
135       sleep(1);
136     }
137 
138     return best;
139   }
140 
toString()141   @Override public String toString() {
142     List<Object> modifiers = new ArrayList<>();
143     if (tls) modifiers.add("tls");
144     if (gzip) modifiers.add("gzip");
145     if (chunked) modifiers.add("chunked");
146     modifiers.addAll(protocols);
147 
148     return String.format("%s %s\nbodyByteCount=%s headerCount=%s concurrencyLevel=%s",
149         client, modifiers, bodyByteCount, headerCount, concurrencyLevel);
150   }
151 
sleep(int millis)152   private void sleep(int millis) {
153     try {
154       Thread.sleep(millis);
155     } catch (InterruptedException ignored) {
156     }
157   }
158 
startServer()159   private MockWebServer startServer() throws IOException {
160     Logger.getLogger(MockWebServer.class.getName()).setLevel(Level.WARNING);
161     MockWebServer server = new MockWebServer();
162 
163     if (tls) {
164       SSLContext sslContext = SslContextBuilder.localhost();
165       server.useHttps(sslContext.getSocketFactory(), false);
166       server.setProtocols(protocols);
167     }
168 
169     final MockResponse response = newResponse();
170     server.setDispatcher(new Dispatcher() {
171       @Override public MockResponse dispatch(RecordedRequest request) {
172         return response;
173       }
174     });
175 
176     server.start();
177     return server;
178   }
179 
newResponse()180   private MockResponse newResponse() throws IOException {
181     byte[] bytes = new byte[bodyByteCount];
182     random.nextBytes(bytes);
183     Buffer body = new Buffer().write(bytes);
184 
185     MockResponse result = new MockResponse();
186 
187     if (gzip) {
188       Buffer gzipBody = new Buffer();
189       GzipSink gzipSink = new GzipSink(gzipBody);
190       gzipSink.write(body, body.size());
191       gzipSink.close();
192       body = gzipBody;
193       result.addHeader("Content-Encoding: gzip");
194     }
195 
196     if (chunked) {
197       result.setChunkedBody(body, 1024);
198     } else {
199       result.setBody(body);
200     }
201 
202     for (int i = 0; i < headerCount; i++) {
203       result.addHeader(randomString(12), randomString(20));
204     }
205 
206     return result;
207   }
208 
randomString(int length)209   private String randomString(int length) {
210     String alphabet = "-abcdefghijklmnopqrstuvwxyz";
211     char[] result = new char[length];
212     for (int i = 0; i < length; i++) {
213       result[i] = alphabet.charAt(random.nextInt(alphabet.length()));
214     }
215     return new String(result);
216   }
217 }
218