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}