diff --git a/platform/core-impl/src/com/intellij/util/CachedValueChecker.java b/platform/core-impl/src/com/intellij/util/CachedValueChecker.java index 24ba444b816d..245f9a48f3c9 100644 --- a/platform/core-impl/src/com/intellij/util/CachedValueChecker.java +++ b/platform/core-impl/src/com/intellij/util/CachedValueChecker.java @@ -15,12 +15,21 @@ */ package com.intellij.util; +import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.UserDataHolder; import com.intellij.psi.PsiElement; import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.DebugReflectionUtil.BackLink; import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; import java.util.Set; @@ -30,27 +39,50 @@ import java.util.Set; */ class CachedValueChecker { private static final Logger LOG = Logger.getInstance("#com.intellij.util.CachedValueChecker"); - private static final boolean DO_CHECKS = ApplicationManager.getApplication().isUnitTestMode(); - private static Set ourCheckedClasses = ContainerUtil.newConcurrentSet(); + private static final boolean DO_CHECKS = ApplicationManager.getApplication().isUnitTestMode() || + ApplicationManager.getApplication().isInternal(); + private static final Set ourCheckedKeys = ContainerUtil.newConcurrentSet(); - static void checkProvider(CachedValueProvider provider, UserDataHolder userDataHolder) { + static void checkProvider(@NotNull final CachedValueProvider provider, + @NotNull final Key key, + @NotNull final UserDataHolder userDataHolder) { if (!DO_CHECKS) return; + if (!ourCheckedKeys.add(key.toString())) return; // store strings because keys are created afresh in each (test) project - Class providerClass = provider.getClass(); - if (!ourCheckedClasses.add(providerClass)) return; - - for (Field field : providerClass.getDeclaredFields()) { - try { - field.setAccessible(true); - Object o = field.get(provider); - if (o instanceof PsiElement && o != userDataHolder) { - LOG.error("Incorrect CachedValue use. Provider references PSI, causing memory leaks and possible invalid element access: field " + field.getName() + " of " + provider); - return; - } - } - catch (IllegalAccessException e) { - LOG.error(e); - } + Set visited = ContainerUtil.newIdentityTroveSet(); + BackLink path = findReferencedPsi(provider, userDataHolder, 6, visited, null); + if (path != null) { + LOG.error("Incorrect CachedValue use. Provider references PSI, causing memory leaks and possible invalid element access, provider=" + + provider + "\n" + path); } } + + @Nullable + private static BackLink findReferencedPsi(@NotNull Object o, + @Nullable final UserDataHolder toIgnore, + final int depth, + @NotNull final Set visited, + @Nullable final BackLink backLink) { + if (depth == 0 || o == toIgnore || !visited.add(o)) return null; + if (o instanceof Project || o instanceof Module || o instanceof Application) return null; + if (o instanceof PsiElement) { + if (toIgnore instanceof PsiElement && + ((PsiElement)toIgnore).getContainingFile() != null && + PsiTreeUtil.isAncestor((PsiElement)o, (PsiElement)toIgnore, true)) { + // allow to capture PSI parents, assuming that they stay valid at least as long as the element itself + return null; + } + return backLink; + } + + final Ref result = Ref.create(); + DebugReflectionUtil.processStronglyReferencedValues(o, new PairProcessor() { + @Override + public boolean process(Object next, Field field) { + result.set(findReferencedPsi(next, toIgnore, depth - 1, visited, new BackLink(next, field, backLink))); + return result.isNull(); + } + }); + return result.get(); + } } diff --git a/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java b/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java index 9253688da18e..3b9016e74677 100644 --- a/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java +++ b/platform/core-impl/src/com/intellij/util/CachedValuesManagerImpl.java @@ -54,7 +54,7 @@ public class CachedValuesManagerImpl extends CachedValuesManager { @NotNull Key> key, @NotNull CachedValueProvider provider, boolean trackValue) { - CachedValueChecker.checkProvider(provider, dataHolder); + CachedValueChecker.checkProvider(provider, key, dataHolder); CachedValue value; if (dataHolder instanceof UserDataHolderEx) { UserDataHolderEx dh = (UserDataHolderEx)dataHolder; diff --git a/platform/core-impl/src/com/intellij/util/DebugReflectionUtil.java b/platform/core-impl/src/com/intellij/util/DebugReflectionUtil.java new file mode 100644 index 000000000000..21f93312a751 --- /dev/null +++ b/platform/core-impl/src/com/intellij/util/DebugReflectionUtil.java @@ -0,0 +1,175 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.util; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.concurrency.AtomicFieldUpdater; +import com.intellij.util.containers.FList; +import gnu.trove.THashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import sun.misc.Unsafe; + +import java.lang.ref.Reference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class DebugReflectionUtil { + private static final Map allFields = new THashMap(); + private static final Field[] EMPTY_FIELD_ARRAY = new Field[0]; + private static final Method Unsafe_shouldBeInitialized = ReflectionUtil.getDeclaredMethod(Unsafe.class, "shouldBeInitialized", Class.class); + + @NotNull + private static Field[] getAllFields(@NotNull Class aClass) { + Field[] cached = allFields.get(aClass); + if (cached == null) { + try { + Field[] declaredFields = aClass.getDeclaredFields(); + List fields = new ArrayList(declaredFields.length + 5); + for (Field declaredField : declaredFields) { + declaredField.setAccessible(true); + Class type = declaredField.getType(); + if (isTrivial(type)) continue; // unable to hold references, skip + fields.add(declaredField); + } + Class superclass = aClass.getSuperclass(); + if (superclass != null) { + for (Field sup : getAllFields(superclass)) { + if (!fields.contains(sup)) { + fields.add(sup); + } + } + } + cached = fields.isEmpty() ? EMPTY_FIELD_ARRAY : fields.toArray(new Field[fields.size()]); + } + catch (IncompatibleClassChangeError e) { + //this exception may be thrown because there are two different versions of org.objectweb.asm.tree.ClassNode from different plugins + //I don't see any sane way to fix it until we load all the plugins by the same classloader in tests + cached = EMPTY_FIELD_ARRAY; + } + catch (SecurityException e) { + cached = EMPTY_FIELD_ARRAY; + } + catch (NoClassDefFoundError e) { + cached = EMPTY_FIELD_ARRAY; + } + + allFields.put(aClass, cached); + } + return cached; + } + + private static boolean isTrivial(@NotNull Class type) { + return type.isPrimitive() || type == String.class || type == Class.class || type.isArray() && isTrivial(type.getComponentType()); + } + + public static boolean processStronglyReferencedValues(@NotNull Object root, PairProcessor processor) { + Class rootClass = root.getClass(); + for (Field field : getAllFields(rootClass)) { + String fieldName = field.getName(); + if (root instanceof Reference && "referent".equals(fieldName)) continue; // do not follow weak/soft refs + Object value; + try { + value = field.get(root); + } + catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + if (value == null) continue; + if (!processor.process(value, field)) return false; + } + if (rootClass.isArray()) { + try { + //noinspection ConstantConditions + for (Object o : (Object[])root) { + if (o == null) continue; + if (isTrivial(o.getClass())) continue; + if (!processor.process(o, null)) return false; + } + } + catch (ClassCastException ignored) { + } + } + // check for objects leaking via static fields. process classes which already were initialized only + if (root instanceof Class && isLoadedAlready((Class)root)) { + try { + for (Field field : getAllFields((Class)root)) { + if ((field.getModifiers() & Modifier.STATIC) == 0) continue; + Object value = field.get(null); + if (value == null) continue; + if (!processor.process(value, field)) return false; + } + } + catch (IllegalAccessException ignored) { + } + } + return true; + } + + private static boolean isLoadedAlready(Class root) { + if (Unsafe_shouldBeInitialized == null) return false; + boolean isLoadedAlready = false; + try { + isLoadedAlready = !(Boolean)Unsafe_shouldBeInitialized.invoke(AtomicFieldUpdater.getUnsafe(), root); + } + catch (Exception ignored) { + } + //AtomicFieldUpdater.getUnsafe().shouldBeInitialized((Class)root); + return isLoadedAlready; + } + + public static class BackLink { + public final Object value; + private final Field field; + private final BackLink backLink; + + public BackLink(@NotNull Object value, @Nullable Field field, @Nullable BackLink backLink) { + this.value = value; + this.field = field; + this.backLink = backLink; + } + + @Override + public String toString() { + BackLink backLink = this; + String result = ""; + while (backLink != null) { + String valueStr; + try { + valueStr = backLink.value instanceof FList + ? "FList" : backLink.value instanceof Collection ? "Collection" : String.valueOf(backLink.value); + valueStr = StringUtil.trimLog(StringUtil.convertLineSeparators(valueStr, "//"), 100); + } + catch (Throwable e) { + valueStr = "(" + e.getMessage() + " while computing .toString())"; + } + Field field = backLink.field; + String fieldStr = field == null ? "?" : field.getName() + " of " + field.getDeclaringClass(); + result += "via " + fieldStr + "; Value: " + valueStr + " of " + backLink.value.getClass() + "\n"; + backLink = backLink.backLink; + } + return result; + } + } +} diff --git a/platform/testFramework/src/com/intellij/testFramework/LeakHunter.java b/platform/testFramework/src/com/intellij/testFramework/LeakHunter.java index 28a302765a83..3aac1cc0dfdb 100644 --- a/platform/testFramework/src/com/intellij/testFramework/LeakHunter.java +++ b/platform/testFramework/src/com/intellij/testFramework/LeakHunter.java @@ -22,181 +22,60 @@ import com.intellij.openapi.project.impl.ProjectImpl; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.UserDataHolder; import com.intellij.openapi.util.UserDataHolderEx; +import com.intellij.util.DebugReflectionUtil; +import com.intellij.util.DebugReflectionUtil.BackLink; +import com.intellij.util.PairProcessor; import com.intellij.util.Processor; import com.intellij.util.ReflectionUtil; -import com.intellij.util.concurrency.AtomicFieldUpdater; -import com.intellij.util.containers.FList; import com.intellij.util.containers.Queue; import com.intellij.util.io.PersistentEnumeratorBase; import com.intellij.util.ui.UIUtil; -import gnu.trove.THashMap; import gnu.trove.TIntHashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -import sun.misc.Unsafe; import javax.swing.*; -import java.lang.ref.Reference; import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.*; /** * User: cdr */ public class LeakHunter { - private static final Map allFields = new THashMap(); - private static final Field[] EMPTY_FIELD_ARRAY = new Field[0]; - @NotNull - private static Field[] getAllFields(@NotNull Class aClass) { - Field[] cached = allFields.get(aClass); - if (cached == null) { - try { - Field[] declaredFields = aClass.getDeclaredFields(); - List fields = new ArrayList(declaredFields.length + 5); - for (Field declaredField : declaredFields) { - declaredField.setAccessible(true); - Class type = declaredField.getType(); - if (isTrivial(type)) continue; // unable to hold references, skip - fields.add(declaredField); - } - Class superclass = aClass.getSuperclass(); - if (superclass != null) { - for (Field sup : getAllFields(superclass)) { - if (!fields.contains(sup)) { - fields.add(sup); - } - } - } - cached = fields.isEmpty() ? EMPTY_FIELD_ARRAY : fields.toArray(new Field[fields.size()]); - } - catch (IncompatibleClassChangeError e) { - //this exception may be thrown because there are two different versions of org.objectweb.asm.tree.ClassNode from different plugins - //I don't see any sane way to fix it until we load all the plugins by the same classloader in tests - cached = EMPTY_FIELD_ARRAY; - } - catch (SecurityException e) { - cached = EMPTY_FIELD_ARRAY; - } - catch (NoClassDefFoundError e) { - cached = EMPTY_FIELD_ARRAY; - } - - allFields.put(aClass, cached); - } - return cached; - } - - private static boolean isTrivial(@NotNull Class type) { - return type.isPrimitive() || type == String.class || type == Class.class || type.isArray() && isTrivial(type.getComponentType()); - } - - private static class BackLink { - private final Object value; - private final Field field; - private final BackLink backLink; - - private BackLink(@NotNull Object value, Field field, BackLink backLink) { - this.value = value; - this.field = field; - this.backLink = backLink; - } - } - - private static void walkObjects(@NotNull Class lookFor, - @NotNull Collection startRoots, - @NotNull Processor leakProcessor) { + private static void walkObjects(@NotNull final Class lookFor, @NotNull Collection startRoots, @NotNull final Processor leakProcessor) { TIntHashSet visited = new TIntHashSet(); - Queue toVisit = new Queue(1000000); + final Queue toVisit = new Queue(1000000); for (Object startRoot : startRoots) { toVisit.addLast(new BackLink(startRoot, null, null)); } while (true) { if (toVisit.isEmpty()) return; - BackLink backLink = toVisit.pullFirst(); + final BackLink backLink = toVisit.pullFirst(); Object root = backLink.value; if (!visited.add(System.identityHashCode(root))) continue; - Class rootClass = root.getClass(); - for (Field field : getAllFields(rootClass)) { - String fieldName = field.getName(); - if (root instanceof Reference && "referent".equals(fieldName)) continue; // do not follow weak/soft refs - Object value; - try { - value = field.get(root); - } - catch (IllegalArgumentException e) { - throw new RuntimeException(e); - } - catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - if (value == null) continue; - Class valueClass = value.getClass(); - if (lookFor.isAssignableFrom(valueClass) && isReallyLeak(field, fieldName, value, valueClass)) { - BackLink newBackLink = new BackLink(value, field, backLink); - leakProcessor.process(newBackLink); - } - else { - BackLink newBackLink = new BackLink(value, field, backLink); - toVisit.addLast(newBackLink); - } - } - if (rootClass.isArray()) { - try { - for (Object o : (Object[])root) { - if (o == null) continue; - if (isTrivial(o.getClass())) continue; - toVisit.addLast(new BackLink(o, null, backLink)); + DebugReflectionUtil.processStronglyReferencedValues(root, new PairProcessor() { + @Override + public boolean process(Object value, Field field) { + Class valueClass = value.getClass(); + if (lookFor.isAssignableFrom(valueClass) && isReallyLeak(value)) { + leakProcessor.process(new BackLink(value, field, backLink)); } - } - catch (ClassCastException ignored) { - } - } - // check for objects leaking via static fields. process classes which already were initialized only - if (root instanceof Class && isLoadedAlready((Class)root)) { - try { - for (Field field : getAllFields((Class)root)) { - if ((field.getModifiers() & Modifier.STATIC) == 0) continue; - Object value = field.get(null); - if (value == null) continue; - Class valueClass = value.getClass(); - if (lookFor.isAssignableFrom(valueClass) && isReallyLeak(field, field.getName(), value, valueClass)) { - BackLink newBackLink = new BackLink(value, field, backLink); - leakProcessor.process(newBackLink); - } - else { - BackLink newBackLink = new BackLink(value, field, backLink); - toVisit.addLast(newBackLink); - } + else { + toVisit.addLast(new BackLink(value, field, backLink)); } + return true; } - catch (IllegalAccessException ignored) { - } - } + }); } } - private static final Method Unsafe_shouldBeInitialized = ReflectionUtil.getDeclaredMethod(Unsafe.class, "shouldBeInitialized", Class.class); - private static boolean isLoadedAlready(Class root) { - if (Unsafe_shouldBeInitialized == null) return false; - boolean isLoadedAlready = false; - try { - isLoadedAlready = !(Boolean)Unsafe_shouldBeInitialized.invoke(AtomicFieldUpdater.getUnsafe(), root); - } - catch (Exception ignored) { - } - //AtomicFieldUpdater.getUnsafe().shouldBeInitialized((Class)root); - return isLoadedAlready; - } - private static final Key IS_NOT_A_LEAK = Key.create("IS_NOT_A_LEAK"); public static void markAsNotALeak(@NotNull UserDataHolder object) { object.putUserData(IS_NOT_A_LEAK, Boolean.TRUE); } - private static boolean isReallyLeak(Field field, String fieldName, Object value, Class valueClass) { + private static boolean isReallyLeak(Object value) { return !(value instanceof UserDataHolder) || ((UserDataHolder)value).getUserData(IS_NOT_A_LEAK) == null; } @@ -243,17 +122,7 @@ public class LeakHunter { String place = leaked instanceof Project ? PlatformTestCase.getCreationPlace((Project)leaked) : ""; System.out.println("Leaked object found:" + leaked + "; hash: " + System.identityHashCode(leaked) + "; place: " + place); - while (backLink != null) { - String valueStr; - try { - valueStr = backLink.value instanceof FList ? "FList" : backLink.value instanceof Collection ? "Collection" : String.valueOf(backLink.value); - } - catch (Throwable e) { - valueStr = "(" + e.getMessage() + " while computing .toString())"; - } - System.out.println("-->" + backLink.field + "; Value: " + valueStr + "; " + backLink.value.getClass()); - backLink = backLink.backLink; - } + System.out.println(backLink); System.out.println(";-----"); throw new AssertionError();