From 807bc74dfc710e59b5b53140df54f67b65c755ee Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Mon, 15 Jul 2024 12:38:34 +0200 Subject: [PATCH] backport tracking pool from jetty 12 Signed-off-by: Ludovic Orban --- .../eclipse/jetty/io/ArrayByteBufferPool.java | 38 ++++ .../io/ArrayRetainableByteBufferPool.java | 165 ++++++++++++++++++ .../jetty/io/RetainableByteBuffer.java | 2 +- 3 files changed, 204 insertions(+), 1 deletion(-) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java index 5cb7e16f6f05..c2c66633cdd6 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayByteBufferPool.java @@ -339,4 +339,42 @@ protected void removed(RetainableByteBuffer retainedBuffer) ArrayByteBufferPool.this.release(retainedBuffer.getBuffer()); } } + + /** + *

A variant of {@link ArrayByteBufferPool} that tracks buffer + * acquires/releases of the retained buffers, useful to identify buffer leaks.

+ * @see ArrayRetainableByteBufferPool.Tracking + */ + public static class Tracking extends ArrayByteBufferPool + { + public Tracking() + { + } + + public Tracking(int minCapacity, int factor, int maxCapacity) + { + super(minCapacity, factor, maxCapacity); + } + + public Tracking(int minCapacity, int factor, int maxCapacity, int maxQueueLength) + { + super(minCapacity, factor, maxCapacity, maxQueueLength); + } + + public Tracking(int minCapacity, int factor, int maxCapacity, int maxBucketSize, long maxHeapMemory, long maxDirectMemory) + { + super(minCapacity, factor, maxCapacity, maxBucketSize, maxHeapMemory, maxDirectMemory); + } + + public Tracking(int minCapacity, int factor, int maxCapacity, int maxBucketSize, long maxHeapMemory, long maxDirectMemory, long retainedHeapMemory, long retainedDirectMemory) + { + super(minCapacity, factor, maxCapacity, maxBucketSize, maxHeapMemory, maxDirectMemory, retainedHeapMemory, retainedDirectMemory); + } + + @Override + protected RetainableByteBufferPool newRetainableByteBufferPool(int factor, int maxCapacity, int maxBucketSize, long retainedHeapMemory, long retainedDirectMemory) + { + return new ArrayRetainableByteBufferPool.Tracking(0, factor, maxCapacity, maxBucketSize, retainedHeapMemory, retainedDirectMemory); + } + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayRetainableByteBufferPool.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayRetainableByteBufferPool.java index 8dea3c248229..b2fa75ac980a 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayRetainableByteBufferPool.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ArrayRetainableByteBufferPool.java @@ -14,12 +14,20 @@ package org.eclipse.jetty.io; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.ByteBuffer; +import java.time.Instant; import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntUnaryOperator; +import java.util.stream.Collectors; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.NanoTime; @@ -486,4 +494,161 @@ public String toString() entries > 0 ? (inUse * 100) / entries : 0); } } + + /** + *

A variant of {@link ArrayRetainableByteBufferPool} that tracks buffer + * acquires/releases, useful to identify buffer leaks.

+ *

Use {@link #getLeaks()} when the system is idle to get + * the {@link Buffer}s that have been leaked, which contain + * the stack trace information of where the buffer was acquired.

+ */ + public static class Tracking extends ArrayRetainableByteBufferPool + { + private static final Logger LOG = LoggerFactory.getLogger(Tracking.class); + + private final Set buffers = ConcurrentHashMap.newKeySet(); + + public Tracking() + { + super(); + } + + public Tracking(int minCapacity, int factor, int maxCapacity, int maxBucketSize) + { + super(minCapacity, factor, maxCapacity, maxBucketSize); + } + + public Tracking(int minCapacity, int factor, int maxCapacity, int maxBucketSize, long maxHeapMemory, long maxDirectMemory) + { + super(minCapacity, factor, maxCapacity, maxBucketSize, maxHeapMemory, maxDirectMemory); + } + + public Tracking(int minCapacity, int factor, int maxCapacity, int maxBucketSize, IntUnaryOperator bucketIndexFor, IntUnaryOperator bucketCapacity, long maxHeapMemory, long maxDirectMemory) + { + super(minCapacity, factor, maxCapacity, maxBucketSize, bucketIndexFor, bucketCapacity, maxHeapMemory, maxDirectMemory); + } + + @Override + public RetainableByteBuffer acquire(int size, boolean direct) + { + RetainableByteBuffer buffer = super.acquire(size, direct); + Buffer wrapper = new Buffer(buffer, size); + if (LOG.isDebugEnabled()) + LOG.debug("acquired {}", wrapper); + buffers.add(wrapper); + return wrapper; + } + + public Set getLeaks() + { + return buffers; + } + + public String dumpLeaks() + { + return getLeaks().stream() + .map(Buffer::dump) + .collect(Collectors.joining(System.lineSeparator())); + } + + public class Buffer extends RetainableByteBuffer + { + private final RetainableByteBuffer wrapped; + private final int size; + private final Instant acquireInstant; + private final Throwable acquireStack; + private final List retainStacks = new CopyOnWriteArrayList<>(); + private final List releaseStacks = new CopyOnWriteArrayList<>(); + private final List overReleaseStacks = new CopyOnWriteArrayList<>(); + + private Buffer(RetainableByteBuffer wrapped, int size) + { + super(wrapped.getBuffer(), x -> {}); + this.wrapped = wrapped; + this.size = size; + this.acquireInstant = Instant.now(); + this.acquireStack = new Throwable(); + } + + public int getSize() + { + return size; + } + + public Instant getAcquireInstant() + { + return acquireInstant; + } + + public Throwable getAcquireStack() + { + return acquireStack; + } + + @Override + protected void acquire() + { + wrapped.acquire(); + } + + @Override + public boolean isRetained() + { + return wrapped.isRetained(); + } + + @Override + public void retain() + { + wrapped.retain(); + retainStacks.add(new Throwable()); + } + + @Override + public boolean release() + { + try + { + boolean released = wrapped.release(); + if (released) + { + buffers.remove(this); + if (LOG.isDebugEnabled()) + LOG.debug("released {}", this); + } + releaseStacks.add(new Throwable()); + return released; + } + catch (IllegalStateException e) + { + buffers.add(this); + overReleaseStacks.add(new Throwable()); + throw e; + } + } + + public String dump() + { + StringWriter w = new StringWriter(); + PrintWriter pw = new PrintWriter(w); + getAcquireStack().printStackTrace(pw); + pw.println("\n" + retainStacks.size() + " retain(s)"); + for (Throwable retainStack : retainStacks) + { + retainStack.printStackTrace(pw); + } + pw.println("\n" + releaseStacks.size() + " release(s)"); + for (Throwable releaseStack : releaseStacks) + { + releaseStack.printStackTrace(pw); + } + pw.println("\n" + overReleaseStacks.size() + " over-release(s)"); + for (Throwable overReleaseStack : overReleaseStacks) + { + overReleaseStack.printStackTrace(pw); + } + return String.format("%s@%x of %d bytes on %s wrapping %s acquired at %s", getClass().getSimpleName(), hashCode(), getSize(), getAcquireInstant(), wrapped, w); + } + } + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java b/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java index bc748041f348..0b274785f4ca 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java @@ -82,7 +82,7 @@ public boolean isDirect() * The reason why this method exists on top of {@link #retain()} is to be able to * have some safety checks that must know why the ref counter is being incremented. */ - void acquire() + protected void acquire() { if (references.getAndUpdate(c -> c == 0 ? 1 : c) != 0) throw new IllegalStateException("re-pooled while still used " + this);