diff --git a/sdk/android/api/org/webrtc/TextureBufferImpl.java b/sdk/android/api/org/webrtc/TextureBufferImpl.java index a24f284790..bcb9478e17 100644 --- a/sdk/android/api/org/webrtc/TextureBufferImpl.java +++ b/sdk/android/api/org/webrtc/TextureBufferImpl.java @@ -19,6 +19,8 @@ import android.support.annotation.Nullable; * release callback. ToI420() is implemented by providing a Handler and a YuvConverter. */ public class TextureBufferImpl implements VideoFrame.TextureBuffer { + private static final int RELEASE_TIMEOUT_MS = 10000; + // This is the full resolution the texture has in memory after applying the transformation matrix // that might include cropping. This resolution is useful to know when sampling the texture to // avoid downscaling artifacts. @@ -60,7 +62,7 @@ public class TextureBufferImpl implements VideoFrame.TextureBuffer { this.transformMatrix = transformMatrix; this.toI420Handler = toI420Handler; this.yuvConverter = yuvConverter; - this.refCountDelegate = new RefCountDelegate(releaseCallback); + this.refCountDelegate = new RefCountDelegate(releaseCallback, RELEASE_TIMEOUT_MS); } @Override diff --git a/sdk/android/src/java/org/webrtc/RefCountDelegate.java b/sdk/android/src/java/org/webrtc/RefCountDelegate.java index 58be7aa0fb..89c55724d4 100644 --- a/sdk/android/src/java/org/webrtc/RefCountDelegate.java +++ b/sdk/android/src/java/org/webrtc/RefCountDelegate.java @@ -10,7 +10,13 @@ package org.webrtc; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.GuardedBy; import android.support.annotation.Nullable; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -19,16 +25,44 @@ import java.util.concurrent.atomic.AtomicInteger; class RefCountDelegate implements RefCounted { private final AtomicInteger refCount = new AtomicInteger(1); private final @Nullable Runnable releaseCallback; + private final @Nullable RefCountMonitor refCountMonitor; /** + * Initializes a new ref count. The initial ref count will be 1. + * * @param releaseCallback Callback that will be executed once the ref count reaches zero. */ public RefCountDelegate(@Nullable Runnable releaseCallback) { + this(releaseCallback, /*releaseTimeoutMs=*/0); + } + + /** + * Initializes a new ref count with a release timeout. The initial ref count will be 1. + * + * @param releaseCallback Callback that will be executed once the ref count reaches zero. + * @param releaseTimeoutMs If release timeout is not 0, release of this object will monitored. + * When timeout is reached, stack traces for all threads that have called retain/release will + * be printed. + */ + public RefCountDelegate(@Nullable Runnable releaseCallback, int releaseTimeoutMs) { + if (releaseTimeoutMs < 0) { + throw new IllegalArgumentException("Release timeout must be positive."); + } + this.releaseCallback = releaseCallback; + if (releaseTimeoutMs != 0) { + refCountMonitor = new RefCountMonitor(this, releaseTimeoutMs); + refCountMonitor.storeCurrentStackTrace(); + } else { + refCountMonitor = null; + } } @Override public void retain() { + if (refCountMonitor != null) { + refCountMonitor.storeCurrentStackTrace(); + } int updated_count = refCount.incrementAndGet(); if (updated_count < 2) { throw new IllegalStateException("retain() called on an object with refcount < 1"); @@ -37,12 +71,88 @@ class RefCountDelegate implements RefCounted { @Override public void release() { + if (refCountMonitor != null) { + refCountMonitor.storeCurrentStackTrace(); + } int updated_count = refCount.decrementAndGet(); if (updated_count < 0) { throw new IllegalStateException("release() called on an object with refcount < 1"); } if (updated_count == 0 && releaseCallback != null) { + if (refCountMonitor != null) { + refCountMonitor.cancel(); + } releaseCallback.run(); } } + + @Override + protected void finalize() { + if (refCount.get() != 0) { + Logging.e(toString(), "Leaked ref counted object with active references."); + if (refCountMonitor != null) { + refCountMonitor.printStackTraces(toString()); + } + } + } + + private static final class StackTraceHolder { + final String threadName; + // A trick to store a stack trace (fast) is to construct a throwable. + final Throwable throwable; + + StackTraceHolder(String threadName, Throwable throwable) { + this.threadName = threadName; + this.throwable = throwable; + } + } + + private static final class RefCountMonitor { + @GuardedBy("stackTraces") private final List stackTraces = new ArrayList<>(); + + private final Runnable releaseTimeoutRunnable = this::onReleaseTimeout; + private final WeakReference refCountDelegate; + private final int releaseTimeoutMs; + private final Handler releaseTimeoutHandler; + + RefCountMonitor(RefCountDelegate refCountDelegate, int releaseTimeoutMs) { + this.refCountDelegate = new WeakReference<>(refCountDelegate); + this.releaseTimeoutMs = releaseTimeoutMs; + this.releaseTimeoutHandler = new Handler(Looper.getMainLooper()); + + releaseTimeoutHandler.postDelayed(releaseTimeoutRunnable, releaseTimeoutMs); + } + + private void onReleaseTimeout() { + final RefCountDelegate refCountDelegate = this.refCountDelegate.get(); + if (refCountDelegate == null) { + return; + } + if (refCountDelegate.refCount.get() == 0) { + return; + } + + Logging.e(refCountDelegate.toString(), "Still unreleased ref counted object."); + printStackTraces(refCountDelegate.toString()); + releaseTimeoutHandler.postDelayed(releaseTimeoutRunnable, releaseTimeoutMs); + } + + void printStackTraces(String tag) { + synchronized (stackTraces) { + for (StackTraceHolder stackTrace : stackTraces) { + Logging.e(tag, "Stack trace for: " + stackTrace.threadName, stackTrace.throwable); + } + } + } + + void cancel() { + releaseTimeoutHandler.removeCallbacks(releaseTimeoutRunnable); + } + + void storeCurrentStackTrace() { + synchronized (stackTraces) { + stackTraces.add(new StackTraceHolder(Thread.currentThread().getName(), new Throwable())); + } + } + } }