diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/Http1xServerResponse.java b/vertx-core/src/main/java/io/vertx/core/http/impl/Http1xServerResponse.java index 649d2c9407b..3daa202fba8 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/Http1xServerResponse.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/Http1xServerResponse.java @@ -612,8 +612,7 @@ private void prepareHeaders(long contentLength) { } else { // Set content-length header automatically if (contentLength >= 0 && !headers.contains(HttpHeaders.CONTENT_LENGTH) && !headers.contains(HttpHeaders.TRANSFER_ENCODING)) { - String value = contentLength == 0 ? "0" : String.valueOf(contentLength); - headers.set(HttpHeaders.CONTENT_LENGTH, value); + headers.set(HttpHeaders.CONTENT_LENGTH, HttpUtils.positiveLongToString(contentLength)); } } if (headersEndHandler != null) { diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/Http2ServerResponse.java b/vertx-core/src/main/java/io/vertx/core/http/impl/Http2ServerResponse.java index f2cc228470e..7e687cef3a3 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/Http2ServerResponse.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/Http2ServerResponse.java @@ -397,7 +397,7 @@ Future write(ByteBuf chunk, boolean end) { chunk = Unpooled.EMPTY_BUFFER; } if (end && !headWritten && needsContentLengthHeader()) { - headers().set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(chunk.readableBytes())); + headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpUtils.positiveLongToString(chunk.readableBytes())); } boolean sent = checkSendHeaders(end && !hasBody && trailers == null, !hasBody); if (hasBody || (!sent && end)) { @@ -545,7 +545,7 @@ public Future sendFile(String filename, long offset, long length) { long contentLength = Math.min(length, fileLength); // fail early before status code/headers are written to the response if (headers.get(HttpHeaderNames.CONTENT_LENGTH) == null) { - putHeader(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(contentLength)); + putHeader(HttpHeaderNames.CONTENT_LENGTH, HttpUtils.positiveLongToString(contentLength)); } if (headers.get(HttpHeaderNames.CONTENT_TYPE) == null) { String contentType = MimeMapping.mimeTypeForFilename(filename); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java index c163e87d9bd..62660b3d8f5 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java @@ -435,7 +435,7 @@ private boolean requiresContentLength() { private Future write(ByteBuf buff, boolean end) { if (end) { if (buff != null && requiresContentLength()) { - headers().set(CONTENT_LENGTH, String.valueOf(buff.readableBytes())); + headers().set(CONTENT_LENGTH, HttpUtils.positiveLongToString(buff.readableBytes())); } } else if (requiresContentLength()) { throw new IllegalStateException("You must set the Content-Length header to be the total size of the message " diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java index 26d0760f43e..811eeda63e3 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java @@ -837,4 +837,28 @@ public static HostAndPort socketAddressToHostAndPort(SocketAddress socketAddress } return null; } + + private static final String[] SMALL_POSITIVE_LONGS = new String[256]; + + /** + * This try hard to cache the first 256 positive longs as strings [0, 255] to avoid the cost of creating a new + * string for each of them.
+ * The size/capacity of the cache is subject to change but this method is expected to be used for hot and frequent code paths. + */ + public static String positiveLongToString(long value) { + if (value < 0) { + throw new IllegalArgumentException("contentLength must be >= 0"); + } + if (value >= SMALL_POSITIVE_LONGS.length) { + return Long.toString(value); + } + final int index = (int) value; + String str = SMALL_POSITIVE_LONGS[index]; + if (str == null) { + // it's ok to be racy here, String is immutable hence it benefits from safe publication! + str = Long.toString(value); + SMALL_POSITIVE_LONGS[index] = str; + } + return str; + } } diff --git a/vertx-core/src/test/java/io/vertx/benchmarks/ContentLengthToString.java b/vertx-core/src/test/java/io/vertx/benchmarks/ContentLengthToString.java new file mode 100644 index 00000000000..d30e609c281 --- /dev/null +++ b/vertx-core/src/test/java/io/vertx/benchmarks/ContentLengthToString.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2011-2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + +package io.vertx.benchmarks; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.Random; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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.Warmup; +import org.openjdk.jmh.infra.BenchmarkParams; +import org.openjdk.jmh.infra.Blackhole; + +import io.vertx.core.http.impl.HttpUtils; + +@Warmup(iterations = 10, time = 1, timeUnit = SECONDS) +@Measurement(iterations = 10, time = 200, timeUnit = MILLISECONDS) +@Fork(value = 2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(java.util.concurrent.TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class ContentLengthToString { + + @Param({"255", "511", "1023" }) + public int maxContentLength; + private int[] contentLengthIndexes; + private long inputSequence; + + @Setup + public void init(Blackhole bh, BenchmarkParams params) { + final int MAX_CONTENT_LENGTH_SIZE = 1024; + if (maxContentLength >= MAX_CONTENT_LENGTH_SIZE) { + throw new IllegalArgumentException("maxContentLength must be < " + MAX_CONTENT_LENGTH_SIZE); + } + contentLengthIndexes = new int[128 * MAX_CONTENT_LENGTH_SIZE]; + if (Integer.bitCount(contentLengthIndexes.length) != 1) { + throw new IllegalArgumentException("contentLengthIndexes must be a power of 2"); + } + Random rnd = new Random(42); + for (int i = 0; i < contentLengthIndexes.length; i++) { + contentLengthIndexes[i] = rnd.nextInt(maxContentLength); + } + } + + private long nextContentLength() { + int[] contentLengthIndexes = this.contentLengthIndexes; + int nextInputIndex = (int) inputSequence & (contentLengthIndexes.length - 1); + int contentLength = contentLengthIndexes[nextInputIndex]; + inputSequence++; + return contentLength; + } + + @Benchmark + public String contentLengthToString() { + return String.valueOf(nextContentLength()); + } + + @Benchmark + public String contentLengthHttpUtils() { + return HttpUtils.positiveLongToString(nextContentLength()); + } + +}