assert that CachedValueProvider-s don't depend on wrong PSI elements, even if indirectly

This commit is contained in:
peter
2015-11-13 08:12:17 +01:00
parent 394e2f20c7
commit 00f0c9cbd7
4 changed files with 244 additions and 168 deletions
@@ -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<Class> ourCheckedClasses = ContainerUtil.newConcurrentSet();
private static final boolean DO_CHECKS = ApplicationManager.getApplication().isUnitTestMode() ||
ApplicationManager.getApplication().isInternal();
private static final Set<String> 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<? extends CachedValueProvider> 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<Object> 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<Object> 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<BackLink> result = Ref.create();
DebugReflectionUtil.processStronglyReferencedValues(o, new PairProcessor<Object, Field>() {
@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();
}
}
@@ -54,7 +54,7 @@ public class CachedValuesManagerImpl extends CachedValuesManager {
@NotNull Key<CachedValue<T>> key,
@NotNull CachedValueProvider<T> provider,
boolean trackValue) {
CachedValueChecker.checkProvider(provider, dataHolder);
CachedValueChecker.checkProvider(provider, key, dataHolder);
CachedValue<T> value;
if (dataHolder instanceof UserDataHolderEx) {
UserDataHolderEx dh = (UserDataHolderEx)dataHolder;
@@ -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<Class, Field[]> allFields = new THashMap<Class, Field[]>();
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<Field> fields = new ArrayList<Field>(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<Object, Field> 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;
}
}
}
@@ -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<Class, Field[]> allFields = new THashMap<Class, Field[]>();
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<Field> fields = new ArrayList<Field>(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<Object> startRoots,
@NotNull Processor<BackLink> leakProcessor) {
private static void walkObjects(@NotNull final Class<?> lookFor, @NotNull Collection<Object> startRoots, @NotNull final Processor<BackLink> leakProcessor) {
TIntHashSet visited = new TIntHashSet();
Queue<BackLink> toVisit = new Queue<BackLink>(1000000);
final Queue<BackLink> toVisit = new Queue<BackLink>(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<Object, Field>() {
@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<Boolean> 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();