-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HHH-16911 Introduce a testing utility to spot memory leaks
- Loading branch information
Showing
2 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
46 changes: 46 additions & 0 deletions
46
...test/java/org/hibernate/orm/test/bootstrap/registry/classloading/LeakUtilitySelfTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* Hibernate, Relational Persistence for Idiomatic Java | ||
* | ||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later | ||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html | ||
*/ | ||
package org.hibernate.orm.test.bootstrap.registry.classloading; | ||
|
||
import org.junit.Assert; | ||
import org.junit.Test; | ||
|
||
/** | ||
* Tests the testing utility PhantomReferenceLeakDetector | ||
*/ | ||
public class LeakUtilitySelfTest { | ||
|
||
@Test | ||
public void verifyLeakUtility() { | ||
PhantomReferenceLeakDetector.assertActionNotLeaking( LeakUtilitySelfTest::notALeak ); | ||
} | ||
|
||
@Test | ||
public void verifyLeakUtilitySpotsLeak() { | ||
Assert.assertFalse( PhantomReferenceLeakDetector.verifyActionNotLeaking( LeakUtilitySelfTest::troubleSomeLeak, 2, 1 ) ); | ||
} | ||
|
||
private static SomeSpecialObject notALeak() { | ||
return new SomeSpecialObject(); | ||
} | ||
|
||
private static SomeSpecialObject troubleSomeLeak() { | ||
final SomeSpecialObject specialThing = new SomeSpecialObject(); | ||
tl.set( specialThing ); | ||
return specialThing; | ||
} | ||
|
||
private static final ThreadLocal tl = new ThreadLocal<>(); | ||
|
||
static class SomeSpecialObject { | ||
@Override | ||
public String toString() { | ||
return "this is some hypothetical critical object"; | ||
} | ||
} | ||
|
||
} |
104 changes: 104 additions & 0 deletions
104
.../org/hibernate/orm/test/bootstrap/registry/classloading/PhantomReferenceLeakDetector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
* Hibernate, Relational Persistence for Idiomatic Java | ||
* | ||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later | ||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html | ||
*/ | ||
package org.hibernate.orm.test.bootstrap.registry.classloading; | ||
|
||
import java.lang.ref.PhantomReference; | ||
import java.lang.ref.Reference; | ||
import java.lang.ref.ReferenceQueue; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.function.Supplier; | ||
|
||
import org.junit.Assert; | ||
|
||
/** | ||
* Utility to help verify that a certain object is free | ||
* to be garbage collected (that we're not leaking it). | ||
* This is particularly useful with Classloaders. | ||
* | ||
* @author Sanne Grinovero (C) 2023 Red Hat Inc. | ||
*/ | ||
public class PhantomReferenceLeakDetector { | ||
|
||
/** | ||
* A single second should be more than enough; this might need | ||
* to be tuned for particularly slow systems to avoid | ||
* flaky tests, but I personally believe it's very | ||
* large: we default to a very generous amount as we won't | ||
* normally wait for this long, unless there is a problem. | ||
* So consider carefully if there's not a deeper | ||
* problem before setting these to even larger amounts. | ||
*/ | ||
private static final int MAX_TOTAL_WAIT_SECONDS = 180; | ||
private static final int GC_ATTEMPTS = MAX_TOTAL_WAIT_SECONDS * 5; | ||
|
||
/** | ||
* Asserts that a certain operation won't be leaking | ||
* a particular object of type T. | ||
* The operation being tested needs to implement {@link Supplier} and | ||
* return a reference to the object to monitor; we expect | ||
* this object to be eligible for garbage collection soon after the | ||
* action is completed, and therefore great care must be taken | ||
* for the test itself to not leak a reference to such object | ||
* either, including on the caller's stack; this implies it | ||
* might be necessary to explicitly null local variables | ||
* if the test infrastructure is referring to the critical object. | ||
* For an object to not be considered leaked, it must be | ||
* garbage collected in a reasonable time after the action; since | ||
* we rely on the GC operation, which is asynchronous and not deterministic, | ||
* it's possible that this test could fail even without a real leak; | ||
* to prevent flaky tests we use a very generously sized timeout and | ||
* we might trigger multiple GC events. | ||
* This approach implies that a failing assertion might not necessarily | ||
* signal that there definitively is a leak, but the test not failing | ||
* should imply we're definitively fine. | ||
* If a test using this utility were to suddenly start failing | ||
* beware of raising the timeouts without investigating: if the object | ||
* is eventually garbage collected but taking an unusual amount of time, | ||
* that's also a sign of something not being quite right. | ||
*/ | ||
public static <T> void assertActionNotLeaking(Supplier<T> action) { | ||
Assert.assertTrue("Operation apparently leaked the critical resource", | ||
verifyActionNotLeaking( action, | ||
GC_ATTEMPTS, | ||
MAX_TOTAL_WAIT_SECONDS ) | ||
); | ||
} | ||
|
||
/** | ||
* Exposed for self-testing w/o having to wait for the regular timeout | ||
*/ | ||
static <T> boolean verifyActionNotLeaking(Supplier<T> action, final int gcAttempts, final int totalWaitSeconds ) { | ||
T criticalReference = action.get(); | ||
final ReferenceQueue<T> referenceQueue = new ReferenceQueue<>(); | ||
final PhantomReference<T> reference = new PhantomReference<>( criticalReference, referenceQueue ); | ||
//Ignore IDE's suggestion to remove the following line: we really need it! | ||
// (it could be inlined above, but I prefer this style so that it serves as an example for | ||
// future maintenance of how this works) | ||
criticalReference = null; | ||
return verifyCollection( referenceQueue, gcAttempts, totalWaitSeconds ); | ||
} | ||
|
||
private static <T> boolean verifyCollection(final ReferenceQueue<T> referenceQueue, final int gcAttempts, final int totalWaitSeconds) { | ||
final int millisEachAttempt = Math.round((float) TimeUnit.SECONDS.toMillis( totalWaitSeconds ) / gcAttempts ); | ||
for ( int i = 0; i < gcAttempts; i++ ) { | ||
Runtime.getRuntime().gc(); | ||
try { | ||
Reference<?> ref = referenceQueue.remove( millisEachAttempt ); | ||
if ( ref != null ) { | ||
return true; | ||
} | ||
} | ||
catch ( InterruptedException e ) { | ||
//let's try another GC: if there's complex finalizers on the path to the object | ||
//that needs to be tested a single GC cycle might not be enough for it to get collected. | ||
//(We don't expect any complex finalizer so we won't be waiting too much either..) | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
} |