blob: 3b7d0e1d14a6d8b02bebe611b8a5964ea1bbeda3 [file] [log] [blame]
/*
* Copyright 2000-2014 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.testFramework;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.impl.ProjectImpl;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FList;
import com.intellij.util.containers.Stack;
import com.intellij.util.io.PersistentEnumerator;
import com.intellij.util.ui.UIUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 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];
public static final Processor<Project> NOT_DEFAULT_PROJECT = new Processor<Project>() {
@Override
public boolean process(Project project) {
return !project.isDefault();
}
};
private static Field[] getAllFields(@NotNull Class aClass) {
Field[] cached = allFields.get(aClass);
if (cached == null) {
Field[] declaredFields = aClass.getDeclaredFields();
List<Field> fields = new ArrayList<Field>(declaredFields.length + 5);
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
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()]);
allFields.put(aClass, cached);
}
return cached;
}
private static final Set<Object> visited = ContainerUtil.<Object>newIdentityTroveSet();
private static class BackLink {
private final Class aClass;
private final Object value;
private final Field field;
private final BackLink backLink;
private BackLink(@NotNull Class aClass, @NotNull Object value, Field field, BackLink backLink) {
this.aClass = aClass;
this.value = value;
this.field = field;
this.backLink = backLink;
}
}
private static final Stack<BackLink> toVisit = new Stack<BackLink>();
private static void walkObjects(@NotNull Class lookFor, @NotNull Processor<BackLink> leakProcessor) {
while (true) {
if (toVisit.isEmpty()) return;
BackLink backLink = toVisit.pop();
Object root = backLink.value;
if (!visited.add(root)) continue;
Class rootClass = backLink.aClass;
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(valueClass, value, field, backLink);
leakProcessor.process(newBackLink);
}
else {
BackLink newBackLink = new BackLink(valueClass, value, field, backLink);
if (toFollow(valueClass)) {
toVisit.push(newBackLink);
}
}
}
if (rootClass.isArray()) {
if (toFollow(rootClass.getComponentType())) {
try {
for (Object o : (Object[])root) {
if (o == null) continue;
Class oClass = o.getClass();
toVisit.push(new BackLink(oClass, o, null, backLink));
}
}
catch (ClassCastException ignored) {
}
}
}
}
}
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) {
return !(value instanceof UserDataHolder) || ((UserDataHolder)value).getUserData(IS_NOT_A_LEAK) == null;
}
private static final Set<String> noFollowClasses = new THashSet<String>();
static {
noFollowClasses.add("java.lang.Boolean");
noFollowClasses.add("java.lang.Byte");
noFollowClasses.add("java.lang.Class");
noFollowClasses.add("java.lang.Character");
noFollowClasses.add("java.lang.Double");
noFollowClasses.add("java.lang.Float");
noFollowClasses.add("java.lang.Integer");
noFollowClasses.add("java.lang.Long");
noFollowClasses.add("java.lang.Object");
noFollowClasses.add("java.lang.Short");
noFollowClasses.add("java.lang.String");
}
private static boolean toFollow(Class oClass) {
String name = oClass.getName();
return !noFollowClasses.contains(name);
}
private static final Key<Boolean> REPORTED_LEAKED = Key.create("REPORTED_LEAKED");
@TestOnly
public static void checkProjectLeak() throws Exception {
checkLeak(ApplicationManager.getApplication(), ProjectImpl.class);
checkLeak(Extensions.getRootArea(), ProjectImpl.class, NOT_DEFAULT_PROJECT);
}
@TestOnly
public static void checkLeak(@NotNull Object root, @NotNull Class suspectClass) throws AssertionError {
checkLeak(root, suspectClass, null);
}
@TestOnly
public static <T> void checkLeak(@NotNull Object root, @NotNull Class<T> suspectClass, @Nullable final Processor<? super T> isReallyLeak) throws AssertionError {
if (SwingUtilities.isEventDispatchThread()) {
UIUtil.dispatchAllInvocationEvents();
}
else {
UIUtil.pump();
}
PersistentEnumerator.clearCacheForTests();
toVisit.clear();
visited.clear();
toVisit.push(new BackLink(root.getClass(), root, null,null));
try {
walkObjects(suspectClass, new Processor<BackLink>() {
@Override
public boolean process(BackLink backLink) {
UserDataHolder leaked = (UserDataHolder)backLink.value;
if (((UserDataHolderBase)leaked).replace(REPORTED_LEAKED,null,Boolean.TRUE) && (isReallyLeak == null || isReallyLeak.process((T)leaked))) {
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" : String.valueOf(backLink.value);
}
catch (Throwable e) {
valueStr = "("+e.getMessage()+" while computing .toString())";
}
System.out.println("-->"+backLink.field+"; Value: "+ valueStr +"; "+backLink.aClass);
backLink = backLink.backLink;
}
System.out.println(";-----");
throw new AssertionError();
}
return true;
}
});
}
finally {
visited.clear();
((THashSet)visited).compact();
toVisit.clear();
toVisit.trimToSize();
}
}
}