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