Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* 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 org.asynchttpclient.bench;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
import io.netty.handler.codec.http2.Http2HeadersEncoder;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.util.AsciiString;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

import java.util.concurrent.TimeUnit;

/**
* Measures the HPACK-encoded wire size of {@code accept-encoding} for the two value spellings:
* <ul>
* <li>AHC current: {@code "gzip,deflate"} (no space) — built in
* {@code HttpUtils.GZIP_DEFLATE = new AsciiString(GZIP + "," + DEFLATE)}.</li>
* <li>HPACK static table entry #16: {@code "gzip, deflate"} (with space, RFC 7541 App. A).</li>
* </ul>
*
* <p>On a fresh encoder (first request of a connection) the static-table value matches as a single
* indexed byte; the non-matching spelling is literal-encoded and inserted into the dynamic table.
* This bench reports {@code gc.alloc.rate.norm} and the encoded byte count via the returned buffer's
* readableBytes (consumed by the blackhole through the return value size).
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class AcceptEncodingHpackBenchmark {

private static final AsciiString ACCEPT_ENCODING = AsciiString.cached("accept-encoding");
private static final AsciiString AHC_VALUE = AsciiString.cached("gzip,deflate");
private static final AsciiString STATIC_VALUE = AsciiString.cached("gzip, deflate");

private int encodeOnce(AsciiString value) throws Exception {
// Fresh encoder per call == "first request on a new connection" worst case.
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder();
Http2Headers headers = new DefaultHttp2Headers().add(ACCEPT_ENCODING, value);
ByteBuf out = Unpooled.buffer();
try {
encoder.encodeHeaders(3, headers, out);
return out.readableBytes();
} finally {
out.release();
}
}

@Benchmark
public int ahc_no_space() throws Exception {
return encodeOnce(AHC_VALUE);
}

@Benchmark
public int static_table_with_space() throws Exception {
return encodeOnce(STATIC_VALUE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* 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 org.asynchttpclient.bench;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.TimeUnit;

/**
* Models the per-checkout allocation of {@code DefaultChannelPool}: each
* {@code offer()} wraps the channel in a freshly allocated {@code IdleChannel}
* holder that is pushed onto a {@code ConcurrentLinkedDeque} (which itself
* allocates a linked node per insert). On {@code poll()} the holder is
* discarded. Under keep-alive churn this is one IdleChannel + one CLD node per
* request.
*
* This bench compares the current "allocate a holder per offer" pattern against
* an alternative that stores the bare channel reference + a parallel timestamp,
* avoiding the holder allocation. It is a standalone model (no Netty Channel
* needed) so it can run on the bare JMH classpath; the shapes mirror
* DefaultChannelPool.IdleChannel exactly (one Object ref + one long + one
* volatile int) and CLD node churn is identical for both arms because both push
* one element.
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ChannelPoolCheckoutBenchmark {

// Mirror of DefaultChannelPool.IdleChannel: Object ref + long start + volatile int owned.
static final class IdleChannel {
static final AtomicIntegerFieldUpdater<IdleChannel> OWNED =
AtomicIntegerFieldUpdater.newUpdater(IdleChannel.class, "owned");
final Object channel;
final long start;
@SuppressWarnings("unused")
private volatile int owned;

IdleChannel(Object channel, long start) {
this.channel = channel;
this.start = start;
}

boolean takeOwnership() {
return OWNED.getAndSet(this, 1) == 0;
}
}

private ConcurrentLinkedDeque<IdleChannel> currentDeque;
private ConcurrentLinkedDeque<Object> bareDeque;
private Object channel;

@Setup(Level.Trial)
public void setup() {
currentDeque = new ConcurrentLinkedDeque<>();
bareDeque = new ConcurrentLinkedDeque<>();
channel = new Object();
}

/** Current behavior: allocate an IdleChannel holder on every offer. */
@Benchmark
public void currentOfferPoll(Blackhole bh) {
currentDeque.offerFirst(new IdleChannel(channel, 123L));
IdleChannel c = currentDeque.pollFirst();
if (c != null && c.takeOwnership()) {
bh.consume(c.channel);
}
}

