JAL-3597 slight tidying of code and Javadoc, passmark now heap <= 38MB
[jalview.git] / test / jalview / gui / FreeUpMemoryTest.java
1 package jalview.gui;
2
3 import static org.testng.Assert.assertNotNull;
4 import static org.testng.Assert.assertTrue;
5
6 import java.awt.event.MouseEvent;
7 import java.io.File;
8 import java.io.IOException;
9 import java.io.PrintStream;
10
11 import org.testng.annotations.BeforeClass;
12 import org.testng.annotations.Test;
13
14 import jalview.analysis.AlignmentGenerator;
15 import jalview.bin.Cache;
16 import jalview.bin.Jalview;
17 import jalview.datamodel.AlignmentI;
18 import jalview.datamodel.SequenceGroup;
19 import jalview.io.DataSourceType;
20 import jalview.io.FileLoader;
21 import junit.extensions.PA;
22
23 /**
24  * Provides a simple test that memory is released when all windows are closed.
25  * <ul>
26  * <li>generates a reasonably large alignment and loads it</li>
27  * <li>performs various operations on the alignment</li>
28  * <li>closes all windows</li>
29  * <li>requests garbage collection</li>
30  * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
31  * </li>
32  * </ul>
33  * If the test fails, this means that reference(s) to large object(s) have
34  * failed to be garbage collected. In this case:
35  * <ul>
36  * <li>set a breakpoint just before the test assertion in
37  * {@code checkUsedMemory}</li>
38  * <li>if the test fails intermittently, make this breakpoint conditional on
39  * {@code usedMemory > expectedMax}</li>
40  * <li>run the test to this point (and check that it is about to fail i.e.
41  * {@code usedMemory > expectedMax})</li>
42  * <li>use <a href="https://visualvm.github.io/">visualvm</a> to obtain a heap
43  * dump from the suspended process (and kill the test or let it fail)</li>
44  * <li>inspect the heap dump using visualvm for large objects and their
45  * referers</li>
46  * <li>Tips:</li>
47  * <ul>
48  * <li>Perform GC from the Monitor view in visualvm before requesting the heap
49  * dump - test failure might be simply a delay to GC</li>
50  * <li>View 'Objects' and filter classes to {@code jalview}. Sort columns by
51  * Count, or Size, and look for anything suspicious. For example, if the object
52  * count for {@code Sequence} is non-zero (it shouldn't be), pick any instance,
53  * and follow the chain of {@code references} to find which class(es) still hold
54  * references to sequence objects</li>
55  * <li>If this chain is impracticably long, re-run the test with a smaller
56  * alignment (set width=100, height=10 in {@code generateAlignment()}), to
57  * capture a heap which is qualitatively the same, but much smaller, so easier
58  * to analyse; note this requires an unconditional breakpoint</li>
59  * </ul>
60  * </ul>
61  * <p>
62  * <h2>Fixing memory leaks</h2>
63  * <p>
64  * Experience shows that often a reference is retained (directly or indirectly)
65  * by a Swing (or related) component (for example a {@code MouseListener} or
66  * {@code ActionListener}). There are two possible approaches to fixing:
67  * <ul>
68  * <li>Purist: ensure that all listeners and similar objects are removed when no
69  * longer needed. May be difficult, to achieve and to maintain as code
70  * changes.</li>
71  * <li>Pragmatic: null references to potentially large objects from Jalview
72  * application classes when no longer needed, typically when a panel is closed.
73  * This ensures that even if the JVM keeps a reference to a panel or viewport,
74  * it does not retain a large heap footprint. This is the approach taken in, for
75  * example, {@code AlignmentPanel.closePanel()} and
76  * {@code AnnotationPanel.dispose()}.</li>
77  * <li>Adjust code if necessary; for example an {@code ActionListener} should
78  * act on {@code av.getAlignment()} and not directly on {@code alignment}, as
79  * the latter pattern could leave persistent references to the alignment</li>
80  * </ul>
81  * Add code to 'null unused large object references' until the test passes. For
82  * a final sanity check, capture the heap dump for a passing test, and satisfy
83  * yourself that only 'small' or 'harmless' {@code jalview} object instances
84  * (such as enums or singletons) are left in the heap.
85  */
86 public class FreeUpMemoryTest
87 {
88   private static final int ONE_MB = 1000 * 1000;
89
90   /*
91    * maximum retained heap usage (in MB) for a passing test
92    */
93   private static int MAX_RESIDUAL_HEAP = 38;
94
95   /**
96    * Configure (read-only) Jalview property settings for test
97    */
98   @BeforeClass(alwaysRun = true)
99   public void setUp()
100   {
101     Jalview.main(
102             new String[]
103             { "-nonews", "-props", "test/jalview/testProps.jvprops" });
104     String True = Boolean.TRUE.toString();
105     Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", True);
106     Cache.applicationProperties.setProperty("SHOW_QUALITY", True);
107     Cache.applicationProperties.setProperty("SHOW_CONSERVATION", True);
108     Cache.applicationProperties.setProperty("SHOW_OCCUPANCY", True);
109     Cache.applicationProperties.setProperty("SHOW_IDENTITY", True);
110   }
111
112   @Test(groups = "Memory")
113   public void testFreeMemoryOnClose() throws IOException
114   {
115     File f = generateAlignment();
116     f.deleteOnExit();
117
118     doStuffInJalview(f);
119
120     Desktop.instance.closeAll_actionPerformed(null);
121
122     checkUsedMemory(MAX_RESIDUAL_HEAP);
123   }
124
125   /**
126    * Returns the current total used memory (available memory - free memory),
127    * rounded down to the nearest MB
128    * 
129    * @return
130    */
131   private static int getUsedMemory()
132   {
133     long availableMemory = Runtime.getRuntime().totalMemory();
134     long freeMemory = Runtime.getRuntime().freeMemory();
135     long usedMemory = availableMemory - freeMemory;
136
137     return (int) (usedMemory/ ONE_MB);
138   }
139
140   /**
141    * Requests garbage collection and then checks whether remaining memory in use
142    * is less than the expected value (in Megabytes)
143    * 
144    * @param expectedMax
145    */
146   protected void checkUsedMemory(int expectedMax)
147   {
148     /*
149      * request garbage collection and wait for it to run (up to 3 times);
150      * NB there is no guarantee when, or whether, it will do so
151      */
152     long usedMemory = 0L;
153     int gcCount = 0;
154     while (gcCount < 3)
155     {
156       gcCount++;
157       System.gc();
158       waitFor(1500);
159       usedMemory = getUsedMemory();
160       if (usedMemory < expectedMax)
161       {
162         break;
163       }
164     }
165
166     /*
167      * if this assertion fails (reproducibly!)
168      * - set a breakpoint here, conditional on (usedMemory > expectedMax)
169      * - run VisualVM to inspect the heap usage, and run GC from VisualVM to check 
170      *   it is not simply delayed garbage collection causing the test failure 
171      * - take a heap dump and identify large objects in the heap and their referers
172      * - fix code as necessary to null the references on close
173      */
174     System.out
175             .println("Used memory after " + gcCount + " call(s) to gc() = "
176                     + usedMemory + "MB (should be <=" + expectedMax + ")");
177     assertTrue(usedMemory <= expectedMax, String.format(
178             "Used memory %d should be less than %d (Recommend running test manually to verify)",
179             usedMemory, expectedMax));
180   }
181
182   /**
183    * Loads an alignment from file and exercises various operations in Jalview
184    * 
185    * @param f
186    */
187   protected void doStuffInJalview(File f)
188   {
189     /*
190      * load alignment, wait for consensus and other threads to complete
191      */
192     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(f.getPath(),
193             DataSourceType.FILE);
194     while (af.getViewport().isCalcInProgress())
195     {
196       waitFor(200);
197     }
198
199     /*
200      * open an Overview window
201      */
202     af.overviewMenuItem_actionPerformed(null);
203     assertNotNull(af.alignPanel.overviewPanel);
204
205     /*
206      * exercise the pop-up menu in the Overview Panel (JAL-2864)
207      */
208     Object[] args = new Object[] {
209         new MouseEvent(af, 0, 0, 0, 0, 0, 1, true) };
210     PA.invokeMethod(af.alignPanel.overviewPanel,
211             "showPopupMenu(java.awt.event.MouseEvent)", args);
212
213     /*
214      * set a selection group - potential memory leak if it retains
215      * a reference to the alignment
216      */
217     SequenceGroup sg = new SequenceGroup();
218     sg.setStartRes(0);
219     sg.setEndRes(100);
220     AlignmentI al = af.viewport.getAlignment();
221     for (int i = 0; i < al.getHeight(); i++)
222     {
223       sg.addSequence(al.getSequenceAt(i), false);
224     }
225     af.viewport.setSelectionGroup(sg);
226
227     /*
228      * compute Tree and PCA (on all sequences, 100 columns)
229      */
230     af.openTreePcaDialog();
231     CalculationChooser dialog = af.alignPanel.getCalculationDialog();
232     dialog.openPcaPanel("BLOSUM62", dialog.getSimilarityParameters(true));
233     dialog.openTreePanel("BLOSUM62", dialog.getSimilarityParameters(false));
234
235     /*
236      * wait until Tree and PCA have been computed
237      */
238     while (af.viewport.getCurrentTree() == null
239             || dialog.getPcaPanel().isWorking())
240     {
241       waitFor(10);
242     }
243
244     /*
245      * give Swing time to add the PCA panel (?!?)
246      */
247     waitFor(100);
248   }
249
250   /**
251    * Wait for waitMs miliseconds
252    * 
253    * @param waitMs
254    */
255   protected void waitFor(int waitMs)
256   {
257     try
258     {
259       Thread.sleep(waitMs);
260     } catch (InterruptedException e)
261     {
262     }
263   }
264
265   /**
266    * Generates an alignment and saves it in a temporary file, to be loaded by
267    * Jalview. We use a peptide alignment (so Conservation and Quality are
268    * calculated), which is wide enough to ensure Consensus, Conservation and
269    * Occupancy have a significant memory footprint (if not removed from the
270    * heap).
271    * 
272    * @return
273    * @throws IOException
274    */
275   private File generateAlignment() throws IOException
276   {
277     File f = File.createTempFile("MemoryTest", "fa");
278     PrintStream ps = new PrintStream(f);
279     AlignmentGenerator ag = new AlignmentGenerator(false, ps);
280     int width = 100000;
281     int height = 100;
282     ag.generate(width, height, 0, 10, 15);
283     ps.close();
284     return f;
285   }
286 }