Home | History | Annotate | Download | only in benchmarks
      1 /*
      2  * Copyright (C) 2014 Square, Inc. and others.
      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 
     17 package com.squareup.okio.benchmarks;
     18 
     19 import java.io.EOFException;
     20 import java.io.File;
     21 import java.io.FileInputStream;
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.util.concurrent.TimeUnit;
     25 
     26 import org.openjdk.jmh.annotations.Benchmark;
     27 import org.openjdk.jmh.annotations.BenchmarkMode;
     28 import org.openjdk.jmh.annotations.Fork;
     29 import org.openjdk.jmh.annotations.Group;
     30 import org.openjdk.jmh.annotations.GroupThreads;
     31 import org.openjdk.jmh.annotations.Level;
     32 import org.openjdk.jmh.annotations.Measurement;
     33 import org.openjdk.jmh.annotations.Mode;
     34 import org.openjdk.jmh.annotations.OutputTimeUnit;
     35 import org.openjdk.jmh.annotations.Param;
     36 import org.openjdk.jmh.annotations.Scope;
     37 import org.openjdk.jmh.annotations.Setup;
     38 import org.openjdk.jmh.annotations.State;
     39 import org.openjdk.jmh.annotations.TearDown;
     40 import org.openjdk.jmh.annotations.Threads;
     41 import org.openjdk.jmh.annotations.Warmup;
     42 
     43 import okio.Buffer;
     44 import okio.BufferedSource;
     45 import okio.Okio;
     46 import okio.Sink;
     47 import okio.Timeout;
     48 
     49 import static java.util.Objects.requireNonNull;
     50 
     51 @Fork(1)
     52 @Warmup(iterations = 10, time = 10)
     53 @Measurement(iterations = 10, time = 10)
     54 @State(Scope.Benchmark)
     55 @BenchmarkMode(Mode.Throughput)
     56 @OutputTimeUnit(TimeUnit.SECONDS)
     57 public class BufferPerformanceBench {
     58 
     59   public static final File OriginPath =
     60       new File(System.getProperty("okio.bench.origin.path", "/dev/urandom"));
     61 
     62   /* Test Workload
     63    *
     64    * Each benchmark thread maintains three buffers; a receive buffer, a process buffer
     65    * and a send buffer. At every operation:
     66    *
     67    *   - We fill up the receive buffer using the origin, write the request to the process
     68    *     buffer, and consume the process buffer.
     69    *   - We fill up the process buffer using the origin, write the response to the send
     70    *     buffer, and consume the send buffer.
     71    *
     72    * We use an "origin" source that serves as a preexisting sequence of bytes we can read
     73    * from the file system. The request and response bytes are initialized in the beginning
     74    * and reused throughout the benchmark in order to eliminate GC effects.
     75    *
     76    * Typically, we simulate the usage of small reads and large writes. Requests and
     77    * responses are satisfied with precomputed buffers to eliminate GC effects on
     78    * results.
     79    *
     80    * There are two types of benchmark tests; hot tests are "pedal to the metal" and
     81    * use all CPU they can take. These are useful to magnify performance effects of
     82    * changes but are not realistic use cases that should drive optimization efforts.
     83    * Cold tests introduce think time between the receiving of the request and sending
     84    * of the response. They are more useful as a reasonably realistic workload where
     85    * buffers can be read from and written to during request/response handling but
     86    * may hide subtle effects of most changes on performance. Prefer to look at the cold
     87    * benchmarks first to decide if a bottleneck is worth pursuing, then use the hot
     88    * benchmarks to fine tune optimization efforts.
     89    *
     90    * Benchmark threads do not explicitly communicate between each other (except to sync
     91    * iterations as needed by JMH).
     92    *
     93    * We simulate think time for each benchmark thread by parking the thread for a
     94    * configurable number of microseconds (1000 by default).
     95    */
     96 
     97 
     98   @Benchmark
     99   @Threads(1)
    100   public void threads1hot(HotBuffers buffers) throws IOException {
    101     readWriteRecycle(buffers);
    102   }
    103 
    104   @Benchmark
    105   @Threads(2)
    106   public void threads2hot(HotBuffers buffers) throws IOException {
    107     readWriteRecycle(buffers);
    108   }
    109 
    110   @Benchmark
    111   @Threads(4)
    112   public void threads4hot(HotBuffers buffers) throws IOException {
    113     readWriteRecycle(buffers);
    114   }
    115 
    116   @Benchmark
    117   @Threads(8)
    118   public void threads8hot(HotBuffers buffers) throws IOException {
    119     readWriteRecycle(buffers);
    120   }
    121 
    122   @Benchmark
    123   @Threads(16)
    124   public void threads16hot(HotBuffers buffers) throws IOException {
    125     readWriteRecycle(buffers);
    126   }
    127 
    128   @Benchmark
    129   @Threads(32)
    130   public void threads32hot(HotBuffers buffers) throws IOException {
    131     readWriteRecycle(buffers);
    132   }
    133 
    134   @Benchmark
    135   @GroupThreads(1)
    136   @Group("cold")
    137   public void thinkReadHot(HotBuffers buffers) throws IOException {
    138     buffers.receive(requestBytes).readAll(NullSink);
    139   }
    140 
    141   @Benchmark
    142   @GroupThreads(3)
    143   @Group("cold")
    144   public void thinkWriteCold(ColdBuffers buffers) throws IOException {
    145     buffers.transmit(responseBytes).readAll(NullSink);
    146   }
    147 
    148   private void readWriteRecycle(HotBuffers buffers) throws IOException {
    149     buffers.receive(requestBytes).readAll(NullSink);
    150     buffers.transmit(responseBytes).readAll(NullSink);
    151   }
    152 
    153   @Param({ "1000" })
    154   int maxThinkMicros = 1000;
    155 
    156   @Param({ "1024" })
    157   int maxReadBytes = 1024;
    158 
    159   @Param({ "1024" })
    160   int maxWriteBytes = 1024;
    161 
    162   @Param({ "2048" })
    163   int requestSize = 2048;
    164 
    165   @Param({ "1" })
    166   int responseFactor = 1;
    167 
    168   byte[] requestBytes;
    169 
    170   byte[] responseBytes;
    171 
    172   @Setup(Level.Trial)
    173   public void storeRequestResponseData() throws IOException {
    174     checkOrigin(OriginPath);
    175 
    176     requestBytes = storeSourceData(new byte[requestSize]);
    177     responseBytes = storeSourceData(new byte[requestSize * responseFactor]);
    178   }
    179 
    180   private byte[] storeSourceData(byte[] dest) throws IOException {
    181     requireNonNull(dest, "dest == null");
    182     try (BufferedSource source = Okio.buffer(Okio.source(OriginPath))) {
    183       source.readFully(dest);
    184     }
    185     return dest;
    186   }
    187 
    188   private void checkOrigin(File path) throws IOException {
    189     requireNonNull(path, "path == null");
    190 
    191     if (!path.canRead()) {
    192       throw new IllegalArgumentException("can not access: " + path);
    193     }
    194 
    195     try (InputStream in = new FileInputStream(path)) {
    196       int available = in.read();
    197       if (available < 0) {
    198         throw new IllegalArgumentException("can not read: " + path);
    199       }
    200     }
    201   }
    202 
    203   /*
    204    * The state class hierarchy is larger than it needs to be due to a JMH
    205    * issue where states inheriting setup methods depending on another state
    206    * do not get initialized correctly from benchmark methods making use
    207    * of groups. To work around, we leave the common setup and teardown code
    208    * in superclasses and move the setup method depending on the bench state
    209    * to subclasses. Without the workaround, it would have been enough for
    210    * `ColdBuffers` to inherit from `HotBuffers`.
    211    */
    212 
    213   @State(Scope.Thread)
    214   public static class ColdBuffers extends BufferSetup {
    215 
    216     @Setup(Level.Trial)
    217     public void setupBench(BufferPerformanceBench bench) {
    218       super.bench = bench;
    219     }
    220 
    221     @Setup(Level.Invocation)
    222     public void lag() throws InterruptedException {
    223       TimeUnit.MICROSECONDS.sleep(bench.maxThinkMicros);
    224     }
    225 
    226   }
    227 
    228   @State(Scope.Thread)
    229   public static class HotBuffers extends BufferSetup {
    230 
    231     @Setup(Level.Trial)
    232     public void setupBench(BufferPerformanceBench bench) {
    233       super.bench = bench;
    234     }
    235 
    236   }
    237 
    238   @State(Scope.Thread)
    239   public static abstract class BufferSetup extends BufferState {
    240     BufferPerformanceBench bench;
    241 
    242     public BufferedSource receive(byte[] bytes) throws IOException {
    243       return super.receive(bytes, bench.maxReadBytes);
    244     }
    245 
    246     public BufferedSource transmit(byte[] bytes) throws IOException {
    247       return super.transmit(bytes, bench.maxWriteBytes);
    248     }
    249 
    250     @TearDown
    251     public void dispose() throws IOException {
    252       releaseBuffers();
    253     }
    254 
    255   }
    256 
    257   public static class BufferState {
    258 
    259     @SuppressWarnings("resource")
    260     final Buffer received = new Buffer();
    261     @SuppressWarnings("resource")
    262     final Buffer sent = new Buffer();
    263     @SuppressWarnings("resource")
    264     final Buffer process = new Buffer();
    265 
    266     public void releaseBuffers() throws IOException {
    267       received.clear();
    268       sent.clear();
    269       process.clear();
    270     }
    271 
    272     /**
    273      * Fills up the receive buffer, hands off to process buffer and returns it for consuming.
    274      * Expects receive and process buffers to be empty. Leaves the receive buffer empty and
    275      * process buffer full.
    276      */
    277     protected Buffer receive(byte[] bytes, int maxChunkSize) throws IOException {
    278       writeChunked(received, bytes, maxChunkSize).readAll(process);
    279       return process;
    280     }
    281 
    282     /**
    283      * Fills up the process buffer, hands off to send buffer and returns it for consuming.
    284      * Expects process and sent buffers to be empty. Leaves the process buffer empty and
    285      * sent buffer full.
    286      */
    287     protected BufferedSource transmit(byte[] bytes, int maxChunkSize) throws IOException {
    288       writeChunked(process, bytes, maxChunkSize).readAll(sent);
    289       return sent;
    290     }
    291 
    292     private BufferedSource writeChunked(Buffer buffer, byte[] bytes, final int chunkSize) {
    293       int remaining = bytes.length;
    294       int offset = 0;
    295       while (remaining > 0) {
    296         int bytesToWrite = Math.min(remaining, chunkSize);
    297         buffer.write(bytes, offset, bytesToWrite);
    298         remaining -= bytesToWrite;
    299         offset += bytesToWrite;
    300       }
    301       return buffer;
    302     }
    303 
    304   }
    305 
    306   @SuppressWarnings("resource")
    307   private static final Sink NullSink = new Sink() {
    308 
    309     @Override public void write(Buffer source, long byteCount) throws EOFException {
    310       source.skip(byteCount);
    311     }
    312 
    313     @Override public void flush() {
    314       // nothing
    315     }
    316 
    317     @Override public Timeout timeout() {
    318       return Timeout.NONE;
    319     }
    320 
    321     @Override public void close() {
    322       // nothing
    323     }
    324 
    325     @Override public String toString() {
    326       return "NullSink{}";
    327     }
    328   };
    329 
    330 }
    331