/**
* Alternative: push the bare channel ref. Models pushing the Channel itself
* and reading the timestamp/owned flag from a Netty channel attribute
* instead of a per-checkout holder. Only the CLD node is allocated.
*/
@Benchmark
public void bareOfferPoll(Blackhole bh) {
bareDeque.offerFirst(channel);
Object c = bareDeque.pollFirst();
bh.consume(c);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* 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 org.asynchttpclient.bench;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
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.infra.Blackhole;

import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Models {@code DefaultChannelPool.removeAll(Channel)} which calls
* {@code ConcurrentLinkedDeque.remove(Object)} — an O(n) full traversal of the partition deque
* performed on every connection close. Compared against a poll/offer (LIFO) pair which is O(1).
*
* Element identity mirrors IdleChannel.equals (compares wrapped value), so remove() must scan.
*
* Run multi-threaded:
* /tmp/run-jmh.sh ChannelPoolDequeBenchmark -t 8 -f 1 -wi 5 -i 8
*/
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ChannelPoolDequeBenchmark {

/** Steady-state number of idle connections per partition (deque length). */
@Param({"4", "32", "128"})
public int poolDepth;

private ConcurrentLinkedDeque<Holder> deque;
private Holder[] elements;
private final AtomicInteger removeCursor = new AtomicInteger();

static final class Holder {
final int id;
Holder(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return this == o || (o instanceof Holder && id == ((Holder) o).id);
}
@Override public int hashCode() { return id; }
}

@Setup(Level.Invocation)
public void setup() {
deque = new ConcurrentLinkedDeque<>();
elements = new Holder[poolDepth];
for (int i = 0; i < poolDepth; i++) {
elements[i] = new Holder(i);
deque.offerFirst(elements[i]);
}
}

/** Current removeAll path: O(n) remove(Object) scanning by equals. Removes the tail (worst case for LIFO insert). */
@Benchmark
public boolean currentRemoveAll() {
int idx = removeCursor.getAndIncrement() % poolDepth;
// remove a NEW Holder equal-by-id, exactly as DefaultChannelPool.removeAll builds
// `new IdleChannel(channel, Long.MIN_VALUE)` and lets the deque scan for it.
return deque.remove(new Holder(idx));
}

/** Baseline O(1) lease/return that poll()/offer() use, for scale reference. */
@Benchmark
public void pollOffer(Blackhole bh) {
Holder h = deque.pollFirst();
if (h != null) {
deque.offerFirst(h);
}
bh.consume(h);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* 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 org.asynchttpclient.bench;

import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.asynchttpclient.cookie.ThreadSafeCookieStore;
import org.asynchttpclient.uri.Uri;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
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 java.util.List;
import java.util.concurrent.TimeUnit;

/**
* Measures allocations of {@link ThreadSafeCookieStore#get(Uri)} which is on the
* request path: every outgoing request for which a cookie store is configured
* calls it to collect applicable cookies. The current implementation walks
* sub-domains and, for each, runs a Stream pipeline
* ({@code entrySet().stream().filter(lambda).map(lambda).collect(toList())}).
*
* This bench pins the per-get byte cost so a proposal can quantify replacing
* the Stream pipeline + per-subdomain list copies with an imperative scan.
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class CookieStoreGetBenchmark {

private ThreadSafeCookieStore store;
private Uri requestUri;

@Param({"1", "5"})
public int cookiesPerDomain;

@Setup(Level.Trial)
public void setup() {
store = new ThreadSafeCookieStore();
Uri uri = Uri.create("https://www.example.com/some/path");
for (int i = 0; i < cookiesPerDomain; i++) {
DefaultCookie c = new DefaultCookie("cookie" + i, "value" + i);
c.setDomain("www.example.com");
c.setPath("/some");
store.add(uri, c);
}
// a couple of parent-domain cookies to force the sub-domain walk to find matches
DefaultCookie root = new DefaultCookie("root", "v");
root.setDomain("example.com");
root.setPath("/");
store.add(uri, root);

requestUri = Uri.create("https://www.example.com/some/path/leaf");
}

@Benchmark
public List<Cookie> get() {
return store.get(requestUri);
}
}
Loading
Loading