1 // Copyright 2019 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import androidx.annotation.VisibleForTesting; 8 9 import org.chromium.build.BuildConfig; 10 import org.chromium.build.annotations.CheckDiscard; 11 12 import java.lang.ref.PhantomReference; 13 import java.lang.ref.ReferenceQueue; 14 import java.util.Collections; 15 import java.util.HashSet; 16 import java.util.Set; 17 18 /** 19 * Used to assert that clean-up logic has been run before an object is GC'ed. 20 * 21 * <p>Usage: 22 * <pre> 23 * class MyClassWithCleanup { 24 * private final mLifetimeAssert = LifetimeAssert.create(this); 25 * 26 * public void destroy() { 27 * // If mLifetimeAssert is GC'ed before this is called, it will throw an exception 28 * // with a stack trace showing the stack during LifetimeAssert.create(). 29 * LifetimeAssert.setSafeToGc(mLifetimeAssert, true); 30 * } 31 * } 32 * </pre> 33 */ 34 @CheckDiscard("Lifetime assertions aren't used when DCHECK is off.") 35 public class LifetimeAssert { 36 interface TestHook { onCleaned(WrappedReference ref, String msg)37 void onCleaned(WrappedReference ref, String msg); 38 } 39 40 /** Thrown for failed assertions. */ 41 static class LifetimeAssertException extends RuntimeException { LifetimeAssertException(String msg, Throwable causedBy)42 LifetimeAssertException(String msg, Throwable causedBy) { 43 super(msg, causedBy); 44 } 45 } 46 47 /** For capturing where objects were created. */ 48 private static class CreationException extends RuntimeException { CreationException()49 CreationException() { 50 super("vvv This is where object was created. vvv"); 51 } 52 } 53 54 // Used only for unit test. 55 static TestHook sTestHook; 56 57 @VisibleForTesting final WrappedReference mWrapper; 58 59 private final Object mTarget; 60 61 @VisibleForTesting 62 static class WrappedReference extends PhantomReference<Object> { 63 boolean mSafeToGc; 64 final Class<?> mTargetClass; 65 final CreationException mCreationException; 66 WrappedReference( Object target, CreationException creationException, boolean safeToGc)67 public WrappedReference( 68 Object target, CreationException creationException, boolean safeToGc) { 69 super(target, sReferenceQueue); 70 mCreationException = creationException; 71 mSafeToGc = safeToGc; 72 mTargetClass = target.getClass(); 73 sActiveWrappers.add(this); 74 } 75 76 private static ReferenceQueue<Object> sReferenceQueue = new ReferenceQueue<>(); 77 private static Set<WrappedReference> sActiveWrappers = 78 Collections.synchronizedSet(new HashSet<>()); 79 80 static { 81 new Thread("GcStateAssertQueue") { 82 { 83 setDaemon(true); start()84 start(); 85 } 86 87 @Override run()88 public void run() { 89 while (true) { 90 try { 91 // This sleeps until a wrapper is available. 92 WrappedReference wrapper = (WrappedReference) sReferenceQueue.remove(); 93 if (!sActiveWrappers.remove(wrapper)) { 94 // The reference was not a part of the active set. The reference was 95 // cleared by resetForTesting(). 96 continue; 97 } 98 if (!wrapper.mSafeToGc) { 99 String msg = 100 String.format( 101 "Object of type %s was GC'ed without cleanup. Refer" 102 + " to \"Caused by\" for where object was" 103 + " created.", 104 wrapper.mTargetClass.getName()); 105 if (sTestHook != null) { 106 sTestHook.onCleaned(wrapper, msg); 107 } else { 108 throw new LifetimeAssertException( 109 msg, wrapper.mCreationException); 110 } 111 } else if (sTestHook != null) { 112 sTestHook.onCleaned(wrapper, null); 113 } 114 } catch (InterruptedException e) { 115 throw new RuntimeException(e); 116 } 117 } 118 } 119 }; 120 } 121 } 122 LifetimeAssert(WrappedReference wrapper, Object target)123 private LifetimeAssert(WrappedReference wrapper, Object target) { 124 mWrapper = wrapper; 125 mTarget = target; 126 } 127 create(Object target)128 public static LifetimeAssert create(Object target) { 129 if (!BuildConfig.ENABLE_ASSERTS) { 130 return null; 131 } 132 return new LifetimeAssert( 133 new WrappedReference(target, new CreationException(), false), target); 134 } 135 create(Object target, boolean safeToGc)136 public static LifetimeAssert create(Object target, boolean safeToGc) { 137 if (!BuildConfig.ENABLE_ASSERTS) { 138 return null; 139 } 140 return new LifetimeAssert( 141 new WrappedReference(target, new CreationException(), safeToGc), target); 142 } 143 setSafeToGc(LifetimeAssert asserter, boolean value)144 public static void setSafeToGc(LifetimeAssert asserter, boolean value) { 145 if (BuildConfig.ENABLE_ASSERTS) { 146 // This guaratees that the target object is reachable until after mSafeToGc value 147 // is updated here. See comment on Reference.reachabilityFence and review comments 148 // on https://chromium-review.googlesource.com/c/chromium/src/+/1887151 for a 149 // problematic example. This synchronized is used instead of calling 150 // reachabilityFence because robolectric has problems mocking out that method, 151 // and this should work for all Android versions. 152 synchronized (asserter.mTarget) { 153 // asserter is never null when ENABLE_ASSERTS. 154 asserter.mWrapper.mSafeToGc = value; 155 } 156 } 157 } 158 159 /** 160 * Asserts that the remaining objects used with LifetimeAssert do not need to be destroyed and 161 * can be garbage collected. Always clears the set of tracked object, so consecutive invocations 162 * won't throw with the same cause. 163 */ assertAllInstancesDestroyedForTesting()164 public static void assertAllInstancesDestroyedForTesting() throws LifetimeAssertException { 165 if (!BuildConfig.ENABLE_ASSERTS) { 166 return; 167 } 168 // Synchronized set requires manual synchronization when iterating over it. 169 synchronized (WrappedReference.sActiveWrappers) { 170 try { 171 for (WrappedReference ref : WrappedReference.sActiveWrappers) { 172 if (!ref.mSafeToGc) { 173 String msg = 174 String.format( 175 "Object of type %s was not destroyed after test completed." 176 + " Refer to \"Caused by\" for where object was" 177 + " created.", 178 ref.mTargetClass.getName()); 179 throw new LifetimeAssertException(msg, ref.mCreationException); 180 } 181 } 182 } finally { 183 WrappedReference.sActiveWrappers.clear(); 184 } 185 } 186 } 187 188 /** Clears the set of tracked references. */ resetForTesting()189 public static void resetForTesting() { 190 if (!BuildConfig.ENABLE_ASSERTS) { 191 return; 192 } 193 WrappedReference.sActiveWrappers.clear(); 194 } 195 } 196