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