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