| /******************************************************************************* |
| * Copyright (c) 2007, 2020 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.swt.tools.internal; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.util.function.*; |
| import java.util.stream.*; |
| |
| import org.eclipse.swt.*; |
| import org.eclipse.swt.custom.*; |
| import org.eclipse.swt.graphics.*; |
| import org.eclipse.swt.internal.WidgetSpy.*; |
| import org.eclipse.swt.layout.*; |
| import org.eclipse.swt.widgets.*; |
| import org.eclipse.swt.widgets.List; |
| |
| /** |
| * Instructions on how to use the Sleak tool with a standlaone SWT example: |
| * |
| * Modify the main method below to launch your application. |
| * Run Sleak. |
| * |
| */ |
| public class Sleak { |
| List list; |
| Canvas canvas; |
| Button enableTracking, diff, stackTrace, saveAs, save; |
| Combo diffType; |
| Text text; |
| |
| String filterPath = ""; |
| String fileName = "sleakout"; |
| String selectedName; |
| boolean incrementFileNames = true; |
| boolean saveImages = true; |
| int fileCount; |
| |
| java.util.List<ObjectWithError> oldObjects = new ArrayList<> (); |
| java.util.List<ObjectWithError> objects = new ArrayList<> (); |
| |
| NonDisposedWidgetTracker nonDisposedWidgetTracker = new NonDisposedWidgetTracker(); |
| |
| public static void main (String [] args) { |
| DeviceData data = new DeviceData(); |
| data.tracking = true; |
| Display display = new Display (data); |
| Sleak sleak = new Sleak (); |
| Shell shell = new Shell(display); |
| shell.setText ("S-Leak"); |
| Point size = shell.getSize (); |
| shell.setSize (size.x / 2, size.y / 2); |
| GridLayout layout = new GridLayout(2, false); |
| layout.horizontalSpacing = 0; |
| layout.verticalSpacing = 0; |
| shell.setLayout(layout); |
| sleak.create (shell); |
| shell.open(); |
| |
| // Launch your application here |
| // e.g. |
| // Shell shell2 = new Shell(display); |
| // Button button1 = new Button(shell2, SWT.PUSH); |
| // button1.setBounds(10, 10, 100, 50); |
| // button1.setText("Hello World"); |
| // Image image = new Image(display, 20, 20); |
| // Button button2 = new Button(shell2, SWT.PUSH); |
| // button2.setBounds(10, 70, 100, 50); |
| // button2.setImage(image); |
| // button1.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { |
| // Font oldFont = button1.getFont(); |
| // int style = oldFont.getFontData()[0].getStyle() ^ SWT.BOLD; |
| // button1.setFont(new Font(display, "Arial", 10, style)); |
| // button1.setForeground(new Color(100,200,100)); |
| // oldFont.dispose(); |
| // })); |
| // shell2.open(); |
| |
| while (!shell.isDisposed ()) { |
| if (!display.readAndDispatch ()) display.sleep (); |
| } |
| display.dispose (); |
| } |
| |
| public void create (Composite parent) { |
| |
| SashForm sash = new SashForm(parent, SWT.SMOOTH); |
| sash.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| sash.setOrientation(SWT.HORIZONTAL); |
| sash.setVisible(true); |
| |
| Composite left = new Composite(sash, 0); |
| left.setLayout(new GridLayout()); |
| left.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| Composite right = new Composite(sash, 0); |
| right.setLayout(new GridLayout()); |
| |
| sash.setWeights(new int[] {40,60}); |
| |
| // Right side |
| canvas = new Canvas (right, SWT.BORDER); |
| canvas.addListener (SWT.Paint, event -> paintCanvas (event)); |
| canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10)); |
| text = new Text (right, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); |
| text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 10)); |
| setVisible(text, false); |
| |
| // Left side |
| enableTracking = new Button (left, SWT.CHECK); |
| enableTracking.setText ("Enable resource tracking"); |
| enableTracking.setToolTipText("Enable Device resource tracking. Only resources allocated once enabled will be tracked. To track devices created before view is created, turn on tracing options, see https://www.eclipse.org/swt/tools.php"); |
| enableTracking.addListener (SWT.Selection, e -> toggleEnableTracking ()); |
| enableTracking.setSelection(enableTracking.getDisplay().isTracking()); |
| enableTracking.setLayoutData(new GridData(SWT.NONE, SWT.NONE, false, false)); |
| |
| Composite buttons = new Composite(left, 0); |
| buttons.setLayout(new GridLayout(4, false)); |
| buttons.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); |
| |
| diff = new Button (buttons, SWT.PUSH); |
| diff.setText ("Snap && Diff"); |
| diff.addListener (SWT.Selection, event -> refreshDifference ()); |
| GridData diffData = new GridData(SWT.FILL, SWT.NONE, true, false); |
| diffData.horizontalSpan = 2; |
| diff.setLayoutData(diffData); |
| |
| save = new Button (buttons, SWT.PUSH); |
| save.setText ("Save"); |
| save.setToolTipText("Saves to the previously selected file."); |
| save.addListener (SWT.Selection, event -> saveToFile (false)); |
| save.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false)); |
| |
| saveAs = new Button (buttons, SWT.PUSH); |
| saveAs.setText ("Save As..."); |
| saveAs.setToolTipText("Saves the contents of the list to a file, optionally with the stack traces if selected."); |
| saveAs.addListener (SWT.Selection, event -> saveToFile (true)); |
| saveAs.setLayoutData(new GridData(SWT.FILL, SWT.NONE, false, false)); |
| |
| Composite checkboxAndCombo = new Composite(left, 0); |
| checkboxAndCombo.setLayout(new GridLayout(2, false)); |
| checkboxAndCombo.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); |
| |
| diffType = new Combo (checkboxAndCombo, SWT.CHECK); |
| diffType.add ("Object identity"); |
| diffType.add ("Creator class and line"); |
| diffType.add ("Creator class"); |
| diffType.select(0); |
| diffType.setLayoutData(new GridData(SWT.NONE, SWT.NONE, false, false)); |
| |
| stackTrace = new Button (checkboxAndCombo, SWT.CHECK); |
| stackTrace.setText ("Show Stack"); |
| stackTrace.addListener (SWT.Selection, e -> toggleStackTrace ()); |
| stackTrace.setLayoutData(new GridData(SWT.RIGHT, SWT.NONE, true, false)); |
| |
| list = new List (left, SWT.BORDER | SWT.V_SCROLL); |
| list.addListener (SWT.Selection, event -> refreshObject ()); |
| list.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); |
| |
| stackTrace.setSelection (false); |
| filterNonDisposedWidgetTypes(); |
| } |
| |
| private void toggleEnableTracking() { |
| Display display = enableTracking.getDisplay(); |
| boolean tracking = display.isTracking(); |
| display.setTracking(!tracking); |
| setWidgetTrackingEnabled(tracking); |
| } |
| |
| void refreshLabel (java.util.List<ObjectWithError> createdObjects, java.util.List<ObjectWithError> deletedObjects) { |
| Function<? super ObjectWithError, ? extends String> classifier = o -> o.object.getClass().getSimpleName(); |
| |
| Map<String, Long> deleted = deletedObjects.stream().collect( |
| Collectors.groupingBy(classifier, Collectors.counting())); |
| |
| Map<String, Long> created = createdObjects.stream().collect( |
| Collectors.groupingBy(classifier, Collectors.counting())); |
| |
| StringBuilder sb = new StringBuilder(); |
| Stream<String> deletedAndCreated = Stream.concat(deleted.keySet().stream(), created.keySet().stream()); |
| deletedAndCreated.distinct().sorted().forEach(type -> addCounts(sb, type, deleted.get(type), created.get(type))); |
| |
| String description = sb.length() > 0 ? sb.toString() : "0 object(s)"; |
| list.setToolTipText(description); |
| } |
| |
| static void addCounts (StringBuilder string, String type, Long deleted, Long created) { |
| if (deleted != null || created != null) { |
| if (deleted != null) { |
| string.append("-" + deleted); |
| if (created != null) { |
| string.append(" / "); |
| } |
| } |
| if (created != null) { |
| string.append(created); |
| } |
| string.append(" " + type + "(s)\n"); |
| } |
| } |
| |
| void refreshDifference () { |
| Display display = canvas.getDisplay(); |
| DeviceData info = getDeviceData(display); |
| |
| boolean hasOldData = !oldObjects.isEmpty(); |
| |
| java.util.List<ObjectWithError> old = new ArrayList<>(oldObjects); |
| java.util.List<ObjectWithError> disposed = new ArrayList<>(); |
| java.util.List<ObjectWithError> created = new ArrayList<>(); |
| java.util.List<ObjectWithError> same = collectNewObjects(info, old, disposed, created); |
| java.util.List<ObjectWithError> nonDisposedWidgets = getNonDisposedWidgets(); |
| created.addAll(nonDisposedWidgets); |
| resetNonDisposedWidgets(); |
| |
| if (diffType.getSelectionIndex() > 0) { |
| old.removeAll(same); |
| if (!old.isEmpty()) { |
| Iterator<ObjectWithError> createdIter = created.iterator (); |
| while (createdIter.hasNext ()) { |
| ObjectWithError createdObj = createdIter.next (); |
| StackTraceElement stack = createdObj.getCreator (); |
| Iterator<ObjectWithError> oldIter = old.iterator (); |
| while (oldIter.hasNext ()) { |
| ObjectWithError oldObj = oldIter.next(); |
| if (creatorEquals(stack, oldObj.getCreator ())) { |
| createdIter.remove (); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| objects.clear(); |
| objects.addAll(created); |
| |
| oldObjects.clear(); |
| oldObjects.addAll(same); |
| oldObjects.addAll(created); |
| |
| |
| list.removeAll (); |
| text.setText (""); |
| canvas.redraw (); |
| if(hasOldData) { |
| for (ObjectWithError object : created) { |
| list.add (object.object.toString()); |
| } |
| } |
| if(hasOldData) { |
| refreshLabel (created, disposed); |
| } else { |
| refreshLabel (Collections.emptyList(), Collections.emptyList()); |
| } |
| } |
| |
| private static java.util.List<ObjectWithError> collectNewObjects(DeviceData info, |
| java.util.List<ObjectWithError> oldObjects, |
| java.util.List<ObjectWithError> disposedObjects, |
| java.util.List<ObjectWithError> createdObjects |
| ) { |
| disposedObjects.addAll(oldObjects); |
| ArrayList<ObjectWithError> sameObjects = new ArrayList<>(); |
| |
| for (int i=0; i<info.objects.length; i++) { |
| boolean found = false; |
| Iterator<ObjectWithError> oldObject = oldObjects.iterator (); |
| Object infoObject = info.objects [i]; |
| if (!(infoObject instanceof Color)) { |
| // Bug 563018: Colors don't require disposal, so exclude them from the list of allocated objects. |
| while (oldObject.hasNext () && !found) { |
| ObjectWithError next = oldObject.next (); |
| if (infoObject == next.object) { |
| sameObjects.add(next); |
| found = true; |
| } |
| } |
| if (!found) { |
| createdObjects.add(new ObjectWithError (infoObject, info.errors[i])); |
| } |
| } |
| } |
| // objects that were not found in new system state are disposed |
| disposedObjects.removeAll(sameObjects); |
| return sameObjects; |
| } |
| |
| private DeviceData getDeviceData(Display display) { |
| DeviceData info = display.getDeviceData (); |
| if (!info.tracking) { |
| Shell shell = canvas.getShell(); |
| MessageBox dialog = new MessageBox (shell, SWT.ICON_WARNING | SWT.YES | SWT.NO); |
| dialog.setText (shell.getText ()); |
| dialog.setMessage ("Warning: Device is not tracking resource allocation\nWould you like to enable tracking now for future created resources?"); |
| if (SWT.YES == dialog.open ()) { |
| enableTracking.setSelection(true); |
| toggleEnableTracking(); |
| } |
| } |
| return info; |
| } |
| |
| boolean creatorEquals (StackTraceElement first, StackTraceElement second) { |
| switch (diffType.getSelectionIndex()) { |
| case 1: return first.equals(second); |
| case 2: return first.getClassName().equals(second.getClassName()); |
| default: throw new IllegalArgumentException(); |
| } |
| } |
| private void saveToFile(boolean prompt) { |
| if (prompt || selectedName == null) { |
| FileDialog dialog = new FileDialog(saveAs.getShell(), SWT.SAVE); |
| dialog.setFilterPath(filterPath); |
| dialog.setFileName(fileName); |
| dialog.setOverwrite(true); |
| selectedName = dialog.open(); |
| fileCount = 0; |
| if (selectedName == null) { |
| return; |
| } |
| filterPath = dialog.getFilterPath(); |
| fileName = dialog.getFileName(); |
| |
| MessageBox msg = new MessageBox(saveAs.getShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO); |
| msg.setText("Append incrementing file counter?"); |
| msg.setMessage("Append an incrementing file count to the file name on each save, starting at 000?"); |
| incrementFileNames = msg.open() == SWT.YES; |
| |
| msg = new MessageBox(saveAs.getShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO); |
| msg.setText("Save images for each resource?"); |
| msg.setMessage("Save an image (png) for each resource?"); |
| saveImages = msg.open() == SWT.YES; |
| } |
| |
| String fileName = selectedName; |
| if (incrementFileNames) { |
| fileName = String.format("%s_%03d", fileName, fileCount++); |
| } |
| try (PrintWriter file = new PrintWriter(new FileOutputStream(fileName))) { |
| |
| int i = 0; |
| for (ObjectWithError o : objects) { |
| Object object = o.object; |
| Error error = o.error; |
| file.print(object.toString()); |
| if (saveImages) { |
| String suffix = String.format("%05d.png", i++); |
| String pngName = String.format("%s_%s", fileName, suffix); |
| Image image = new Image(saveAs.getDisplay(), 100, 100); |
| try { |
| GC gc = new GC(image); |
| try { |
| draw(gc, object); |
| } finally { |
| gc.dispose(); |
| } |
| ImageLoader loader = new ImageLoader(); |
| loader.data = new ImageData[] { image.getImageData() }; |
| loader.save(pngName, SWT.IMAGE_PNG); |
| } finally { |
| image.dispose(); |
| } |
| |
| file.print(" -> "); |
| file.print(suffix); |
| } |
| file.println(); |
| if (stackTrace.getSelection()) { |
| error.printStackTrace(file); |
| System.out.println(); |
| } |
| } |
| } catch (IOException e1) { |
| MessageBox msg = new MessageBox(saveAs.getShell(), SWT.ICON_ERROR | SWT.OK); |
| msg.setText("Failed to save"); |
| msg.setMessage("Failed to save S-Leak file.\n" + e1.getMessage()); |
| msg.open(); |
| } |
| } |
| |
| void toggleStackTrace () { |
| refreshObject (); |
| canvas.getParent().layout (); |
| } |
| |
| void paintCanvas (Event event) { |
| canvas.setCursor (null); |
| int index = list.getSelectionIndex (); |
| if (index == -1) return; |
| GC gc = event.gc; |
| Object object = objects.get(index).object; |
| draw(gc, object); |
| } |
| |
| void draw(GC gc, Object object) { |
| if (object instanceof Cursor) { |
| if (((Cursor)object).isDisposed ()) return; |
| canvas.setCursor ((Cursor) object); |
| return; |
| } |
| if (object instanceof Font) { |
| if (((Font)object).isDisposed ()) return; |
| gc.setFont ((Font) object); |
| String string = ""; |
| String lf = text.getLineDelimiter (); |
| for (FontData data : gc.getFont ().getFontData ()) { |
| String style = "NORMAL"; |
| int bits = data.getStyle (); |
| if (bits != 0) { |
| if ((bits & SWT.BOLD) != 0) style = "BOLD "; |
| if ((bits & SWT.ITALIC) != 0) style += "ITALIC"; |
| } |
| string += data.getName () + " " + data.getHeight () + " " + style + lf; |
| } |
| gc.drawString (string, 0, 0); |
| return; |
| } |
| //NOTHING TO DRAW FOR GC |
| // if (object instanceof GC) { |
| // return; |
| // } |
| if (object instanceof Image) { |
| if (((Image)object).isDisposed ()) return; |
| gc.drawImage ((Image) object, 0, 0); |
| return; |
| } |
| if (object instanceof Path) { |
| if (((Path)object).isDisposed ()) return; |
| gc.drawPath ((Path) object); |
| return; |
| } |
| if (object instanceof Pattern) { |
| if (((Pattern)object).isDisposed ()) return; |
| gc.setBackgroundPattern ((Pattern)object); |
| gc.fillRectangle (canvas.getClientArea ()); |
| gc.setBackgroundPattern (null); |
| return; |
| } |
| if (object instanceof Region) { |
| if (((Region)object).isDisposed ()) return; |
| String string = ((Region)object).getBounds().toString(); |
| gc.drawString (string, 0, 0); |
| return; |
| } |
| if (object instanceof TextLayout) { |
| if (((TextLayout)object).isDisposed ()) return; |
| ((TextLayout)object).draw (gc, 0, 0); |
| return; |
| } |
| if (object instanceof Transform) { |
| if (((Transform)object).isDisposed ()) return; |
| String string = ((Transform)object).toString(); |
| gc.drawString (string, 0, 0); |
| return; |
| } |
| } |
| |
| void refreshObject () { |
| int index = list.getSelectionIndex (); |
| if (index == -1) return; |
| if (stackTrace.getSelection ()) { |
| text.setText (objects.get(index).getStack()); |
| setVisible(text, true); |
| setVisible(canvas, false); |
| text.getParent().layout(); |
| } else { |
| setVisible(canvas, true); |
| setVisible(text, false); |
| canvas.redraw (); |
| } |
| } |
| |
| private void setVisible(Control control, boolean visible) { |
| control.setVisible(visible); |
| ((GridData)control.getLayoutData()).exclude = !visible; |
| } |
| |
| private void filterNonDisposedWidgetTypes() { |
| java.util.List<Class<? extends Widget>> trackedTypes = Arrays.asList( |
| // Composite.class, |
| // Menu.class |
| ); |
| nonDisposedWidgetTracker.setTrackedTypes(trackedTypes); |
| } |
| |
| private void setWidgetTrackingEnabled(boolean tracking) { |
| nonDisposedWidgetTracker.setTrackingEnabled(tracking); |
| } |
| |
| private java.util.List<ObjectWithError> getNonDisposedWidgets() { |
| java.util.List<ObjectWithError> nonDisposedWidgets = new ArrayList<>(); |
| nonDisposedWidgetTracker.getNonDisposedWidgets().forEach((w, e) -> nonDisposedWidgets.add(new ObjectWithError(w, e))); |
| return nonDisposedWidgets; |
| } |
| |
| private void resetNonDisposedWidgets() { |
| nonDisposedWidgetTracker.startTracking(); |
| } |
| |
| private static final class ObjectWithError { |
| final Object object; |
| final Error error; |
| String stack; |
| StackTraceElement creator; |
| |
| ObjectWithError(Object o, Error e) { |
| this.object = o; |
| this.error = e; |
| } |
| |
| StackTraceElement getCreator() { |
| if (creator == null) { |
| String objectType = object.getClass().getName(); |
| Iterator<StackTraceElement> stack = Arrays.asList(error.getStackTrace()).iterator(); |
| while (stack.hasNext()) { |
| StackTraceElement element = stack.next(); |
| if (element.getClassName().equals(objectType) && element.getMethodName().equals("<init>")) { |
| creator = stack.hasNext() ? stack.next() : null; |
| break; |
| } |
| } |
| } |
| return creator; |
| } |
| |
| String getStack() { |
| if (stack == null) { |
| ByteArrayOutputStream stream = new ByteArrayOutputStream(); |
| PrintStream s = new PrintStream(stream); |
| error.printStackTrace(s); |
| stack = stream.toString(); |
| } |
| return stack; |
| } |
| } |
| } |