| /* |
| * Copyright (C) 2014 Square, Inc. and others. |
| * |
| * 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.okio.benchmarks; |
| |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.openjdk.jmh.annotations.Benchmark; |
| import org.openjdk.jmh.annotations.BenchmarkMode; |
| import org.openjdk.jmh.annotations.Fork; |
| import org.openjdk.jmh.annotations.Group; |
| import org.openjdk.jmh.annotations.GroupThreads; |
| import org.openjdk.jmh.annotations.Level; |
| import org.openjdk.jmh.annotations.Measurement; |
| import org.openjdk.jmh.annotations.Mode; |
| import org.openjdk.jmh.annotations.OutputTimeUnit; |
| import org.openjdk.jmh.annotations.Param; |
| import org.openjdk.jmh.annotations.Scope; |
| import org.openjdk.jmh.annotations.Setup; |
| import org.openjdk.jmh.annotations.State; |
| import org.openjdk.jmh.annotations.TearDown; |
| import org.openjdk.jmh.annotations.Threads; |
| import org.openjdk.jmh.annotations.Warmup; |
| |
| import okio.Buffer; |
| import okio.BufferedSource; |
| import okio.Okio; |
| import okio.Sink; |
| import okio.Timeout; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| @Fork(1) |
| @Warmup(iterations = 10, time = 10) |
| @Measurement(iterations = 10, time = 10) |
| @State(Scope.Benchmark) |
| @BenchmarkMode(Mode.Throughput) |
| @OutputTimeUnit(TimeUnit.SECONDS) |
| public class BufferPerformanceBenchmark { |
| |
| public static final File OriginPath = |
| new File(System.getProperty("okio.bench.origin.path", "/dev/urandom")); |
| |
| /* Test Workload |
| * |
| * Each benchmark thread maintains three buffers; a receive buffer, a process buffer |
| * and a send buffer. At every operation: |
| * |
| * - We fill up the receive buffer using the origin, write the request to the process |
| * buffer, and consume the process buffer. |
| * - We fill up the process buffer using the origin, write the response to the send |
| * buffer, and consume the send buffer. |
| * |
| * We use an "origin" source that serves as a preexisting sequence of bytes we can read |
| * from the file system. The request and response bytes are initialized in the beginning |
| * and reused throughout the benchmark in order to eliminate GC effects. |
| * |
| * Typically, we simulate the usage of small reads and large writes. Requests and |
| * responses are satisfied with precomputed buffers to eliminate GC effects on |
| * results. |
| * |
| * There are two types of benchmark tests; hot tests are "pedal to the metal" and |
| * use all CPU they can take. These are useful to magnify performance effects of |
| * changes but are not realistic use cases that should drive optimization efforts. |
| * Cold tests introduce think time between the receiving of the request and sending |
| * of the response. They are more useful as a reasonably realistic workload where |
| * buffers can be read from and written to during request/response handling but |
| * may hide subtle effects of most changes on performance. Prefer to look at the cold |
| * benchmarks first to decide if a bottleneck is worth pursuing, then use the hot |
| * benchmarks to fine tune optimization efforts. |
| * |
| * Benchmark threads do not explicitly communicate between each other (except to sync |
| * iterations as needed by JMH). |
| * |
| * We simulate think time for each benchmark thread by parking the thread for a |
| * configurable number of microseconds (1000 by default). |
| */ |
| |
| |
| @Benchmark |
| @Threads(1) |
| public void threads1hot(HotBuffers buffers) throws IOException { |
| readWriteRecycle(buffers); |
| } |
| |
| @Benchmark |
| @Threads(2) |
| public void threads2hot(HotBuffers buffers) throws IOException { |
| readWriteRecycle(buffers); |
| } |
| |
| @Benchmark |
| @Threads(4) |
| public void threads4hot(HotBuffers buffers) throws IOException { |
| readWriteRecycle(buffers); |
| } |
| |
| @Benchmark |
| @Threads(8) |
| public void threads8hot(HotBuffers buffers) throws IOException { |
| readWriteRecycle(buffers); |
| } |
| |
| @Benchmark |
| @Threads(16) |
| public void threads16hot(HotBuffers buffers) throws IOException { |
| readWriteRecycle(buffers); |
| } |
| |
| @Benchmark |
| @Threads(32) |
| public void threads32hot(HotBuffers buffers) throws IOException { |
| readWriteRecycle(buffers); |
| } |
| |
| @Benchmark |
| @GroupThreads(1) |
| @Group("cold") |
| public void thinkReadHot(HotBuffers buffers) throws IOException { |
| buffers.receive(requestBytes).readAll(NullSink); |
| } |
| |
| @Benchmark |
| @GroupThreads(3) |
| @Group("cold") |
| public void thinkWriteCold(ColdBuffers buffers) throws IOException { |
| buffers.transmit(responseBytes).readAll(NullSink); |
| } |
| |
| private void readWriteRecycle(HotBuffers buffers) throws IOException { |
| buffers.receive(requestBytes).readAll(NullSink); |
| buffers.transmit(responseBytes).readAll(NullSink); |
| } |
| |
| @Param({ "1000" }) |
| int maxThinkMicros = 1000; |
| |
| @Param({ "1024" }) |
| int maxReadBytes = 1024; |
| |
| @Param({ "1024" }) |
| int maxWriteBytes = 1024; |
| |
| @Param({ "2048" }) |
| int requestSize = 2048; |
| |
| @Param({ "1" }) |
| int responseFactor = 1; |
| |
| byte[] requestBytes; |
| |
| byte[] responseBytes; |
| |
| @Setup(Level.Trial) |
| public void storeRequestResponseData() throws IOException { |
| checkOrigin(OriginPath); |
| |
| requestBytes = storeSourceData(new byte[requestSize]); |
| responseBytes = storeSourceData(new byte[requestSize * responseFactor]); |
| } |
| |
| private byte[] storeSourceData(byte[] dest) throws IOException { |
| requireNonNull(dest, "dest == null"); |
| try (BufferedSource source = Okio.buffer(Okio.source(OriginPath))) { |
| source.readFully(dest); |
| } |
| return dest; |
| } |
| |
| private void checkOrigin(File path) throws IOException { |
| requireNonNull(path, "path == null"); |
| |
| if (!path.canRead()) { |
| throw new IllegalArgumentException("can not access: " + path); |
| } |
| |
| try (InputStream in = new FileInputStream(path)) { |
| int available = in.read(); |
| if (available < 0) { |
| throw new IllegalArgumentException("can not read: " + path); |
| } |
| } |
| } |
| |
| /* |
| * The state class hierarchy is larger than it needs to be due to a JMH |
| * issue where states inheriting setup methods depending on another state |
| * do not get initialized correctly from benchmark methods making use |
| * of groups. To work around, we leave the common setup and teardown code |
| * in superclasses and move the setup method depending on the bench state |
| * to subclasses. Without the workaround, it would have been enough for |
| * `ColdBuffers` to inherit from `HotBuffers`. |
| */ |
| |
| @State(Scope.Thread) |
| public static class ColdBuffers extends BufferSetup { |
| |
| @Setup(Level.Trial) |
| public void setupBench(BufferPerformanceBenchmark bench) { |
| super.bench = bench; |
| } |
| |
| @Setup(Level.Invocation) |
| public void lag() throws InterruptedException { |
| TimeUnit.MICROSECONDS.sleep(bench.maxThinkMicros); |
| } |
| |
| } |
| |
| @State(Scope.Thread) |
| public static class HotBuffers extends BufferSetup { |
| |
| @Setup(Level.Trial) |
| public void setupBench(BufferPerformanceBenchmark bench) { |
| super.bench = bench; |
| } |
| |
| } |
| |
| @State(Scope.Thread) |
| public abstract static class BufferSetup extends BufferState { |
| BufferPerformanceBenchmark bench; |
| |
| public BufferedSource receive(byte[] bytes) throws IOException { |
| return super.receive(bytes, bench.maxReadBytes); |
| } |
| |
| public BufferedSource transmit(byte[] bytes) throws IOException { |
| return super.transmit(bytes, bench.maxWriteBytes); |
| } |
| |
| @TearDown |
| public void dispose() throws IOException { |
| releaseBuffers(); |
| } |
| |
| } |
| |
| public static class BufferState { |
| |
| @SuppressWarnings("resource") |
| final Buffer received = new Buffer(); |
| @SuppressWarnings("resource") |
| final Buffer sent = new Buffer(); |
| @SuppressWarnings("resource") |
| final Buffer process = new Buffer(); |
| |
| public void releaseBuffers() throws IOException { |
| received.clear(); |
| sent.clear(); |
| process.clear(); |
| } |
| |
| /** |
| * Fills up the receive buffer, hands off to process buffer and returns it for consuming. |
| * Expects receive and process buffers to be empty. Leaves the receive buffer empty and |
| * process buffer full. |
| */ |
| protected Buffer receive(byte[] bytes, int maxChunkSize) throws IOException { |
| writeChunked(received, bytes, maxChunkSize).readAll(process); |
| return process; |
| } |
| |
| /** |
| * Fills up the process buffer, hands off to send buffer and returns it for consuming. |
| * Expects process and sent buffers to be empty. Leaves the process buffer empty and |
| * sent buffer full. |
| */ |
| protected BufferedSource transmit(byte[] bytes, int maxChunkSize) throws IOException { |
| writeChunked(process, bytes, maxChunkSize).readAll(sent); |
| return sent; |
| } |
| |
| private BufferedSource writeChunked(Buffer buffer, byte[] bytes, final int chunkSize) { |
| int remaining = bytes.length; |
| int offset = 0; |
| while (remaining > 0) { |
| int bytesToWrite = Math.min(remaining, chunkSize); |
| buffer.write(bytes, offset, bytesToWrite); |
| remaining -= bytesToWrite; |
| offset += bytesToWrite; |
| } |
| return buffer; |
| } |
| |
| } |
| |
| @SuppressWarnings("resource") |
| private static final Sink NullSink = new Sink() { |
| |
| @Override public void write(Buffer source, long byteCount) throws EOFException { |
| source.skip(byteCount); |
| } |
| |
| @Override public void flush() { |
| // nothing |
| } |
| |
| @Override public Timeout timeout() { |
| return Timeout.NONE; |
| } |
| |
| @Override public void close() { |
| // nothing |
| } |
| |
| @Override public String toString() { |
| return "NullSink{}"; |
| } |
| }; |
| |
| } |