• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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