001/*
002 * $Id: MemoryMonitor.java,v 1.12 2011/03/24 16:06:35 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.util;
032
033import static javax.swing.GroupLayout.DEFAULT_SIZE;
034import static javax.swing.GroupLayout.Alignment.LEADING;
035
036import java.awt.BorderLayout;
037import java.awt.Color;
038import java.awt.Font;
039import java.awt.event.MouseAdapter;
040import java.awt.event.MouseEvent;
041import java.awt.event.MouseListener;
042import java.text.DecimalFormat;
043import java.text.SimpleDateFormat;
044import java.util.Date;
045
046import javax.swing.GroupLayout;
047import javax.swing.JFrame;
048import javax.swing.JLabel;
049import javax.swing.JPanel;
050import javax.swing.JPopupMenu;
051import javax.swing.SwingUtilities;
052
053import ucar.unidata.idv.IntegratedDataViewer;
054import ucar.unidata.util.CacheManager;
055import ucar.unidata.util.GuiUtils;
056import ucar.unidata.util.Msg;
057
058// initial version taken verbatim from Unidata :(
059public class MemoryMonitor extends JPanel implements Runnable {
060
061    /** flag for running */
062    private boolean running = false;
063
064    /** sleep interval */
065    private final long sleepInterval = 2000;
066
067    /** a thread */
068    private Thread thread;
069
070    /** percent threshold */
071    private final int percentThreshold;
072
073    /** number of times above the threshold */
074    private int timesAboveThreshold = 0;
075    
076    /** percent cancel */
077    private final int percentCancel;
078    
079    /** have we tried to cancel the load yet */
080    private boolean triedToCancel = false;
081
082    /** format */
083    private static DecimalFormat fmt = new DecimalFormat("#0");
084
085    /** the label */
086    private JLabel label = new JLabel("");
087    
088    /** Keep track of the last time we ran the gc and cleared the cache */
089    private static long lastTimeRanGC = -1;
090    
091    /** Keep track of the IDV so we can try to cancel loads if mem usage gets high */
092    private IntegratedDataViewer idv;
093
094    private boolean showClock = false;
095
096    private static final Font clockFont = new Font("Dialog", Font.BOLD, 11);
097
098    private static SimpleDateFormat clockFormat = new SimpleDateFormat("HH:mm:ss z");
099
100    /**
101     * Default constructor
102     */
103    public MemoryMonitor(IntegratedDataViewer idv) {
104        this(idv, 75, 95, false);
105    }
106
107    /**
108     * Create a new MemoryMonitor
109     * 
110     * @param percentThreshold the percentage of use memory before garbage
111     *        collection is run
112     * 
113     */
114    public MemoryMonitor(IntegratedDataViewer idv, final int percentThreshold, final int percentCancel, boolean showClock) {
115        super(new BorderLayout());
116        this.idv = idv;
117        this.showClock = showClock;
118        Font f = label.getFont();
119        label.setToolTipText("Used memory/Max used memory/Max memory");
120        label.setFont(f);
121        this.percentThreshold = percentThreshold;
122        this.percentCancel = percentCancel;
123
124        GroupLayout layout = new GroupLayout(this);
125        this.setLayout(layout);
126        layout.setHorizontalGroup(
127            layout.createParallelGroup(LEADING)
128            .addComponent(label, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
129        );
130        layout.setVerticalGroup(
131            layout.createParallelGroup(LEADING)
132            .addComponent(label, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
133        );
134
135//        MouseListener ml = new MouseAdapter() {
136//            @Override public void mouseClicked(MouseEvent e) {
137//                if (SwingUtilities.isRightMouseButton(e))
138//                    popupMenu(e);
139//            }
140//        };
141        MouseListener ml = new MouseAdapter() {
142            public void mouseClicked(MouseEvent e) {
143                if (!SwingUtilities.isRightMouseButton(e)) {
144                    toggleClock();
145                    showStats();
146                }
147                handleMouseEvent(e);
148            }
149        };
150
151        label.addMouseListener(ml);
152        label.setOpaque(true);
153        label.setBackground(doColorThing(0));
154        start();
155    }
156
157    /**
158     * Handle a mouse event
159     *
160     * @param event the event
161     */
162    private void handleMouseEvent(MouseEvent event) {
163        if (SwingUtilities.isRightMouseButton(event)) {
164            popupMenu(event);
165            return;
166        }
167    }
168
169    private void toggleClock() {
170        this.showClock = !this.showClock;
171        idv.getStateManager().putPreference("idv.monitor.showclock", this.showClock);
172    }
173
174    private String getToolTip() {
175        if (showClock) {
176            return "Current time";
177        } else {
178            return "Used memory/Max used memory/Max memory";
179        }
180    }
181
182    /**
183     * Popup a menu on an event
184     * 
185     * @param event the event
186     */
187    private void popupMenu(final MouseEvent event) {
188        JPopupMenu popup = new JPopupMenu();
189//        if (running) {
190//            popup.add(GuiUtils.makeMenuItem("Stop Running",
191//                MemoryMonitor.this, "toggleRunning"));
192//        } else {
193//            popup.add(GuiUtils.makeMenuItem("Resume Running",
194//                MemoryMonitor.this, "toggleRunning"));
195//        }
196
197        popup.add(GuiUtils.makeMenuItem("Clear Memory & Cache",
198            MemoryMonitor.this, "runGC"));
199        popup.show(this, event.getX(), event.getY());
200    }
201
202    /**
203     * Toggle running
204     */
205    public void toggleRunning() {
206        if (running) {
207            stop();
208        } else {
209            start();
210        }
211    }
212
213    /**
214     * Set the label font
215     * 
216     * @param f the font
217     */
218    public void setLabelFont(final Font f) {
219        label.setFont(f);
220    }
221
222    /**
223     * Stop running
224     */
225    public synchronized void stop() {
226        running = false;
227        label.setEnabled(false);
228    }
229
230    /**
231     * Start running
232     */
233    private synchronized void start() {
234        if (running)
235            return;
236
237        label.setEnabled(true);
238        running = true;
239        triedToCancel = false;
240        thread = new Thread(this, "Memory monitor");
241        thread.start();
242    }
243
244    /**
245     * Run the GC and clear the cache
246     */
247    public void runGC() {
248        CacheManager.clearCache();
249        Runtime.getRuntime().gc();
250        lastTimeRanGC = System.currentTimeMillis();
251    }
252
253    /**
254     * Show the statistics.
255     */
256    private void showStats() throws IllegalStateException {
257        label.setToolTipText(getToolTip());
258        if (showClock) {
259            Date d = new Date();
260            clockFormat.setTimeZone(GuiUtils.getTimeZone());
261            label.setText("  " + clockFormat.format(d));
262            repaint();
263            return;
264        }
265
266        double totalMemory = Runtime.getRuntime().maxMemory();
267        double highWaterMark = Runtime.getRuntime().totalMemory();
268        double freeMemory = Runtime.getRuntime().freeMemory();
269        double usedMemory = (highWaterMark - freeMemory);
270
271        double megabyte = 1024 * 1024;
272
273        totalMemory = totalMemory / megabyte;
274        usedMemory = usedMemory / megabyte;
275        highWaterMark = highWaterMark / megabyte;
276
277        long now = System.currentTimeMillis();
278        if (lastTimeRanGC < 0)
279            lastTimeRanGC = now;
280
281        // For the threshold use the physical memory
282        int percent = (int)(100.0f * (usedMemory / totalMemory));
283        if (percent > percentThreshold) {
284            timesAboveThreshold++;
285            if (timesAboveThreshold > 5) {
286                // Only run every 5 seconds
287                if (now - lastTimeRanGC > 5000) {
288                    // For now just clear the cache. Don't run the gc
289                    CacheManager.clearCache();
290                    // runGC();
291                    lastTimeRanGC = now;
292                }
293            }
294            int stretchedPercent = Math.round(((float)percent - (float)percentThreshold) * (100.0f / (100.0f - (float)percentThreshold)));
295            label.setBackground(doColorThing(stretchedPercent));
296        } else {
297            timesAboveThreshold = 0;
298            lastTimeRanGC = now;
299            label.setBackground(doColorThing(0));
300        }
301        
302        // TODO: evaluate this method--should we really cancel stuff for the user?
303        // Decided that no, we shouldn't.  At least not until we get a more bulletproof way of doing it.
304        // action:idv.stopload is unreliable and doesnt seem to stop object creation, just data loading.
305        if (percent > this.percentCancel) {
306            if (!triedToCancel) {
307//              System.err.println("Canceled the load... not much memory available");
308//              idv.handleAction("action:idv.stopload");
309                triedToCancel = true;
310            }
311        }
312        else {
313            triedToCancel = false;
314        }
315
316        label.setText(" "
317            + Msg.msg("Memory:") + " "
318            + fmt.format(usedMemory) + "/"
319            + fmt.format(highWaterMark) + "/"
320            + fmt.format(totalMemory) + " " + Msg.msg("MB")
321            + " ");
322
323        repaint();
324    }
325
326    private Color doColorThing(final int percent) {
327        Float alpha = new Float(percent).floatValue() / 100;
328        return new Color(1.0f, 0.0f, 0.0f, alpha);
329    }
330
331    /**
332     * Run this monitor
333     */
334    public void run() {
335        while (running) {
336            try {
337                showStats();
338                Thread.sleep(sleepInterval);
339            } catch (Exception exc) {
340            }
341        }
342    }
343
344    /**
345     * Set whether we are running
346     * 
347     * @param r true if we are running
348     */
349    public void setRunning(final boolean r) {
350        running = r;
351    }
352
353    /**
354     * Get whether we are running
355     * 
356     * @return true if we are
357     */
358    public boolean getRunning() {
359        return running;
360    }
361
362    /**
363     * Test routine
364     * 
365     * @param args not used
366     */
367    public static void main(final String[] args) {
368        JFrame f = new JFrame();
369        MemoryMonitor mm = new MemoryMonitor(null);
370        f.getContentPane().add(mm);
371        f.pack();
372        f.setVisible(true);
373    }
374
375}