blob: e56f1dba20c0c10664517158c2ed0b375d8ca02a [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import androidx.annotation.VisibleForTesting;
import org.chromium.build.BuildConfig;
import org.chromium.build.annotations.CheckDiscard;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Used to assert that clean-up logic has been run before an object is GC'ed.
*
* <p>Usage:
* <pre>
* class MyClassWithCleanup {
* private final mLifetimeAssert = LifetimeAssert.create(this);
*
* public void destroy() {
* // If mLifetimeAssert is GC'ed before this is called, it will throw an exception
* // with a stack trace showing the stack during LifetimeAssert.create().
* LifetimeAssert.setSafeToGc(mLifetimeAssert, true);
* }
* }
* </pre>
*/
@CheckDiscard("Lifetime assertions aren't used when DCHECK is off.")
public class LifetimeAssert {
interface TestHook {
void onCleaned(WrappedReference ref, String msg);
}
/** Thrown for failed assertions. */
static class LifetimeAssertException extends RuntimeException {
LifetimeAssertException(String msg, Throwable causedBy) {
super(msg, causedBy);
}
}
/** For capturing where objects were created. */
private static class CreationException extends RuntimeException {
CreationException() {
super("vvv This is where object was created. vvv");
}
}
// Used only for unit test.
static TestHook sTestHook;
@VisibleForTesting final WrappedReference mWrapper;
private final Object mTarget;
@VisibleForTesting
static class WrappedReference extends PhantomReference<Object> {
boolean mSafeToGc;
final Class<?> mTargetClass;
final CreationException mCreationException;
public WrappedReference(
Object target, CreationException creationException, boolean safeToGc) {
super(target, sReferenceQueue);
mCreationException = creationException;
mSafeToGc = safeToGc;
mTargetClass = target.getClass();
sActiveWrappers.add(this);
}
private static ReferenceQueue<Object> sReferenceQueue = new ReferenceQueue<>();
private static Set<WrappedReference> sActiveWrappers =
Collections.synchronizedSet(new HashSet<>());
static {
new Thread("GcStateAssertQueue") {
{
setDaemon(true);
start();
}
@Override
public void run() {
while (true) {
try {
// This sleeps until a wrapper is available.
WrappedReference wrapper = (WrappedReference) sReferenceQueue.remove();
if (!sActiveWrappers.remove(wrapper)) {
// The reference was not a part of the active set. The reference was
// cleared by resetForTesting().
continue;
}
if (!wrapper.mSafeToGc) {
String msg =
String.format(
"Object of type %s was GC'ed without cleanup. Refer"
+ " to \"Caused by\" for where object was"
+ " created.",
wrapper.mTargetClass.getName());
if (sTestHook != null) {
sTestHook.onCleaned(wrapper, msg);
} else {
throw new LifetimeAssertException(
msg, wrapper.mCreationException);
}
} else if (sTestHook != null) {
sTestHook.onCleaned(wrapper, null);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
}
}
private LifetimeAssert(WrappedReference wrapper, Object target) {
mWrapper = wrapper;
mTarget = target;
}
public static LifetimeAssert create(Object target) {
if (!BuildConfig.ENABLE_ASSERTS) {
return null;
}
return new LifetimeAssert(
new WrappedReference(target, new CreationException(), false), target);
}
public static LifetimeAssert create(Object target, boolean safeToGc) {
if (!BuildConfig.ENABLE_ASSERTS) {
return null;
}
return new LifetimeAssert(
new WrappedReference(target, new CreationException(), safeToGc), target);
}
public static void setSafeToGc(LifetimeAssert asserter, boolean value) {
if (BuildConfig.ENABLE_ASSERTS) {
// This guaratees that the target object is reachable until after mSafeToGc value
// is updated here. See comment on Reference.reachabilityFence and review comments
// on https://chromium-review.googlesource.com/c/chromium/src/+/1887151 for a
// problematic example. This synchronized is used instead of calling
// reachabilityFence because robolectric has problems mocking out that method,
// and this should work for all Android versions.
synchronized (asserter.mTarget) {
// asserter is never null when ENABLE_ASSERTS.
asserter.mWrapper.mSafeToGc = value;
}
}
}
/**
* Asserts that the remaining objects used with LifetimeAssert do not need to be destroyed and
* can be garbage collected. Always clears the set of tracked object, so consecutive invocations
* won't throw with the same cause.
*/
public static void assertAllInstancesDestroyedForTesting() throws LifetimeAssertException {
if (!BuildConfig.ENABLE_ASSERTS) {
return;
}
// Synchronized set requires manual synchronization when iterating over it.
synchronized (WrappedReference.sActiveWrappers) {
try {
for (WrappedReference ref : WrappedReference.sActiveWrappers) {
if (!ref.mSafeToGc) {
String msg =
String.format(
"Object of type %s was not destroyed after test completed."
+ " Refer to \"Caused by\" for where object was"
+ " created.",
ref.mTargetClass.getName());
throw new LifetimeAssertException(msg, ref.mCreationException);
}
}
} finally {
WrappedReference.sActiveWrappers.clear();
}
}
}
/** Clears the set of tracked references. */
public static void resetForTesting() {
if (!BuildConfig.ENABLE_ASSERTS) {
return;
}
WrappedReference.sActiveWrappers.clear();
}
}