Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 82732b159501b2eecb384d5a1e3507b7f5e3fc1c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*
 * Copyright (c) 2014 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
 *
 */
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.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.IItemLabelProvider;
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.base.Strings;
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 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;

	private ComposedAdapterFactory factory;

	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();
			factory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE);
		}

		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) {
					System.err.printf("[MEM] 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;
		disposeFactory();
	}

	void disposeFactory() {
		if(factory != null) {
			factory.dispose();
			factory = 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>() {

			public String apply(WeakReference<?> input) {
				return label(input.get());
			}
		};
	}

	String label(Object input) {
		IItemLabelProvider provider = (IItemLabelProvider)factory.adapt(input, IItemLabelProvider.class);
		String result = (provider == null) ? String.valueOf(input) : provider.getText(input);

		if(Strings.isNullOrEmpty(result)) {
			result = String.valueOf(input);
		}

		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 {
			System.err.printf("[MEM] 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();
		}
	}

	//
	// 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
	}
}

Back to the top