/******************************************************************************* * Copyright (c) 2015-2021 MINRES Technologies GmbH and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * MINRES Technologies GmbH - initial API and implementation *******************************************************************************/ package com.minres.scviewer.e4.application.internal.status; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.wb.swt.ResourceManager; import org.eclipse.wb.swt.SWTResourceManager; import org.osgi.service.prefs.Preferences; import com.minres.scviewer.e4.application.Constants; /** * The Heap Status control, which shows the heap usage statistics in the window * trim. Part of the code is taken from the eclipse internal implementation */ public class HeapStatus extends Composite { /** The armed. */ private boolean armed; /** The gc image. */ private Image gcImage; /** The disabled gc image. */ private Image disabledGcImage; /** The arm col. */ private Color bgCol, usedMemCol, lowMemCol, freeMemCol, topLeftCol, bottomRightCol, sepCol, textCol, markCol, armCol; /** The button. */ private Canvas button; /** The preferences. */ private Preferences preferences; /** The update interval. */ private int updateInterval; /** The show max. */ private boolean showMax; /** The total mem. */ private long totalMem; /** The prev total mem. */ private long prevTotalMem = -1L; /** The prev used mem. */ private long prevUsedMem = -1L; /** The has changed. */ private boolean hasChanged; /** The used mem. */ private long usedMem; /** The mark. */ private long mark = -1; /** The img bounds. */ // start with 12x12 private Rectangle imgBounds = new Rectangle(0, 0, 12, 12); /** The max mem. */ private long maxMem = Long.MAX_VALUE; /** The max mem known. */ private boolean maxMemKnown; /** The low mem threshold. */ private float lowMemThreshold = 0.05f; /** The show low mem threshold. */ private boolean showLowMemThreshold = true; /** The update tooltip. */ private boolean updateTooltip = false; /** The is in gc. */ protected volatile boolean isInGC = false; /** The timer. */ private final Runnable timer = new Runnable() { @Override public void run() { if (!isDisposed()) { updateStats(); if (hasChanged) { if (updateTooltip) { updateToolTip(); } redraw(); hasChanged = false; } getDisplay().timerExec(updateInterval, this); } } }; /** The pref listener. */ private final IPreferenceChangeListener prefListener = event -> { if (IHeapStatusConstants.PREF_UPDATE_INTERVAL.equals(event.getKey())) { setUpdateIntervalInMS(preferences.getInt(IHeapStatusConstants.PREF_UPDATE_INTERVAL, 100)); } else if (IHeapStatusConstants.PREF_SHOW_MAX.equals(event.getKey())) { showMax = preferences.getBoolean(IHeapStatusConstants.PREF_SHOW_MAX, true); } }; /** * Creates a new heap status control with the given parent, and using the given * preference store to obtain settings such as the refresh interval. * * @param parent the parent composite * @param preferences the preference store */ public HeapStatus(Composite parent, Preferences preferences) { super(parent, SWT.NONE); maxMem = getMaxMem(); maxMemKnown = maxMem != Long.MAX_VALUE; this.preferences = preferences; if (this.preferences instanceof IEclipsePreferences) ((IEclipsePreferences) this.preferences).addPreferenceChangeListener(prefListener); setUpdateIntervalInMS(preferences.getInt(IHeapStatusConstants.PREF_UPDATE_INTERVAL, 100)); showMax = preferences.getBoolean(IHeapStatusConstants.PREF_SHOW_MAX, true); button = new Canvas(this, SWT.NONE); button.setToolTipText("Run Garbage Collection"); ImageDescriptor imageDesc = ResourceManager.getPluginImageDescriptor(Constants.PLUGIN_ID, "icons/trash.png"); //$NON-NLS-1$ Display display = getDisplay(); gcImage = imageDesc.createImage(); if (gcImage != null) { imgBounds = gcImage.getBounds(); disabledGcImage = new Image(display, gcImage, SWT.IMAGE_DISABLE); } usedMemCol = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); lowMemCol = SWTResourceManager.getColor(255, 70, 70); // medium red freeMemCol = SWTResourceManager.getColor(255, 190, 125); // light orange bgCol = SWTResourceManager.getColor(SWT.COLOR_WIDGET_BACKGROUND); sepCol = topLeftCol = armCol = SWTResourceManager.getColor(SWT.COLOR_WIDGET_NORMAL_SHADOW); bottomRightCol = SWTResourceManager.getColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW); markCol = textCol = SWTResourceManager.getColor(SWT.COLOR_INFO_FOREGROUND); createContextMenu(); Listener listener = event -> { switch (event.type) { case SWT.Dispose: doDispose(); break; case SWT.Resize: Rectangle rect = getClientArea(); button.setBounds(rect.width - imgBounds.width - 1, 1, imgBounds.width, rect.height - 2); break; case SWT.Paint: if (event.widget == HeapStatus.this) { paintComposite(event.gc); } else if (event.widget == button) { paintButton(event.gc); } break; case SWT.MouseUp: if (event.button == 1 && !isInGC) { arm(false); gc(); } break; case SWT.MouseDown: if (event.button == 1) { if (event.widget == HeapStatus.this) { setMark(); } else if (event.widget == button && !isInGC) arm(true); } break; case SWT.MouseEnter: HeapStatus.this.updateTooltip = true; updateToolTip(); break; case SWT.MouseExit: if (event.widget == HeapStatus.this) { HeapStatus.this.updateTooltip = false; } else if (event.widget == button) { arm(false); } break; default: break; } }; addListener(SWT.Dispose, listener); addListener(SWT.MouseDown, listener); addListener(SWT.Paint, listener); addListener(SWT.Resize, listener); addListener(SWT.MouseEnter, listener); addListener(SWT.MouseExit, listener); button.addListener(SWT.MouseDown, listener); button.addListener(SWT.MouseExit, listener); button.addListener(SWT.MouseUp, listener); button.addListener(SWT.Paint, listener); // make sure stats are updated before first paint updateStats(); getDisplay().asyncExec(() -> { if (!isDisposed()) { getDisplay().timerExec(updateInterval, timer); } }); } /* * (non-Javadoc) * * @see * org.eclipse.swt.widgets.Control#setBackground(org.eclipse.swt.graphics.Color) */ @Override public void setBackground(Color color) { bgCol = color; button.redraw(); button.update(); } /* * (non-Javadoc) * * @see * org.eclipse.swt.widgets.Control#setForeground(org.eclipse.swt.graphics.Color) */ @Override public void setForeground(Color color) { if (color == null) { usedMemCol = getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND); } else { usedMemCol = color; } button.redraw(); button.update(); } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Control#getForeground() */ @Override public Color getForeground() { if (usedMemCol != null) { return usedMemCol; } return getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND); } /** * Returns the maximum memory limit, or Long.MAX_VALUE if the max is not known. * * @return the max mem */ private long getMaxMem() { long max = Long.MAX_VALUE; try { // Must use reflect to allow compilation against JCL/Foundation Method maxMemMethod = Runtime.class.getMethod("maxMemory"); //$NON-NLS-1$ Object o = maxMemMethod.invoke(Runtime.getRuntime()); if (o instanceof Long) { max = ((Long) o).longValue(); } } catch (Exception e) { // ignore if method missing or if there are other failures trying to determine // the max } return max; } /** * Sets the update interval in ms. * * @param interval the new update interval in ms */ private void setUpdateIntervalInMS(int interval) { updateInterval = Math.max(100, interval); } /** * Do dispose. */ private void doDispose() { if (preferences instanceof IEclipsePreferences) ((IEclipsePreferences) preferences).removePreferenceChangeListener(prefListener); if (gcImage != null) { gcImage.dispose(); } if (disabledGcImage != null) { disabledGcImage.dispose(); } } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Composite#computeSize(int, int, boolean) */ @Override public Point computeSize(int wHint, int hHint, boolean changed) { GC gc = new GC(this); Point p = gc.textExtent("MMMMMMMMMMMM"); int height = imgBounds.height; // choose the largest of // - Text height + margins // - Image height + margins // - Default Trim heightin height = Math.max(height, p.y) + 4; height = Math.max(TrimUtil.TRIM_DEFAULT_HEIGHT, height); gc.dispose(); return new Point(p.x + 15, height); } /** * Arm. * * @param armed the armed */ private void arm(boolean armed) { if (this.armed == armed) { return; } this.armed = armed; button.redraw(); button.update(); } /** * Gc running. * * @param isInGC the is in gc */ private void gcRunning(boolean isInGC) { if (this.isInGC == isInGC) { return; } this.isInGC = isInGC; button.redraw(); button.update(); } /** * Creates the context menu. */ private void createContextMenu() { MenuManager menuMgr = new MenuManager(); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(mgr -> fillMenu(mgr)); Menu menu = menuMgr.createContextMenu(this); setMenu(menu); } /** * Fill menu. * * @param menuMgr the menu mgr */ private void fillMenu(IMenuManager menuMgr) { menuMgr.add(new SetMarkAction()); menuMgr.add(new ClearMarkAction()); menuMgr.add(new ShowMaxAction()); } /** * Sets the mark to the current usedMem level. */ private void setMark() { updateStats(); // get up-to-date stats before taking the mark mark = usedMem; hasChanged = true; redraw(); } /** * Clears the mark. */ private void clearMark() { mark = -1; hasChanged = true; redraw(); } /** * Gc. */ private void gc() { gcRunning(true); Thread t = new Thread() { @Override public void run() { busyGC(); getDisplay().asyncExec(() -> { if (!isDisposed()) gcRunning(false); }); } }; t.start(); } /** * Busy gc. */ private void busyGC() { Object obj = new Object(); WeakReference ref = new WeakReference<>(obj); obj = null; do { System.gc(); System.runFinalization(); } while (ref.get() != null); } /** * Paint button. * * @param gc the gc */ private void paintButton(GC gc) { Rectangle rect = button.getClientArea(); if (isInGC) { if (disabledGcImage != null) { int buttonY = (rect.height - imgBounds.height) / 2 + rect.y; gc.drawImage(disabledGcImage, rect.x, buttonY); } return; } if (armed) { gc.setBackground(armCol); gc.fillRectangle(rect.x, rect.y, rect.width, rect.height); } if (gcImage != null) { int by = (rect.height - imgBounds.height) / 2 + rect.y; // button y gc.drawImage(gcImage, rect.x, by); } } /** * Paint composite. * * @param gc the gc */ private void paintComposite(GC gc) { if (showMax && maxMemKnown) { paintCompositeMaxKnown(gc); } else { paintCompositeMaxUnknown(gc); } } /** * Paint composite max unknown. * * @param gc the gc */ private void paintCompositeMaxUnknown(GC gc) { Rectangle rect = getClientArea(); int x = rect.x; int y = rect.y; int w = rect.width; int h = rect.height; int bw = imgBounds.width; // button width int dx = x + w - bw - 2; // divider x int sw = w - bw - 3; // status width int uw = (int) (sw * usedMem / totalMem); // used mem width int ux = x + 1 + uw; // used mem right edge if (bgCol != null) { gc.setBackground(bgCol); } gc.fillRectangle(rect); gc.setForeground(sepCol); gc.drawLine(dx, y, dx, y + h); gc.drawLine(ux, y, ux, y + h); gc.setForeground(topLeftCol); gc.drawLine(x, y, x + w, y); gc.drawLine(x, y, x, y + h); gc.setForeground(bottomRightCol); gc.drawLine(x + w - 1, y, x + w - 1, y + h); gc.drawLine(x, y + h - 1, x + w, y + h - 1); gc.setBackground(usedMemCol); gc.fillRectangle(x + 1, y + 1, uw, h - 2); String s = convertToMegString(usedMem) + " of " + convertToMegString(totalMem); Point p = gc.textExtent(s); int sx = (rect.width - 15 - p.x) / 2 + rect.x + 1; int sy = (rect.height - 2 - p.y) / 2 + rect.y + 1; gc.setForeground(textCol); gc.drawString(s, sx, sy, true); // draw an I-shaped bar in the foreground colour for the mark (if present) if (mark != -1) { int ssx = (int) (sw * mark / totalMem) + x + 1; paintMark(gc, ssx, y, h); } } /** * Paint composite max known. * * @param gc the gc */ private void paintCompositeMaxKnown(GC gc) { Rectangle rect = getClientArea(); int x = rect.x; int y = rect.y; int w = rect.width; int h = rect.height; int bw = imgBounds.width; // button width int dx = x + w - bw - 2; // divider x int sw = w - bw - 3; // status width int uw = (int) (sw * usedMem / maxMem); // used mem width int ux = x + 1 + uw; // used mem right edge int tw = (int) (sw * totalMem / maxMem); // current total mem width int tx = x + 1 + tw; // current total mem right edge if (bgCol != null) { gc.setBackground(bgCol); } gc.fillRectangle(rect); gc.setForeground(sepCol); gc.drawLine(dx, y, dx, y + h); gc.drawLine(ux, y, ux, y + h); gc.drawLine(tx, y, tx, y + h); gc.setForeground(topLeftCol); gc.drawLine(x, y, x + w, y); gc.drawLine(x, y, x, y + h); gc.setForeground(bottomRightCol); gc.drawLine(x + w - 1, y, x + w - 1, y + h); gc.drawLine(x, y + h - 1, x + w, y + h - 1); if (lowMemThreshold != 0 && ((double) (maxMem - usedMem) / (double) maxMem < lowMemThreshold)) { gc.setBackground(lowMemCol); } else { gc.setBackground(usedMemCol); } gc.fillRectangle(x + 1, y + 1, uw, h - 2); gc.setBackground(freeMemCol); gc.fillRectangle(ux + 1, y + 1, tx - (ux + 1), h - 2); // paint line for low memory threshold if (showLowMemThreshold && lowMemThreshold != 0) { gc.setForeground(lowMemCol); int thresholdX = x + 1 + (int) (sw * (1.0 - lowMemThreshold)); gc.drawLine(thresholdX, y + 1, thresholdX, y + h - 2); } String s = convertToMegString(usedMem) + " of " + convertToMegString(totalMem); Point p = gc.textExtent(s); int sx = (rect.width - 15 - p.x) / 2 + rect.x + 1; int sy = (rect.height - 2 - p.y) / 2 + rect.y + 1; gc.setForeground(textCol); gc.drawString(s, sx, sy, true); // draw an I-shaped bar in the foreground colour for the mark (if present) if (mark != -1) { int ssx = (int) (sw * mark / maxMem) + x + 1; paintMark(gc, ssx, y, h); } } /** * Paint mark. * * @param gc the gc * @param x the x * @param y the y * @param h the h */ private void paintMark(GC gc, int x, int y, int h) { gc.setForeground(markCol); gc.drawLine(x, y + 1, x, y + h - 2); gc.drawLine(x - 1, y + 1, x + 1, y + 1); gc.drawLine(x - 1, y + h - 2, x + 1, y + h - 2); } /** * Update stats. */ private void updateStats() { Runtime runtime = Runtime.getRuntime(); totalMem = runtime.totalMemory(); long freeMem = runtime.freeMemory(); usedMem = totalMem - freeMem; if (convertToMeg(prevUsedMem) != convertToMeg(usedMem)) { prevUsedMem = usedMem; this.hasChanged = true; } if (prevTotalMem != totalMem) { prevTotalMem = totalMem; this.hasChanged = true; } } /** * Update tool tip. */ private void updateToolTip() { String usedStr = convertToMegString(usedMem); String totalStr = convertToMegString(totalMem); String maxStr = maxMemKnown ? convertToMegString(maxMem) : ""; String markStr = mark == -1 ? "" : convertToMegString(mark); String toolTip = "Heap size: " + usedStr + " of total: " + totalStr + " max: " + maxStr + " mark: " + markStr; if (!toolTip.equals(getToolTipText())) { setToolTipText(toolTip); } } /** * Converts the given number of bytes to a printable number of megabytes * (rounded up). * * @param numBytes the num bytes * @return the string */ private String convertToMegString(long numBytes) { return Long.toString(convertToMeg(numBytes)) + "M"; } /** * Converts the given number of bytes to the corresponding number of megabytes * (rounded up). * * @param numBytes the num bytes * @return the long */ private long convertToMeg(long numBytes) { return (numBytes + (512 * 1024)) / (1024 * 1024); } /** * The Class SetMarkAction. */ class SetMarkAction extends Action { /** * Instantiates a new sets the mark action. */ SetMarkAction() { super("&Set Mark"); } /* * (non-Javadoc) * * @see org.eclipse.jface.action.Action#run() */ @Override public void run() { setMark(); } } /** * The Class ClearMarkAction. */ class ClearMarkAction extends Action { /** * Instantiates a new clear mark action. */ ClearMarkAction() { super("&Clear Mark"); } /* * (non-Javadoc) * * @see org.eclipse.jface.action.Action#run() */ @Override public void run() { clearMark(); } } /** * The Class ShowMaxAction. */ class ShowMaxAction extends Action { /** * Instantiates a new show max action. */ ShowMaxAction() { super("Show &Max Heap", IAction.AS_CHECK_BOX); setEnabled(maxMemKnown); setChecked(showMax); } /* * (non-Javadoc) * * @see org.eclipse.jface.action.Action#run() */ @Override public void run() { preferences.putBoolean(IHeapStatusConstants.PREF_SHOW_MAX, isChecked()); redraw(); } } }