blob: 6fbd9611fad5db6fdc73e00e46f6785bcd2b52bf [file] [log] [blame]
/*
* Copyright (c) 2014, 2015 CEA, Christian W. Damus, and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Christian W. Damus (CEA) - Initial API and implementation
* Christian W. Damus - bug 451013
* Christian W. Damus - bug 483721
*
*/
package org.eclipse.papyrus.junit.framework.classification.rules;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* A simple JUnit rule for tracking memory leaks. Simply {@linkplain #add(Object) add objects} during your test execution, make assertions if desired,
* and on successful completion of the body of the test, this rule verifies that none of the tracked objects have leaked.
* Tests that are sensitive to references being retained temporarily via {@link SoftReference}s should be annotated as {@link SoftReferenceSensitive
* @SoftReferenceSensitive} so that the rule may employ extra measures to ensure that soft references are cleared.
*
* @see SoftReferenceSensitive
*/
public class MemoryLeakRule extends TestWatcher {
private static final boolean DEBUG = Boolean.getBoolean("MemoryLeakRule.debug");
private static final int DEQUEUE_REF_ITERATIONS = 3;
private static final int DEQUEUE_REF_TIMEOUT = 1000; // Millis
private static final int GC_ITERATIONS = 10;
private static final int CLEAR_SOFT_REFS_ITERATIONS = 3;
private static final Map<Class<?>, Boolean> WARMED_UP_SUITES = new WeakHashMap<Class<?>, Boolean>();
private static boolean warmingUp;
private ReferenceQueue<Object> queue;
private List<WeakReference<Object>> tracker;
private String testName;
private Class<?> testClass;
private boolean isSoftReferenceSensitive;
public MemoryLeakRule() {
super();
}
public void add(Object leak) {
assertThat("Cannot track null references for memory leaks.", leak, notNullValue());
if (queue == null) {
queue = new ReferenceQueue<Object>();
tracker = Lists.newArrayList();
}
tracker.add(new WeakReference<Object>(leak, queue));
}
public String getTestName() {
return testName;
}
@Override
protected void starting(Description description) {
testName = description.getMethodName();
testClass = description.getTestClass();
isSoftReferenceSensitive = description.getAnnotation(SoftReferenceSensitive.class) != null;
if (isSoftReferenceSensitive && !isWarmedUp() && !warmingUp) {
// Warm up the soft-reference sensitive tests by running this one up-front, first,
// because the first such test to execute always results in a spurious failure
// (at least, such is the case on the Mac build of JRE 1.6)
warmingUp = true;
try {
warmUp();
} finally {
warmingUp = false;
}
}
}
@Override
protected void succeeded(Description description) {
// If the test's assertions (if any) all succeeded, then check for leaks on the way out
if (tracker == null) {
// No leaks to assert
return;
}
// Assert that our tracked objects are now all unreachable
while (!tracker.isEmpty()) {
Reference<?> ref = dequeueTracker();
for (int i = 0; ((ref == null) && isSoftReferenceSensitive) && (i < CLEAR_SOFT_REFS_ITERATIONS); i++) {
// Maybe there are soft references retaining our objects? Desperation move.
// On some platforms, our simulated OOME doesn't actually purge all soft
// references (contrary to Java spec!), so we have to repeat
forceClearSoftReferenceCaches();
// Try once more
ref = dequeueTracker();
}
if (!tracker.remove(ref) && !tracker.isEmpty()) {
// The remaining tracked elements are leaked
final String leaks = Joiner.on('\n').join(Iterables.transform(tracker, label()));
if (warmingUp) {
debug("Warm-up detected leaks: %s%n", leaks.replace('\n', ' '));
}
fail("One or more objects leaked:\n" + leaks);
break; // Unreachable
}
}
}
@Override
protected void finished(Description description) {
// Clean up
tracker = null;
queue = null;
}
Reference<?> dequeueTracker() {
Reference<?> result = null;
try {
for (int i = 0; (result == null) && (i < DEQUEUE_REF_ITERATIONS); i++) {
// Try to force GC
collectGarbage();
result = queue.remove(DEQUEUE_REF_TIMEOUT);
}
} catch (InterruptedException e) {
e.printStackTrace();
fail("JUnit was interrupted");
}
return result;
}
Function<WeakReference<?>, String> label() {
return new Function<WeakReference<?>, String>() {
@Override
public String apply(WeakReference<?> input) {
return label(input.get());
}
};
}
String label(Object input) {
String result = null;
if (!(input instanceof EObject)) {
result = String.valueOf(input);
} else {
EObject object = (EObject) input;
EClass eclass = object.eClass();
String label = null;
EStructuralFeature nameFeature = eclass.getEStructuralFeature("name"); //$NON-NLS-1$
if (nameFeature != null) {
label = String.valueOf(object.eGet(nameFeature));
} else {
// Look for anything label-like
for (EAttribute next : eclass.getEAllAttributes()) {
if (!next.isMany() && next.getEAttributeType().getInstanceClass() == String.class) {
label = (String) object.eGet(next);
if ((label != null) && !label.isEmpty()) {
break;
}
}
}
}
result = String.format("<%s> %s", eclass.getName(), label);
}
return result;
}
void collectGarbage() {
// Try a few times to decrease the amount of used heap space
final Runtime rt = Runtime.getRuntime();
Long usedMem = rt.totalMemory() - rt.freeMemory();
Long prevUsedMem = usedMem;
for (int i = 0; (prevUsedMem <= usedMem) && (i < GC_ITERATIONS); i++) {
rt.gc();
Thread.yield();
prevUsedMem = usedMem;
usedMem = rt.totalMemory() - rt.freeMemory();
}
}
void forceClearSoftReferenceCaches() {
// There are components in the Eclipse workbench that maintain soft references to objects for
// performance caches. For example, the the Common Navigator Framework used by Model Explorer
// caches mappings of elements in the tree to the content extensions that provided them using
// EvalutationReferences [sic] that are SoftReferences
// This is a really gross HACK and runs the risk that some other thread(s) also may see OOMEs!
try {
List<Object[]> hog = Lists.newLinkedList();
for (;;) {
hog.add(new Object[getLargeMemorySize()]);
}
} catch (OutOfMemoryError e) {
// Good! The JVM guarantees that all soft references are cleared before throwing OOME,
// so we can be assured that they are now cleared
} finally {
if (warmingUp) {
// We have successfully warmed up the soft-references hack
WARMED_UP_SUITES.put(testClass, true);
}
}
}
private static int getLargeMemorySize() {
// These 64 megs are multiplied by the size of a pointer!
return 64 * 1024 * 1024;
}
private boolean isWarmedUp() {
return Boolean.TRUE.equals(WARMED_UP_SUITES.get(testClass));
}
private void warmUp() {
// The first test that relies on the soft-reference clearing hack will
// always fail, so run such a test once up-front. Call this a metahack
try {
debug("Warming up test suite: %s (%s)%n", testClass.getName(), testName);
new JUnitCore().run(Request.method(testClass, testName));
} catch (Exception e) {
// Fine, so the warm-up didn't work
e.printStackTrace();
}
}
private static void debug(String format, Object... args) {
if (DEBUG) {
System.err.printf("[MEM] " + format, args);
}
}
//
// Nested types
//
/**
* Annotates a test that is sensitive to references being cached by {@link SoftReference}s.
* Such tests will take additional drastic measures to try to force the JVM to clear soft
* reference caches in order to release all possible references to objects tracked for leaks.
* Because the first such test is expected always to result in a spurious failure (at least,
* such is the case on the Mac OS X build of J2SE 1.6), the rule "warms up" the test suite
* by running one such test in isolation before running any others.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public static @interface SoftReferenceSensitive {
// Empty annotation
}
}