Skip to content

Commit a3c60be

Browse files
committed
Reduce contention in DefaultByteBufferPool thread-local lookups
1 parent 77dc917 commit a3c60be

3 files changed

Lines changed: 366 additions & 4 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2026 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package io.undertow.benchmarks;
19+
20+
import io.undertow.connector.PooledByteBuffer;
21+
import io.undertow.server.DefaultByteBufferPool;
22+
import org.openjdk.jmh.annotations.Benchmark;
23+
import org.openjdk.jmh.annotations.BenchmarkMode;
24+
import org.openjdk.jmh.annotations.Fork;
25+
import org.openjdk.jmh.annotations.Measurement;
26+
import org.openjdk.jmh.annotations.Mode;
27+
import org.openjdk.jmh.annotations.OutputTimeUnit;
28+
import org.openjdk.jmh.annotations.Param;
29+
import org.openjdk.jmh.annotations.Scope;
30+
import org.openjdk.jmh.annotations.Setup;
31+
import org.openjdk.jmh.annotations.State;
32+
import org.openjdk.jmh.annotations.TearDown;
33+
import org.openjdk.jmh.annotations.Warmup;
34+
35+
import java.util.concurrent.TimeUnit;
36+
37+
@State(Scope.Benchmark)
38+
@Warmup(iterations = 3, time = 3)
39+
@Measurement(iterations = 5, time = 3)
40+
@Fork(3)
41+
@BenchmarkMode(Mode.Throughput)
42+
@OutputTimeUnit(TimeUnit.SECONDS)
43+
public class DefaultByteBufferPoolBenchmark {
44+
45+
@Param({"false", "true"})
46+
private boolean direct;
47+
48+
@Param({"0", "6", "12"})
49+
private int cacheSize;
50+
51+
private DefaultByteBufferPool pool;
52+
53+
@Setup
54+
public void setup() {
55+
pool = new DefaultByteBufferPool(direct, 16 * 1024 - 20, 1024, cacheSize);
56+
}
57+
58+
@TearDown
59+
public void tearDown() {
60+
pool.close();
61+
pool.getArrayBackedPool().close();
62+
}
63+
64+
@Benchmark
65+
public int allocateAndRelease(Buffers state) {
66+
int sum = 0;
67+
try {
68+
for (int i = 0; i < state.buffers.length; i++) {
69+
PooledByteBuffer buffer = pool.allocate();
70+
state.buffers[i] = buffer;
71+
buffer.getBuffer().putInt(i);
72+
}
73+
for (PooledByteBuffer buffer : state.buffers) {
74+
sum += buffer.getBuffer().getInt(0);
75+
}
76+
return sum;
77+
} finally {
78+
for (int i = 0; i < state.buffers.length; i++) {
79+
if (state.buffers[i] != null) {
80+
state.buffers[i].close();
81+
state.buffers[i] = null;
82+
}
83+
}
84+
}
85+
}
86+
87+
@State(Scope.Thread)
88+
public static class Buffers {
89+
90+
@Param({"1", "16"})
91+
private int batchSize;
92+
93+
private PooledByteBuffer[] buffers;
94+
95+
@Setup
96+
public void setup() {
97+
buffers = new PooledByteBuffer[batchSize];
98+
}
99+
}
100+
}

‎core/src/main/java/io/undertow/server/DefaultByteBufferPool.java‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
// TODO: move this somewhere more appropriate
4343
public class DefaultByteBufferPool implements ByteBufferPool {
4444

45-
private final ThreadLocalCache threadLocalCache = new ThreadLocalCache();
45+
private final ThreadLocalCache threadLocalCache;
4646
// Access requires synchronization on the threadLocalDataList instance
4747
private final List<WeakReference<ThreadLocalData>> threadLocalDataList = new ArrayList<>();
4848
private final ConcurrentLinkedQueue<ByteBuffer> queue = new ConcurrentLinkedQueue<>();
@@ -85,6 +85,7 @@ public DefaultByteBufferPool(boolean direct, int bufferSize, int maximumPoolSize
8585
this.bufferSize = bufferSize;
8686
this.maximumPoolSize = maximumPoolSize;
8787
this.threadLocalCacheSize = threadLocalCacheSize;
88+
this.threadLocalCache = new ThreadLocalCache(threadLocalCacheSize > 0);
8889
this.leakDectionPercent = leakDecetionPercent;
8990
if(direct) {
9091
arrayBackedPool = new DefaultByteBufferPool(false, bufferSize, maximumPoolSize, 0, leakDecetionPercent);
@@ -301,6 +302,7 @@ public String toString() {
301302
private class ThreadLocalData {
302303
final ArrayDeque<ByteBuffer> buffers = new ArrayDeque<>(threadLocalCacheSize);
303304
int allocationDepth = 0;
305+
int cacheAccesses;
304306

305307
@Override
306308
protected void finalize() throws Throwable {
@@ -340,18 +342,34 @@ protected void finalize() throws Throwable {
340342
}
341343
}
342344

343-
// This is used instead of Java ThreadLocal class. Unlike in the ThreadLocal class, the remove() method in this
344-
// class can be called by a different thread than the one that initialized the data.
345+
// The registry permits cleanup from another thread.
345346
private static class ThreadLocalCache {
346347

348+
private final ThreadLocal<WeakReference<ThreadLocalData>> localReference;
349+
347350
final Map<Thread, ThreadLocalData> localsByThread = Collections.synchronizedMap(new WeakHashMap<>());
348351

352+
ThreadLocalCache(boolean cachingEnabled) {
353+
localReference = cachingEnabled ? new ThreadLocal<>() : null;
354+
}
355+
349356
ThreadLocalData get() {
350-
return localsByThread.get(Thread.currentThread());
357+
WeakReference<ThreadLocalData> reference = localReference == null ? null : localReference.get();
358+
ThreadLocalData local = reference == null ? null : reference.get();
359+
if (local == null) {
360+
return localsByThread.get(Thread.currentThread());
361+
}
362+
// WeakHashMap only releases stale values when accessed; keep retiring dead threads' caches.
363+
if ((++local.cacheAccesses & 255) == 0) {
364+
localsByThread.size();
365+
}
366+
return local;
351367
}
352368

353369
void set(ThreadLocalData threadLocalData) {
354370
localsByThread.put(Thread.currentThread(), threadLocalData);
371+
// A long-lived thread must not keep this pool alive through ThreadLocalData.
372+
localReference.set(new WeakReference<>(threadLocalData));
355373
}
356374

357375
void remove(ThreadLocalData threadLocalData) {

0 commit comments

Comments
 (0)