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