package jalview.gui; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.io.PrintStream; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import jalview.analysis.AlignmentGenerator; import jalview.bin.Cache; import jalview.bin.Jalview; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceGroup; import jalview.io.DataSourceType; import jalview.io.FileLoader; import junit.extensions.PA; /** * Provides a simple test that memory is released when all windows are closed. * * If the test fails, this means that reference(s) to large object(s) have * failed to be garbage collected. In this case: * *

*

Fixing memory leaks

*

* Experience shows that often a reference is retained (directly or indirectly) * by a Swing (or related) component (for example a {@code MouseListener} or * {@code ActionListener}). There are two possible approaches to fixing: *

* Add code to 'null unused large object references' until the test passes. For * a final sanity check, capture the heap dump for a passing test, and satisfy * yourself that only 'small' or 'harmless' {@code jalview} object instances * (such as enums or singletons) are left in the heap. */ public class FreeUpMemoryTest { private static final int ONE_MB = 1000 * 1000; /* * maximum retained heap usage (in MB) for a passing test */ private static int MAX_RESIDUAL_HEAP = 38; /** * Configure (read-only) Jalview property settings for test */ @BeforeClass(alwaysRun = true) public void setUp() { Jalview.main( new String[] { "-nonews", "-props", "test/jalview/testProps.jvprops" }); String True = Boolean.TRUE.toString(); Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", True); Cache.applicationProperties.setProperty("SHOW_QUALITY", True); Cache.applicationProperties.setProperty("SHOW_CONSERVATION", True); Cache.applicationProperties.setProperty("SHOW_OCCUPANCY", True); Cache.applicationProperties.setProperty("SHOW_IDENTITY", True); } @Test(groups = "Memory") public void testFreeMemoryOnClose() throws IOException { File f = generateAlignment(); f.deleteOnExit(); doStuffInJalview(f); Desktop.instance.closeAll_actionPerformed(null); checkUsedMemory(MAX_RESIDUAL_HEAP); } /** * Returns the current total used memory (available memory - free memory), * rounded down to the nearest MB * * @return */ private static int getUsedMemory() { long availableMemory = Runtime.getRuntime().totalMemory(); long freeMemory = Runtime.getRuntime().freeMemory(); long usedMemory = availableMemory - freeMemory; return (int) (usedMemory/ ONE_MB); } /** * Requests garbage collection and then checks whether remaining memory in use * is less than the expected value (in Megabytes) * * @param expectedMax */ protected void checkUsedMemory(int expectedMax) { /* * request garbage collection and wait for it to run (up to 3 times); * NB there is no guarantee when, or whether, it will do so */ long usedMemory = 0L; int gcCount = 0; while (gcCount < 3) { gcCount++; System.gc(); waitFor(1500); usedMemory = getUsedMemory(); if (usedMemory < expectedMax) { break; } } /* * if this assertion fails (reproducibly!) * - set a breakpoint here, conditional on (usedMemory > expectedMax) * - run VisualVM to inspect the heap usage, and run GC from VisualVM to check * it is not simply delayed garbage collection causing the test failure * - take a heap dump and identify large objects in the heap and their referers * - fix code as necessary to null the references on close */ System.out .println("Used memory after " + gcCount + " call(s) to gc() = " + usedMemory + "MB (should be <=" + expectedMax + ")"); assertTrue(usedMemory <= expectedMax, String.format( "Used memory %d should be less than %d (Recommend running test manually to verify)", usedMemory, expectedMax)); } /** * Loads an alignment from file and exercises various operations in Jalview * * @param f */ protected void doStuffInJalview(File f) { /* * load alignment, wait for consensus and other threads to complete */ AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(f.getPath(), DataSourceType.FILE); while (af.getViewport().isCalcInProgress()) { waitFor(200); } /* * open an Overview window */ af.overviewMenuItem_actionPerformed(null); assertNotNull(af.alignPanel.overviewPanel); /* * exercise the pop-up menu in the Overview Panel (JAL-2864) */ Object[] args = new Object[] { new MouseEvent(af, 0, 0, 0, 0, 0, 1, true) }; PA.invokeMethod(af.alignPanel.overviewPanel, "showPopupMenu(java.awt.event.MouseEvent)", args); /* * set a selection group - potential memory leak if it retains * a reference to the alignment */ SequenceGroup sg = new SequenceGroup(); sg.setStartRes(0); sg.setEndRes(100); AlignmentI al = af.viewport.getAlignment(); for (int i = 0; i < al.getHeight(); i++) { sg.addSequence(al.getSequenceAt(i), false); } af.viewport.setSelectionGroup(sg); /* * compute Tree and PCA (on all sequences, 100 columns) */ af.openTreePcaDialog(); CalculationChooser dialog = af.alignPanel.getCalculationDialog(); dialog.openPcaPanel("BLOSUM62", dialog.getSimilarityParameters(true)); dialog.openTreePanel("BLOSUM62", dialog.getSimilarityParameters(false)); /* * wait until Tree and PCA have been computed */ while (af.viewport.getCurrentTree() == null || dialog.getPcaPanel().isWorking()) { waitFor(10); } /* * give Swing time to add the PCA panel (?!?) */ waitFor(100); } /** * Wait for waitMs miliseconds * * @param waitMs */ protected void waitFor(int waitMs) { try { Thread.sleep(waitMs); } catch (InterruptedException e) { } } /** * Generates an alignment and saves it in a temporary file, to be loaded by * Jalview. We use a peptide alignment (so Conservation and Quality are * calculated), which is wide enough to ensure Consensus, Conservation and * Occupancy have a significant memory footprint (if not removed from the * heap). * * @return * @throws IOException */ private File generateAlignment() throws IOException { File f = File.createTempFile("MemoryTest", "fa"); PrintStream ps = new PrintStream(f); AlignmentGenerator ag = new AlignmentGenerator(false, ps); int width = 100000; int height = 100; ag.generate(width, height, 0, 10, 15); ps.close(); return f; } }