001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2025
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.util;
030
031import static javax.swing.GroupLayout.DEFAULT_SIZE;
032import static javax.swing.GroupLayout.Alignment.LEADING;
033
034import java.awt.BorderLayout;
035import java.awt.Color;
036import java.awt.Font;
037import java.awt.event.MouseAdapter;
038import java.awt.event.MouseEvent;
039import java.awt.event.MouseListener;
040
041import java.text.DecimalFormat;
042import java.text.SimpleDateFormat;
043
044import java.util.Date;
045
046import javax.swing.*;
047
048import edu.wisc.ssec.mcidasv.McIDASV;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052import ucar.unidata.idv.StateManager;
053import ucar.unidata.util.CacheManager;
054import ucar.unidata.util.GuiUtils;
055import ucar.unidata.util.Msg;
056
057public class MemoryMonitor extends JPanel implements Runnable {
058    
059    private static final Logger logger =
060        LoggerFactory.getLogger(MemoryMonitor.class);
061    
062    private static final long serialVersionUID = 1L;
063
064    /** flag for running */
065    private boolean running = false;
066
067    /** sleep interval */
068    private final long sleepInterval = 2000;
069
070    /** a thread */
071    private Thread thread;
072
073    private boolean isWarned = false;
074    private int sustainTimer = 1;
075    private int warnTimer = 1;
076    private static JDialog dialog = null;
077
078    /** percent threshold */
079    private final int percentThreshold;
080
081    /** number of times above the threshold */
082    private int timesAboveThreshold = 0;
083    
084    /** percent cancel */
085    private final int percentCancel;
086    
087    /** have we tried to cancel the load yet */
088    private boolean triedToCancel = false;
089
090    /** format */
091    private static DecimalFormat fmt = new DecimalFormat("#0");
092
093    /** the label */
094    private JLabel label = new JLabel("");
095    
096    /** Keep track of the last time we ran the gc and cleared the cache */
097    private static long lastTimeRanGC = -1;
098    
099//    /** Keep track of the IDV so we can try to cancel loads if mem usage gets high */
100//    private IntegratedDataViewer idv;
101    private StateManager stateManager;
102
103    private String memoryString;
104
105    private String mbString;
106
107    private boolean showClock = false;
108
109    private static SimpleDateFormat clockFormat =
110        new SimpleDateFormat("HH:mm:ss z");
111    
112    private static double MEGABYTE = 1024 * 1024;
113
114    /**
115     * Default constructor.
116     * 
117     * @param stateManager Reference back to application session's 
118     *                     {@code StateManager}. Cannot be {@code null}.
119     */
120    public MemoryMonitor(StateManager stateManager) {
121        this(stateManager, 75, 95, false);
122    }
123
124    /**
125     * Create a new MemoryMonitor.
126     *
127     * @param stateManager Reference back to application session's 
128     *                     {@code StateManager}. Cannot be {@code null}.
129     * @param percentThreshold Percentage of use memory before garbage
130     *                         collection is run.
131     * @param percentCancel Not currently in use.
132     * @param showClock Whether or not the clock should be shown instead of 
133     *                  the memory monitor widget.
134     * 
135     */
136    public MemoryMonitor(StateManager stateManager,
137                         final int percentThreshold,
138                         final int percentCancel,
139                         boolean showClock)
140    {
141        super(new BorderLayout());
142        this.stateManager = stateManager;
143        this.showClock = showClock;
144        Font f = label.getFont();
145        label.setToolTipText("Used memory/Max used memory/Max memory");
146        label.setFont(f);
147        this.percentThreshold = percentThreshold;
148        this.percentCancel = percentCancel;
149
150        GroupLayout layout = new GroupLayout(this);
151        this.setLayout(layout);
152        layout.setHorizontalGroup(
153            layout.createParallelGroup(LEADING)
154            .addComponent(label, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
155        );
156        layout.setVerticalGroup(
157            layout.createParallelGroup(LEADING)
158            .addComponent(label, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
159        );
160
161//        MouseListener ml = new MouseAdapter() {
162//            @Override public void mouseClicked(MouseEvent e) {
163//                if (SwingUtilities.isRightMouseButton(e))
164//                    popupMenu(e);
165//            }
166//        };
167        MouseListener ml = new MouseAdapter() {
168            public void mouseClicked(MouseEvent e) {
169                if (!SwingUtilities.isRightMouseButton(e)) {
170                    toggleClock();
171                    try {
172                        showStats();
173                    } catch (Exception ex) {
174                        
175                    }
176                }
177                handleMouseEvent(e);
178            }
179        };
180
181        label.addMouseListener(ml);
182        label.setOpaque(true);
183        label.setBackground(doColorThing(0));
184        memoryString = Msg.msg("Memory:");
185        mbString = Msg.msg("MB");
186        start();
187    }
188
189    /**
190     * Handle a mouse event
191     *
192     * @param event the event
193     */
194    private void handleMouseEvent(MouseEvent event) {
195        if (SwingUtilities.isRightMouseButton(event)) {
196            popupMenu(event);
197        }
198    }
199
200    /**
201     * 
202     */
203    private void toggleClock() {
204        this.showClock = !this.showClock;
205        stateManager.putPreference("idv.monitor.showclock", this.showClock);
206    }
207
208    /**
209     * Returns a description of either the clock or memory monitor GUI.
210     * 
211     * @return Description of either the clock or memory monitor GUI.
212     */
213    private String getToolTip() {
214        if (showClock) {
215            return "Current time";
216        } else {
217            return "Used memory/Max used memory/Max memory";
218        }
219    }
220
221    /**
222     * Popup a menu on an event
223     * 
224     * @param event the event
225     */
226    private void popupMenu(final MouseEvent event) {
227        JPopupMenu popup = new JPopupMenu();
228//        if (running) {
229//            popup.add(GuiUtils.makeMenuItem("Stop Running",
230//                MemoryMonitor.this, "toggleRunning"));
231//        } else {
232//            popup.add(GuiUtils.makeMenuItem("Resume Running",
233//                MemoryMonitor.this, "toggleRunning"));
234//        }
235
236        popup.add(GuiUtils.makeMenuItem("Clear Memory & Cache",
237            MemoryMonitor.this, "runGC"));
238        popup.show(this, event.getX(), event.getY());
239    }
240
241    /**
242     * Toggle running
243     */
244    public void toggleRunning() {
245        if (running) {
246            stop();
247        } else {
248            start();
249        }
250    }
251
252    /**
253     * Set the label font
254     * 
255     * @param f the font
256     */
257    public void setLabelFont(final Font f) {
258        label.setFont(f);
259    }
260
261    /**
262     * Stop running
263     */
264    public synchronized void stop() {
265        running = false;
266        label.setEnabled(false);
267    }
268
269    /**
270     * Start running
271     */
272    private synchronized void start() {
273        if (!running) {
274            label.setEnabled(true);
275            running = true;
276            triedToCancel = false;
277            thread = new Thread(this, "Memory monitor");
278            thread.start();
279        }
280    }
281
282    /**
283     * Run the GC and clear the cache
284     */
285    public void runGC() {
286        CacheManager.clearCache();
287        Runtime.getRuntime().gc();
288        lastTimeRanGC = System.currentTimeMillis();
289    }
290
291    /**
292     * Show the statistics.
293     */
294    private void showStats() throws IllegalStateException {
295        label.setToolTipText(getToolTip());
296        if (showClock) {
297            Date d = new Date();
298            clockFormat.setTimeZone(GuiUtils.getTimeZone());
299            label.setText("  " + clockFormat.format(d));
300            repaint();
301            return;
302        }
303
304        double totalMemory = Runtime.getRuntime().maxMemory();
305        double highWaterMark = Runtime.getRuntime().totalMemory();
306        double freeMemory = Runtime.getRuntime().freeMemory();
307        double usedMemory = (highWaterMark - freeMemory);
308
309        totalMemory = totalMemory / MEGABYTE;
310        usedMemory = usedMemory / MEGABYTE;
311        highWaterMark = highWaterMark / MEGABYTE;
312
313        long now = System.currentTimeMillis();
314        if (lastTimeRanGC < 0) {
315            lastTimeRanGC = now;
316        }
317
318        // For the threshold use the physical memory
319        int percent = (int)(100.0f * (usedMemory / totalMemory));
320        if (percent > percentThreshold) {
321            timesAboveThreshold++;
322            if (timesAboveThreshold > 5) {
323                // Only run every 5 seconds
324                if (now - lastTimeRanGC > 5000) {
325                    // For now just clear the cache. Don't run the gc
326                    CacheManager.clearCache();
327                    // runGC();
328                    lastTimeRanGC = now;
329                }
330            }
331            int stretchedPercent = Math.round(((float)percent - (float)percentThreshold) * (100.0f / (100.0f - (float)percentThreshold)));
332            label.setBackground(doColorThing(stretchedPercent));
333        } else {
334            timesAboveThreshold = 0;
335            lastTimeRanGC = now;
336            label.setBackground(doColorThing(0));
337        }
338        
339        // TODO: evaluate this method--should we really cancel stuff for the user?
340        // Decided that no, we shouldn't.  At least not until we get a more bulletproof way of doing it.
341        // action:idv.stopload is unreliable and doesnt seem to stop object creation, just data loading.
342        if (percent > this.percentCancel) {
343            if (!triedToCancel) {
344//              System.err.println("Canceled the load... not much memory available");
345//              idv.handleAction("action:idv.stopload");
346                triedToCancel = true;
347            }
348        } else {
349            triedToCancel = false;
350        }
351
352        if ((!label.getText().equals("Attempted to clear data!")) || ((label.getText().equals("Attempted to clear data!")) && usedMemory < 0.80 * totalMemory)) {
353            label.setText(' '
354                    + memoryString + ' '
355                    + fmt.format(usedMemory) + '/'
356                    + fmt.format(highWaterMark) + '/'
357                    + fmt.format(totalMemory) + ' ' + mbString
358                    + ' ');
359        } else {
360            label.setBackground(doColorThing(0));
361        }
362
363        // McIDAS Inquiry #2608-3141
364        // Warn the user if memory util is beyond certain limit and give them the option to quit
365        if (usedMemory > 0.90 * totalMemory && isWarned == false) {
366            sustainTimer += 1;
367            if (sustainTimer % 15 == 0) {
368                isWarned = true;
369                if (dialog == null || !dialog.isShowing()) {
370                    String[] options = {"No", "Clear data and layers", "Exit McIDAS-V"};
371
372                    JOptionPane optionPane = new JOptionPane(
373                            "McIDAS-V is using a lot of memory!\nIt may freeze, do you want to remove loaded data and layers or exit McIDAS-V?",
374                            JOptionPane.WARNING_MESSAGE,
375                            JOptionPane.YES_NO_CANCEL_OPTION,
376                            null,
377                            options,
378                            options[0]
379                    );
380
381                    JDialog dialog = optionPane.createDialog("Warning");
382                    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
383                    dialog.setModal(true);
384                    dialog.setAlwaysOnTop(true);
385                    dialog.setVisible(true);
386
387                    Object selectedValue = optionPane.getValue();
388                    int resp = -1;
389                    if (selectedValue != null) {
390                        for (int i = 0; i < options.length; i++) {
391                            if (options[i].equals(selectedValue)) {
392                                resp = i;
393                                break;
394                            }
395                        }
396                    }
397
398                    switch (resp) {
399                        case 0:
400                            break;
401
402                        case 1:
403                            try {
404                                // Clear data
405                                McIDASV.getStaticMcv().removeAllLayers(false);
406                                McIDASV.getStaticMcv().removeAllData(false);
407                                CacheManager.clearCache();
408                                runGC();
409                                // Prompt for confirmation (this is required)
410                                // McIDASV.getStaticMcv().removeAllLayersAndData();
411                                label.setText("Attempted to clear data!");
412                                label.setBackground(doColorThing(0));
413                                // This is required
414                                McIDASV.getStaticMcv().getIdvUIManager().showDashboard();
415                                repaint();
416                            } catch (Exception e) {
417                                logger.error("Error while clearing memory: " + e.getMessage(), e);
418                            }
419                            break;
420
421                        case 2:
422                            // Kill the session
423                            McIDASV.getStaticMcv().quit();
424                        default:
425                            logger.info("No action taken.");
426
427                    }
428                }
429            }
430        }
431
432        // Don't spam warnings but bring it back
433        // if the user is still in the same situation
434        if (isWarned) warnTimer += 1;
435        if (warnTimer % 60 == 0) isWarned = false;
436
437        repaint();
438    }
439
440    private Color doColorThing(final int percent) {
441        Float alpha = Float.valueOf(percent).floatValue() / 100;
442        return new Color(1.0f, 0.0f, 0.0f, alpha);
443    }
444    
445    /**
446     * Run this monitor
447     */
448    public void run() {
449        while (running) {
450            try {
451                SwingUtilities.invokeLater(this::showStats);
452                Thread.sleep(sleepInterval);
453            } catch (Exception exc) {
454                logger.warn("Caught exception!", exc);
455            }
456        }
457    }
458
459    /**
460     * Set whether we are running
461     * 
462     * @param r true if we are running
463     */
464    public void setRunning(final boolean r) {
465        running = r;
466    }
467
468    /**
469     * Get whether we are running
470     * 
471     * @return true if we are
472     */
473    public boolean getRunning() {
474        return running;
475    }
476
477    /**
478     * Test routine
479     * 
480     * @param args not used
481     */
482    public static void main(final String[] args) {
483        JFrame f = new JFrame();
484        MemoryMonitor mm = new MemoryMonitor(null);
485        f.getContentPane().add(mm);
486        f.pack();
487        f.setVisible(true);
488    }
489
490}