Skip to content

Commit 76a9f52

Browse files
franz1981vietj
authored andcommitted
Small content-length values caching
1 parent fc56a32 commit 76a9f52

File tree

5 files changed

+110
-5
lines changed

5 files changed

+110
-5
lines changed

src/main/java/io/vertx/core/http/impl/Http1xServerResponse.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,7 @@ private void prepareHeaders(long contentLength) {
737737
} else {
738738
// Set content-length header automatically
739739
if (contentLength >= 0 && !headers.contains(HttpHeaders.CONTENT_LENGTH) && !headers.contains(HttpHeaders.TRANSFER_ENCODING)) {
740-
String value = contentLength == 0 ? "0" : String.valueOf(contentLength);
741-
headers.set(HttpHeaders.CONTENT_LENGTH, value);
740+
headers.set(HttpHeaders.CONTENT_LENGTH, HttpUtils.positiveLongToString(contentLength));
742741
}
743742
}
744743
if (headersEndHandler != null) {

src/main/java/io/vertx/core/http/impl/Http2ServerResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ void write(ByteBuf chunk, boolean end, Handler<AsyncResult<Void>> handler) {
462462
chunk = Unpooled.EMPTY_BUFFER;
463463
}
464464
if (end && !headWritten && needsContentLengthHeader()) {
465-
headers().set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(chunk.readableBytes()));
465+
headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpUtils.positiveLongToString(chunk.readableBytes()));
466466
}
467467
boolean sent = checkSendHeaders(end && !hasBody && trailers == null, !hasBody);
468468
if (hasBody || (!sent && end)) {
@@ -631,7 +631,7 @@ public HttpServerResponse sendFile(String filename, long offset, long length, Ha
631631
long fileLength = file.getReadLength();
632632
long contentLength = Math.min(length, fileLength);
633633
if (headers.get(HttpHeaderNames.CONTENT_LENGTH) == null) {
634-
putHeader(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(contentLength));
634+
putHeader(HttpHeaderNames.CONTENT_LENGTH, HttpUtils.positiveLongToString(contentLength));
635635
}
636636
if (headers.get(HttpHeaderNames.CONTENT_TYPE) == null) {
637637
String contentType = MimeMapping.getMimeTypeForFilename(filename);

src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ private boolean requiresContentLength() {
483483
private void write(ByteBuf buff, boolean end, Handler<AsyncResult<Void>> completionHandler) {
484484
if (end) {
485485
if (buff != null && requiresContentLength()) {
486-
headers().set(CONTENT_LENGTH, String.valueOf(buff.readableBytes()));
486+
headers().set(CONTENT_LENGTH, HttpUtils.positiveLongToString(buff.readableBytes()));
487487
}
488488
} else if (requiresContentLength()) {
489489
throw new IllegalStateException("You must set the Content-Length header to be the total size of the message "

src/main/java/io/vertx/core/http/impl/HttpUtils.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,30 @@ private static boolean matches(CharSequence path, int start, String what, boolea
237237
return false;
238238
}
239239

240+
private static final String[] SMALL_POSITIVE_LONGS = new String[256];
241+
242+
/**
243+
* This try hard to cache the first 256 positive longs as strings [0, 255] to avoid the cost of creating a new
244+
* string for each of them.<br>
245+
* The size/capacity of the cache is subject to change but this method is expected to be used for hot and frequent code paths.
246+
*/
247+
public static String positiveLongToString(long value) {
248+
if (value < 0) {
249+
throw new IllegalArgumentException("contentLength must be >= 0");
250+
}
251+
if (value >= SMALL_POSITIVE_LONGS.length) {
252+
return Long.toString(value);
253+
}
254+
final int index = (int) value;
255+
String str = SMALL_POSITIVE_LONGS[index];
256+
if (str == null) {
257+
// it's ok to be racy here, String is immutable hence it benefits from safe publication!
258+
str = Long.toString(value);
259+
SMALL_POSITIVE_LONGS[index] = str;
260+
}
261+
return str;
262+
}
263+
240264
/**
241265
* Normalizes a path as per <a href="http://tools.ietf.org/html/rfc3986#section-5.2.4>rfc3986</a>.
242266
*
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.benchmarks;
13+
14+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
15+
import static java.util.concurrent.TimeUnit.SECONDS;
16+
17+
import java.util.Random;
18+
19+
import org.openjdk.jmh.annotations.Benchmark;
20+
import org.openjdk.jmh.annotations.BenchmarkMode;
21+
import org.openjdk.jmh.annotations.Fork;
22+
import org.openjdk.jmh.annotations.Measurement;
23+
import org.openjdk.jmh.annotations.Mode;
24+
import org.openjdk.jmh.annotations.OutputTimeUnit;
25+
import org.openjdk.jmh.annotations.Param;
26+
import org.openjdk.jmh.annotations.Scope;
27+
import org.openjdk.jmh.annotations.Setup;
28+
import org.openjdk.jmh.annotations.State;
29+
import org.openjdk.jmh.annotations.Warmup;
30+
import org.openjdk.jmh.infra.BenchmarkParams;
31+
import org.openjdk.jmh.infra.Blackhole;
32+
33+
import io.vertx.core.http.impl.HttpUtils;
34+
35+
@Warmup(iterations = 10, time = 1, timeUnit = SECONDS)
36+
@Measurement(iterations = 10, time = 200, timeUnit = MILLISECONDS)
37+
@Fork(value = 2)
38+
@BenchmarkMode(Mode.AverageTime)
39+
@OutputTimeUnit(java.util.concurrent.TimeUnit.NANOSECONDS)
40+
@State(Scope.Benchmark)
41+
public class ContentLengthToString {
42+
43+
@Param({"255", "511", "1023" })
44+
public int maxContentLength;
45+
private int[] contentLengthIndexes;
46+
private long inputSequence;
47+
48+
@Setup
49+
public void init(Blackhole bh, BenchmarkParams params) {
50+
final int MAX_CONTENT_LENGTH_SIZE = 1024;
51+
if (maxContentLength >= MAX_CONTENT_LENGTH_SIZE) {
52+
throw new IllegalArgumentException("maxContentLength must be < " + MAX_CONTENT_LENGTH_SIZE);
53+
}
54+
contentLengthIndexes = new int[128 * MAX_CONTENT_LENGTH_SIZE];
55+
if (Integer.bitCount(contentLengthIndexes.length) != 1) {
56+
throw new IllegalArgumentException("contentLengthIndexes must be a power of 2");
57+
}
58+
Random rnd = new Random(42);
59+
for (int i = 0; i < contentLengthIndexes.length; i++) {
60+
contentLengthIndexes[i] = rnd.nextInt(maxContentLength);
61+
}
62+
}
63+
64+
private long nextContentLength() {
65+
int[] contentLengthIndexes = this.contentLengthIndexes;
66+
int nextInputIndex = (int) inputSequence & (contentLengthIndexes.length - 1);
67+
int contentLength = contentLengthIndexes[nextInputIndex];
68+
inputSequence++;
69+
return contentLength;
70+
}
71+
72+
@Benchmark
73+
public String contentLengthToString() {
74+
return String.valueOf(nextContentLength());
75+
}
76+
77+
@Benchmark
78+
public String contentLengthHttpUtils() {
79+
return HttpUtils.positiveLongToString(nextContentLength());
80+
}
81+
82+
}

0 commit comments

Comments
 (0)