1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.util;
4 import com.intellij.openapi.Disposable;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.progress.ProcessCanceledException;
7 import com.intellij.openapi.util.objectTree.ThrowableInterner;
8 import com.intellij.util.IncorrectOperationException;
9 import com.intellij.util.SmartList;
10 import com.intellij.util.containers.ContainerUtil;
11 import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
12 import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
13 import org.jetbrains.annotations.NotNull;
14 import org.jetbrains.annotations.Nullable;
15 import org.jetbrains.annotations.TestOnly;
18 import java.util.function.Supplier;
20 final class ObjectTree {
21 private static final ThreadLocal<Throwable> ourTopmostDisposeTrace = new ThreadLocal<>();
23 // identity used here to prevent problems with hashCode/equals overridden by not very bright minds
24 private final Set<Disposable> myRootObjects = new ReferenceOpenHashSet<>(); // guarded by treeLock
25 // guarded by treeLock
26 private final Map<Disposable, ObjectNode> myObject2NodeMap = new Reference2ObjectOpenHashMap<>();
27 // Disposable -> trace or boolean marker (if trace unavailable)
28 private final Map<Disposable, Object> myDisposedObjects = ContainerUtil.createWeakMap(100, 0.5f, ContainerUtil.identityStrategy()); // guarded by treeLock
30 final Object treeLock = new Object();
32 private ObjectNode getNode(@NotNull Disposable object) {
33 return myObject2NodeMap.get(object);
36 private void putNode(@NotNull Disposable object, @Nullable("null means remove") ObjectNode node) {
38 myObject2NodeMap.remove(object);
41 myObject2NodeMap.put(object, node);
46 final RuntimeException register(@NotNull Disposable parent, @NotNull Disposable child) {
47 if (parent == child) return new IllegalArgumentException("Cannot register to itself: "+parent);
48 synchronized (treeLock) {
49 Object wasDisposed = getDisposalInfo(parent);
50 if (wasDisposed != null) {
51 return new IncorrectOperationException("Sorry but parent: " + parent + " has already been disposed " +
52 "(see the cause for stacktrace) so the child: "+child+" will never be disposed",
53 wasDisposed instanceof Throwable ? (Throwable)wasDisposed : null);
56 myDisposedObjects.remove(child); // if we dispose thing and then register it back it means it's not disposed anymore
57 ObjectNode parentNode = getNode(parent);
58 if (parentNode == null) parentNode = createNodeFor(parent, null);
60 ObjectNode childNode = getNode(child);
61 if (childNode == null) {
62 childNode = createNodeFor(child, parentNode);
65 ObjectNode oldParent = childNode.getParent();
66 if (oldParent != null) {
67 oldParent.removeChild(childNode);
70 myRootObjects.remove(child);
72 RuntimeException e = checkWasNotAddedAlready(parentNode, childNode);
73 if (e != null) return e;
75 parentNode.addChild(childNode);
80 Object getDisposalInfo(@NotNull Disposable object) {
81 synchronized (treeLock) {
82 return myDisposedObjects.get(object);
86 private static RuntimeException checkWasNotAddedAlready(@NotNull ObjectNode childNode, @NotNull ObjectNode parentNode) {
87 for (ObjectNode node = childNode; node != null; node = node.getParent()) {
88 if (node == parentNode) {
89 return new IncorrectOperationException("'"+childNode.getObject() + "' was already added as a child of '" + parentNode.getObject()+"'");
96 private ObjectNode createNodeFor(@NotNull Disposable object, @Nullable ObjectNode parentNode) {
97 final ObjectNode newNode = new ObjectNode(this, parentNode, object);
98 if (parentNode == null) {
99 myRootObjects.add(object);
101 putNode(object, newNode);
105 private void runWithTrace(@NotNull Supplier<? extends List<Disposable>> removeFromTreeAction) {
106 boolean needTrace = Disposer.isDebugMode() && ourTopmostDisposeTrace.get() == null;
108 ourTopmostDisposeTrace.set(ThrowableInterner.intern(new Throwable()));
111 // first, atomically remove disposables from the tree to avoid "register during dispose" race conditions
112 List<Disposable> disposables;
113 synchronized (treeLock) {
114 disposables = removeFromTreeAction.get();
117 // second, call "beforeTreeDispose" in pre-order (some clients are hardcoded to see parents-then-children order in "beforeTreeDispose")
118 List<Throwable> exceptions = null;
119 for (int i = disposables.size() - 1; i >= 0; i--) {
120 Disposable disposable = disposables.get(i);
121 if (disposable instanceof Disposable.Parent) {
123 ((Disposable.Parent)disposable).beforeTreeDispose();
125 catch (Throwable t) {
126 if (exceptions == null) exceptions = new SmartList<>();
132 // third, dispose in post-order (bottom-up)
133 for (Disposable disposable : disposables) {
135 //noinspection SSBasedInspection
136 disposable.dispose();
138 catch (Throwable e) {
139 if (exceptions == null) exceptions = new SmartList<>();
145 ourTopmostDisposeTrace.remove();
147 if (exceptions != null) {
148 handleExceptions(exceptions);
152 void executeAllChildren(@NotNull Disposable object) {
154 ObjectNode node = getNode(object);
156 return Collections.emptyList();
158 List<Disposable> disposables = new ArrayList<>();
159 node.getAndRemoveChildrenRecursively(disposables);
164 void executeAll(@NotNull Disposable object, boolean processUnregistered) {
166 ObjectNode node = getNode(object);
167 if (node == null && !processUnregistered) {
168 return Collections.emptyList();
170 List<Disposable> disposables = new ArrayList<>();
172 if (rememberDisposedTrace(object) == null) {
173 disposables.add(object);
177 node.getAndRemoveRecursively(disposables);
183 private static void handleExceptions(@NotNull List<? extends Throwable> exceptions) {
184 if (!exceptions.isEmpty()) {
185 for (Throwable exception : exceptions) {
186 if (!(exception instanceof ProcessCanceledException)) {
187 getLogger().error(exception);
191 ProcessCanceledException pce = ContainerUtil.findInstance(exceptions, ProcessCanceledException.class);
199 void assertNoReferenceKeptInTree(@NotNull Disposable disposable) {
200 synchronized (treeLock) {
201 for (Map.Entry<Disposable, ObjectNode> entry : myObject2NodeMap.entrySet()) {
202 Disposable key = entry.getKey();
203 assert key != disposable;
204 ObjectNode node = entry.getValue();
205 node.assertNoReferencesKept(disposable);
210 void assertIsEmpty(boolean throwError) {
211 synchronized (treeLock) {
212 for (Disposable object : myRootObjects) {
213 if (object == null) continue;
214 ObjectNode objectNode = getNode(object);
215 if (objectNode == null) continue;
216 while (objectNode.getParent() != null) {
217 objectNode = objectNode.getParent();
219 final Throwable trace = objectNode.getTrace();
220 RuntimeException exception = new RuntimeException("Memory leak detected: '" + object + "' of " + object.getClass()
221 + "\nSee the cause for the corresponding Disposer.register() stacktrace:\n",
226 getLogger().error(exception);
232 private static Logger getLogger() {
233 return Logger.getInstance(ObjectTree.class);
237 Object rememberDisposedTrace(@NotNull Disposable object) {
238 synchronized (treeLock) {
239 Throwable trace = ourTopmostDisposeTrace.get();
240 return myDisposedObjects.put(object, trace != null ? trace : Boolean.TRUE);
244 void clearDisposedObjectTraces() {
245 synchronized (treeLock) {
246 myDisposedObjects.clear();
247 for (ObjectNode value : myObject2NodeMap.values()) {
254 <D extends Disposable> D findRegisteredObject(@NotNull Disposable parentDisposable, @NotNull D object) {
255 synchronized (treeLock) {
256 ObjectNode parentNode = getNode(parentDisposable);
257 if (parentNode == null) return null;
258 return parentNode.findChildEqualTo(object);
262 void removeObjectFromTree(@NotNull ObjectNode node) {
263 synchronized (treeLock) {
264 Disposable myObject = node.getObject();
265 putNode(myObject, null);
266 ObjectNode parent = node.getParent();
267 if (parent == null) {
268 myRootObjects.remove(myObject);
271 parent.removeChild(node);
273 node.myParent = null;