package jalview.gui;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
-import jalview.analysis.AlignmentGenerator;
-import jalview.bin.Cache;
-import jalview.bin.Jalview;
-import jalview.io.DataSourceType;
-import jalview.io.FileLoader;
-
+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.
+ * <ul>
+ * <li>generates a reasonably large alignment and loads it</li>
+ * <li>performs various operations on the alignment</li>
+ * <li>closes all windows</li>
+ * <li>requests garbage collection</li>
+ * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
+ * </li>
+ * </ul>
+ * If the test fails, this means that reference(s) to large object(s) have
+ * failed to be garbage collected. In this case:
+ * <ul>
+ * <li>set a breakpoint just before the test assertion in
+ * {@code checkUsedMemory}</li>
+ * <li>if the test fails intermittently, make this breakpoint conditional on
+ * {@code usedMemory > expectedMax}</li>
+ * <li>run the test to this point (and check that it is about to fail i.e.
+ * {@code usedMemory > expectedMax})</li>
+ * <li>use <a href="https://visualvm.github.io/">visualvm</a> to obtain a heap
+ * dump from the suspended process (and kill the test or let it fail)</li>
+ * <li>inspect the heap dump using visualvm for large objects and their
+ * referers</li>
+ * <li>Tips:</li>
+ * <ul>
+ * <li>Perform GC from the Monitor view in visualvm before requesting the heap
+ * dump - test failure might be simply a delay to GC</li>
+ * <li>View 'Objects' and filter classes to {@code jalview}. Sort columns by
+ * Count, or Size, and look for anything suspicious. For example, if the object
+ * count for {@code Sequence} is non-zero (it shouldn't be), pick any instance,
+ * and follow the chain of {@code references} to find which class(es) still hold
+ * references to sequence objects</li>
+ * <li>If this chain is impracticably long, re-run the test with a smaller
+ * alignment (set width=100, height=10 in {@code generateAlignment()}), to
+ * capture a heap which is qualitatively the same, but much smaller, so easier
+ * to analyse; note this requires an unconditional breakpoint</li>
+ * </ul>
+ * </ul>
+ * <p>
+ * <h2>Fixing memory leaks</h2>
+ * <p>
+ * 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:
+ * <ul>
+ * <li>Purist: ensure that all listeners and similar objects are removed when no
+ * longer needed. May be difficult, to achieve and to maintain as code
+ * changes.</li>
+ * <li>Pragmatic: null references to potentially large objects from Jalview
+ * application classes when no longer needed, typically when a panel is closed.
+ * This ensures that even if the JVM keeps a reference to a panel or viewport,
+ * it does not retain a large heap footprint. This is the approach taken in, for
+ * example, {@code AlignmentPanel.closePanel()} and
+ * {@code AnnotationPanel.dispose()}.</li>
+ * <li>Adjust code if necessary; for example an {@code ActionListener} should
+ * act on {@code av.getAlignment()} and not directly on {@code alignment}, as
+ * the latter pattern could leave persistent references to the alignment</li>
+ * </ul>
+ * 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 = 1024 * 1024;
+ private static final int ONE_MB = 1000 * 1000;
+
+ /*
+ * maximum retained heap usage (in MB) for a passing test
+ */
+ private static int MAX_RESIDUAL_HEAP = 45;
/**
* Configure (read-only) Jalview property settings for test
@BeforeClass(alwaysRun = true)
public void setUp()
{
- Jalview.main(new String[] { "-nonews", "-props",
- "test/jalview/testProps.jvprops" });
- Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS",
- Boolean.TRUE.toString());
- Cache.applicationProperties.setProperty("SHOW_QUALITY",
- Boolean.TRUE.toString());
- Cache.applicationProperties.setProperty("SHOW_CONSERVATION",
- Boolean.TRUE.toString());
- Cache.applicationProperties.setProperty("SHOW_OCCUPANCY",
- Boolean.TRUE.toString());
- Cache.applicationProperties.setProperty("SHOW_IDENTITY",
- Boolean.TRUE.toString());
+ 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);
}
- /**
- * A simple test that memory is released when all windows are closed.
- * <ul>
- * <li>generates a reasonably large alignment and loads it</li>
- * <li>performs various operations on the alignment</li>
- * <li>closes all windows</li>
- * <li>requests garbage collection</li>
- * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
- * </li>
- * </ul>
- * If the test fails, this suggests that a reference to some large object
- * (perhaps the alignment data, or consensus profile) has failed to be garbage
- * collected. If this is the case, the heap will need to be inspected manually
- * (suggest using jvisualvm) in order to track down where large objects are
- * still referenced. The code (for example AlignmentViewport.dispose()) should
- * then be updated to ensure references to large objects are set to null when
- * they are no longer required.
- *
- * @throws IOException
- */
@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;
+ Long minUsedMemory = null;
+ int gcCount = 0;
+ while (gcCount < 3)
+ {
+ gcCount++;
+ System.gc();
+ waitFor(1500);
+ usedMemory = getUsedMemory();
+ if (minUsedMemory == null || usedMemory < minUsedMemory)
+ {
+ minUsedMemory = usedMemory;
+ }
+ 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("(Minimum) Used memory after " + gcCount
+ + " call(s) to gc() = " + minUsedMemory + "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);
- waitForThreads(af.getViewport());
+ while (af.getViewport().isCalcInProgress())
+ {
+ waitFor(200);
+ }
- af.closeMenuItem_actionPerformed(true);
+ /*
+ * open an Overview window
+ */
+ af.overviewMenuItem_actionPerformed(null);
+ assertNotNull(af.alignPanel.overviewPanel);
/*
- * request garbage collection and allow 1 second for it to complete;
- * NB there is no guarantee when, or whether, it will run!
+ * exercise the pop-up menu in the Overview Panel (JAL-2864)
*/
- System.gc();
- synchronized (this)
+ 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++)
{
- try
- {
- wait(1000);
- } catch (InterruptedException e)
- {
- }
+ sg.addSequence(al.getSequenceAt(i), false);
}
+ af.viewport.setSelectionGroup(sg);
/*
- * check used memory is 'reasonably low'
+ * compute Tree and PCA (on all sequences, 100 columns)
*/
- long availableMemory = Runtime.getRuntime().totalMemory() / ONE_MB;
- long freeMemory = Runtime.getRuntime().freeMemory() / ONE_MB;
- long usedMemory = availableMemory - freeMemory;
- System.out.println("Memory in use after close all windows: "
- + usedMemory + "MB");
+ af.openTreePcaDialog();
+ CalculationChooser dialog = af.alignPanel.getCalculationDialog();
+ dialog.openPcaPanel("BLOSUM62", dialog.getSimilarityParameters(true));
+ dialog.openTreePanel("BLOSUM62", dialog.getSimilarityParameters(false));
/*
- * if this assertion fails
- * - set a breakpoint here
- * - run jvisualvm to inspect a heap dump of Jalview
- * - identify large objects in the heap and their referers
- * - fix code as necessary to null the references on close
+ * wait until Tree and PCA have been computed
*/
- long expectedMax = 100L;
- assertTrue(usedMemory < expectedMax,
- String.format("Used memory %d > %d", usedMemory, expectedMax));
+ while (af.viewport.getCurrentTree() == null
+ || dialog.getPcaPanel().isWorking())
+ {
+ waitFor(10);
+ }
+
+ /*
+ * give Swing time to add the PCA panel (?!?)
+ */
+ waitFor(100);
}
/**
- * wait for consensus etc thread to complete
+ * Wait for waitMs miliseconds
*
- * @param av
+ * @param waitMs
*/
- protected void waitForThreads(AlignViewport av)
+ protected void waitFor(int waitMs)
{
- while (av.isCalcInProgress())
+ try
+ {
+ Thread.sleep(waitMs);
+ } catch (InterruptedException e)
{
- try
- {
- Thread.sleep(200);
- } catch (Exception x)
- {
- }
}
}
/**
- * Generates an alignment (large enough for this test but not so large it is
- * too slow or runs out of memory) and saves it in a temporary file.
+ * 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
File f = File.createTempFile("MemoryTest", "fa");
PrintStream ps = new PrintStream(f);
AlignmentGenerator ag = new AlignmentGenerator(false, ps);
- ag.generate(1000, 20000, 0, 10, 15);
+ int width = 100000;
+ int height = 100;
+ ag.generate(width, height, 0, 10, 15);
+ ps.close();
return f;
}
}