diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/benchmark/Http1BufferSizeBenchmark.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/benchmark/Http1BufferSizeBenchmark.java new file mode 100644 index 000000000..708b1d0be --- /dev/null +++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/benchmark/Http1BufferSizeBenchmark.java @@ -0,0 +1,161 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.benchmark; + +import java.net.InetSocketAddress; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; +import org.apache.hc.core5.http.nio.AsyncRequestConsumer; +import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; +import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; +import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.BasicResponseProducer; +import org.apache.hc.core5.http.nio.support.BasicServerExchangeHandler; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.testing.nio.Http1TestServer; +import org.apache.hc.core5.util.Timeout; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +/** + * End-to-end HTTP/1.1 throughput of a large-body GET over loopback, as a function of the + * {@link Http1Config} session buffer size. Both the requester and the server are configured with + * the same buffer size, so this isolates the effect of the buffer default on large messages. + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS) +// A fixed, generous heap keeps GC out of the measurement: at ~6 K x 1 MB/s the transient garbage +// otherwise triggers frequent collections whose pauses dominate the run-to-run variance. +@Fork(value = 1, jvmArgs = {"-Xms4g", "-Xmx4g"}) +// Client and server run in the same JVM, so keep concurrency at or below the available cores to +// avoid oversubscription noise; override with JMH -t on larger machines. +@Threads(8) +@State(Scope.Benchmark) +public class Http1BufferSizeBenchmark { + + @Param({"8192", "16384", "32768", "65536"}) + public int bufferSize; + + @Param({"1048576"}) + public int bodySize; + + private static final Timeout TIMEOUT = Timeout.ofSeconds(60); + + private Http1TestServer server; + private HttpAsyncRequester requester; + private HttpHost target; + + @Setup(Level.Trial) + public void setUp() throws Exception { + final Http1Config h1Config = Http1Config.custom().setBufferSize(bufferSize).build(); + final byte[] body = new byte[bodySize]; + + server = new Http1TestServer(); + server.configure(h1Config); + server.register("*", () -> new BasicServerExchangeHandler<>(new AsyncServerRequestHandler>() { + + @Override + public AsyncRequestConsumer> prepare( + final HttpRequest request, final EntityDetails entityDetails, final HttpContext context) { + return new BasicRequestConsumer<>(entityDetails != null ? new DiscardingEntityConsumer<>() : null); + } + + @Override + public void handle( + final Message message, final ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, java.io.IOException { + responseTrigger.submitResponse( + new BasicResponseProducer(HttpStatus.SC_OK, + AsyncEntityProducers.create(body, ContentType.APPLICATION_OCTET_STREAM)), + context); + } + })); + final InetSocketAddress address = server.start(); + + requester = AsyncRequesterBootstrap.bootstrap() + .setIOReactorConfig(IOReactorConfig.custom().setSoTimeout(TIMEOUT).build()) + .setHttp1Config(h1Config) + .setMaxTotal(300) + .setDefaultMaxPerRoute(300) + .create(); + requester.start(); + target = new HttpHost("http", "localhost", address.getPort()); + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + if (requester != null) { + requester.close(CloseMode.GRACEFUL); + } + if (server != null) { + server.close(); + } + } + + @Benchmark + public long getLargeBody() throws Exception { + final Future> future = requester.execute( + new BasicRequestProducer(Method.GET, target, "/"), + new BasicResponseConsumer<>(new DiscardingEntityConsumer()), + TIMEOUT, null); + final Message message = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + return message.head().getCode(); + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/config/Http1Config.java b/httpcore5/src/main/java/org/apache/hc/core5/http/config/Http1Config.java index ffddb5222..b75735912 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/config/Http1Config.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/config/Http1Config.java @@ -141,7 +141,9 @@ public static Http1Config.Builder copy(final Http1Config config) { } private static final int INIT_WINDOW_SIZE = 65535; - private static final int INIT_BUF_SIZE = 8192; + // Larger session buffers cut the number of read/write events on large messages; the gain + // levels off past 32 KB while per-connection footprint keeps growing, so 32 KB is the default. + private static final int INIT_BUF_SIZE = 32 * 1024; private static final Timeout INIT_WAIT_FOR_CONTINUE = Timeout.ofSeconds(3); private static final int INIT_BUF_CHUNK = -1; private static final int INIT_MAX_HEADER_COUNT = 100;