mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-06 06:21:16 +07:00
assert that CachedValueProvider-s don't depend on wrong PSI elements, even if indirectly
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user