blob: 8d9655d87787e62093eb93176a4a5e01d6bb17d6 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}
}