blob: af7a290886d33b012eed666a4054adf640645c55 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2020 SAP AG and IBM Corporation.
* 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:
* SAP AG - initial API and implementation
* Andrew Johnson (IBM Corporation)- for comparisons
*******************************************************************************/
package org.eclipse.mat.inspections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.eclipse.core.runtime.Status;
import org.eclipse.mat.SnapshotException;
import org.eclipse.mat.collect.ArrayInt;
import org.eclipse.mat.collect.ArrayIntBig;
import org.eclipse.mat.collect.BitField;
import org.eclipse.mat.internal.MATPlugin;
import org.eclipse.mat.internal.Messages;
import org.eclipse.mat.query.Bytes;
import org.eclipse.mat.query.Column;
import org.eclipse.mat.query.ContextProvider;
import org.eclipse.mat.query.IContextObject;
import org.eclipse.mat.query.IContextObjectSet;
import org.eclipse.mat.query.IQuery;
import org.eclipse.mat.query.IResult;
import org.eclipse.mat.query.IResultTable;
import org.eclipse.mat.query.ResultMetaData;
import org.eclipse.mat.query.annotations.Argument;
import org.eclipse.mat.query.annotations.Category;
import org.eclipse.mat.query.annotations.CommandName;
import org.eclipse.mat.query.annotations.Icon;
import org.eclipse.mat.snapshot.ClassHistogramRecord;
import org.eclipse.mat.snapshot.ClassLoaderHistogramRecord;
import org.eclipse.mat.snapshot.Histogram;
import org.eclipse.mat.snapshot.IMultiplePathsFromGCRootsComputer;
import org.eclipse.mat.snapshot.ISnapshot;
import org.eclipse.mat.snapshot.MultiplePathsFromGCRootsRecord;
import org.eclipse.mat.snapshot.model.IClass;
import org.eclipse.mat.snapshot.model.IObject;
import org.eclipse.mat.util.IProgressListener;
import org.eclipse.mat.util.MessageUtil;
import com.ibm.icu.text.NumberFormat;
@CommandName("find_leaks")
@Category(Category.HIDDEN)
@Icon("/META-INF/icons/leak.gif")
public class FindLeaksQuery implements IQuery
{
// ///////////////////////////////////////////
//
// static fields
//
// ///////////////////////////////////////////
private final static Set<String> REFERENCE_FIELD_SET = new HashSet<String>(Arrays
.asList(new String[] { "referent" })); //$NON-NLS-1$
private final static int MAX_DEPTH = 1000;
// ////////////////////////////////////////////
//
// Command parameters
//
// ////////////////////////////////////////////
@Argument
public ISnapshot snapshot;
@Argument(isMandatory = false)
public int threshold_percent = 20;
@Argument(isMandatory = false)
public int max_paths = 10000;
// @Argument(isMandatory = false, flag = "big_drop_ratio")
public double big_drop_ratio = 0.7;
public IResult execute(IProgressListener listener) throws Exception
{
long totalHeap;
int[] topDominators;
totalHeap = snapshot.getSnapshotInfo().getUsedHeapSize();
topDominators = snapshot.getImmediateDominatedIds(-1);
long threshold = threshold_percent * totalHeap / 100;
/*
* find suspect single objects
*/
listener.subTask(Messages.FindLeaksQuery_SearchingSingleObjects);
ArrayInt suspiciousObjects = new ArrayInt();
Set<String> suspectNames = new HashSet<String>();
int i = 0;
while (i < topDominators.length && snapshot.getRetainedHeapSize(topDominators[i]) > threshold)
{
suspiciousObjects.add(topDominators[i]);
suspectNames.add(snapshot.getClassOf(topDominators[i]).getName());
i++;
}
if (listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
/*
* Find suspect classes
*/
listener.subTask(Messages.FindLeaksQuery_SearchingGroupsOfObjects);
Histogram histogram = groupByClasses(topDominators, listener);
ArrayList<ClassHistogramRecord> suspiciousClasses = new ArrayList<ClassHistogramRecord>();
ClassHistogramRecord[] classRecords = histogram.getClassHistogramRecords().toArray(new ClassHistogramRecord[0]);
Arrays.sort(classRecords, Histogram.reverseComparator(Histogram.COMPARATOR_FOR_RETAINEDHEAPSIZE));
int k = 0;
while (k < classRecords.length && classRecords[k].getRetainedHeapSize() > threshold)
{
// avoid showing class-suspect for s.th. which was found on object
// level
if (!suspectNames.contains(classRecords[k].getLabel()))
{
suspiciousClasses.add(classRecords[k]);
}
k++;
}
if (listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
/*
* build the results
*/
return buildResult(suspiciousObjects, suspiciousClasses, totalHeap, listener);
}
private Histogram groupByClasses(int[] dominated, IProgressListener listener) throws SnapshotException
{
Histogram histogram = snapshot.getHistogram(dominated, listener);
if (listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
Collection<ClassHistogramRecord> records = histogram.getClassHistogramRecords();
ClassHistogramRecord[] arr = new ClassHistogramRecord[records.size()];
int i = 0;
for (ClassHistogramRecord record : records)
{
record.setRetainedHeapSize(sumRetainedSize(record.getObjectIds(), snapshot));
arr[i++] = record;
if (i % 10 == 0 && listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
}
Collection<ClassLoaderHistogramRecord> loaderRecords = histogram.getClassLoaderHistogramRecords();
ClassLoaderHistogramRecord[] loaderArr = new ClassLoaderHistogramRecord[loaderRecords.size()];
i = 0;
for (ClassLoaderHistogramRecord record : loaderRecords)
{
long retainedSize = 0;
for (ClassHistogramRecord classRecord : record.getClassHistogramRecords())
{
retainedSize += classRecord.getRetainedHeapSize();
}
record.setRetainedHeapSize(retainedSize);
loaderArr[i++] = record;
}
return histogram;
}
private long sumRetainedSize(int[] objectIds, ISnapshot snapshot) throws SnapshotException
{
long sum = 0;
for (int id : objectIds)
{
sum += snapshot.getRetainedHeapSize(id);
}
return sum;
}
private AccumulationPoint findAccumulationPoint(int bigObjectId) throws SnapshotException
{
int dominator = bigObjectId;
double dominatorRetainedSize = snapshot.getRetainedHeapSize(dominator);
int dominated[] = snapshot.getImmediateDominatedIds(dominator);
int depth = 0;
while (dominated != null && dominated.length != 0 && depth < MAX_DEPTH)
{
double dominatedRetainedSize = snapshot.getRetainedHeapSize(dominated[0]);
if (dominatedRetainedSize / dominatorRetainedSize < big_drop_ratio) { return new AccumulationPoint(snapshot
.getObject(dominator)); }
dominatorRetainedSize = dominatedRetainedSize;
dominator = dominated[0];
dominated = snapshot.getImmediateDominatedIds(dominator);
depth++;
}
if (dominated == null || dominated.length == 0)
return new AccumulationPoint(snapshot.getObject(dominator));
return null;
}
private SuspectRecord buildSuspectRecordGroupOfObjects(ClassHistogramRecord record, IProgressListener listener)
throws SnapshotException
{
int[] objectIds = getRandomIds(record.getObjectIds());
IObject suspectClass = snapshot.getObject(record.getClassId());
// calculate the shortest paths to all
// avoid weak paths
Map<IClass, Set<String>> excludeMap = new HashMap<IClass, Set<String>>();
Collection<IClass> classes = snapshot.getClassesByName("java.lang.ref.WeakReference", true); //$NON-NLS-1$
if (classes != null)
for (IClass clazz : classes)
{
excludeMap.put(clazz, REFERENCE_FIELD_SET);
}
IMultiplePathsFromGCRootsComputer comp = snapshot.getMultiplePathsFromGCRoots(objectIds, excludeMap);
MultiplePathsFromGCRootsRecord[] records = comp.getPathsByGCRoot(listener);
ArrayIntBig commonPath = new ArrayIntBig();
if (listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
int numPaths = comp.getAllPaths(listener).length;
int diff = objectIds.length - numPaths;
if (diff > 0)
{
listener.sendUserMessage(IProgressListener.Severity.INFO, MessageUtil.format(
Messages.FindLeaksQuery_PathNotFound, diff, objectIds.length), null);
}
setRetainedSizesForMPaths(records, snapshot);
Arrays.sort(records, MultiplePathsFromGCRootsRecord.getComparatorByNumberOfReferencedObjects());
MultiplePathsFromGCRootsRecord parentRecord = records[0];
// parentRecord.getReferencedRetainedSize()
int threshold = (int) (0.8 * objectIds.length);
while (parentRecord.getCount() > threshold)
{
// System.out.println("count: " + parentRecord.getCount());
commonPath.add(parentRecord.getObjectId());
MultiplePathsFromGCRootsRecord[] children = parentRecord.nextLevel();
if (children == null || children.length == 0)
{
// reached the end ?! report the parent as it is big enough
AccumulationPoint accPoint = new AccumulationPoint(snapshot.getObject(parentRecord.getObjectId()));
SuspectRecordGroupOfObjects result = new SuspectRecordGroupOfObjects(suspectClass, record
.getObjectIds(), record.getRetainedHeapSize(), accPoint, commonPath.toArray(), comp);
return result;
}
setRetainedSizesForMPaths(children, snapshot);
Arrays.sort(children, MultiplePathsFromGCRootsRecord.getComparatorByNumberOfReferencedObjects());
if ((double) children[0].getReferencedRetainedSize() / (double) parentRecord.getReferencedRetainedSize() < big_drop_ratio)
{
// there is a big drop here - return the parent
AccumulationPoint accPoint = new AccumulationPoint(snapshot.getObject(parentRecord.getObjectId()));
SuspectRecordGroupOfObjects result = new SuspectRecordGroupOfObjects(suspectClass, record
.getObjectIds(), record.getRetainedHeapSize(), accPoint, commonPath.toArray(), comp);
return result;
}
// no big drop - take the biggest child and try again
parentRecord = children[0];
}
// return a SuspectRecord without an accumulation point
return new SuspectRecordGroupOfObjects(suspectClass, record.getObjectIds(), record.getRetainedHeapSize(), null,
commonPath.toArray(), comp);
}
private void setRetainedSizesForMPaths(MultiplePathsFromGCRootsRecord[] records, ISnapshot snapshot)
throws SnapshotException
{
for (MultiplePathsFromGCRootsRecord rec : records)
{
int[] referencedObjects = rec.getReferencedObjects();
long retained = 0;
for (int objectId : referencedObjects)
{
retained += snapshot.getRetainedHeapSize(objectId);
}
rec.setReferencedRetainedSize(retained);
}
}
private IResult buildResult(ArrayInt suspiciousObjects, ArrayList<ClassHistogramRecord> suspiciousClasses,
long totalHeap, IProgressListener listener) throws SnapshotException
{
SuspectRecord[] allSuspects = new SuspectRecord[suspiciousObjects.size() + suspiciousClasses.size()];
int j = 0;
int[] suspectObjIds = suspiciousObjects.toArray();
for (int objectId : suspectObjIds)
{
if (listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
IObject suspectObject = snapshot.getObject(objectId);
AccumulationPoint accPoint = findAccumulationPoint(objectId);
SuspectRecord r = new SuspectRecord(suspectObject, suspectObject.getRetainedHeapSize(), accPoint);
allSuspects[j++] = r;
}
for (ClassHistogramRecord record : suspiciousClasses)
{
if (listener.isCanceled())
throw new IProgressListener.OperationCanceledException();
SuspectRecord r = buildSuspectRecordGroupOfObjects(record, /*
* (long)
* (
* threshold
* 0.7),
*/listener);
allSuspects[j++] = r;
}
return new SuspectsResultTable(allSuspects, totalHeap);
}
private int[] getRandomIds(int[] objectIds)
{
if (objectIds.length <= max_paths)
return objectIds;
MATPlugin.log(new Status(Status.INFO, MATPlugin.PLUGIN_ID, MessageUtil.format(
Messages.FindLeaksQuery_TooManySuspects,
objectIds.length, max_paths)));
Random random = new Random();
int length = objectIds.length;
BitField visited = new BitField(length);
int[] result = new int[max_paths];
for (int i = 0; i < max_paths; i++)
{
int index = random.nextInt(length);
while (visited.get(index))
index = random.nextInt(length);
visited.set(index);
result[i] = objectIds[index];
}
return result;
}
public static class AccumulationPoint
{
IObject object;
public AccumulationPoint(IObject object)
{
this.object = object;
}
public IObject getObject()
{
return object;
}
public long getRetainedHeapSize()
{
return getObject().getRetainedHeapSize();
}
}
public static class AccumulationPointOfGroupOfObject extends AccumulationPoint
{
int[] commonPath;
IMultiplePathsFromGCRootsComputer pathsComputer;
public AccumulationPointOfGroupOfObject(IObject object, int[] commonPath,
IMultiplePathsFromGCRootsComputer pathsComputer)
{
super(object);
this.commonPath = commonPath;
this.pathsComputer = pathsComputer;
}
public int[] getCommonPath()
{
return commonPath;
}
public IMultiplePathsFromGCRootsComputer getPathsComputer()
{
return pathsComputer;
}
}
public static class SuspectRecordGroupOfObjects extends SuspectRecord
{
int[] commonPath;
IMultiplePathsFromGCRootsComputer pathsComputer;
int[] suspectInstances;
SuspectRecordGroupOfObjects(IObject suspect, int[] suspectInstances, long suspectRetained,
AccumulationPoint accumulationPoint, int[] commonPath,
IMultiplePathsFromGCRootsComputer pathsComputer)
{
super(suspect, suspectRetained, accumulationPoint);
this.suspectInstances = suspectInstances;
this.commonPath = commonPath;
this.pathsComputer = pathsComputer;
}
public int[] getCommonPath()
{
return commonPath;
}
public IMultiplePathsFromGCRootsComputer getPathsComputer()
{
return pathsComputer;
}
public int[] getSuspectInstances()
{
return suspectInstances;
}
}
public static class SuspectRecord
{
IObject suspect;
Bytes suspectRetained;
AccumulationPoint accumulationPoint;
SuspectRecord(IObject suspect, long suspectRetained, AccumulationPoint accumulationPoint)
{
this.suspect = suspect;
this.suspectRetained = new Bytes(suspectRetained);
this.accumulationPoint = accumulationPoint;
}
public IObject getSuspect()
{
return suspect;
}
public long getSuspectRetained()
{
return suspectRetained.getValue();
}
public AccumulationPoint getAccumulationPoint()
{
return accumulationPoint;
}
}
public static class SuspectsResultTable implements IResultTable
{
SuspectRecord[] data;
long totalHeap;
public SuspectsResultTable(SuspectRecord[] data, long totalHeap)
{
this.data = data;
this.totalHeap = totalHeap;
}
public ResultMetaData getResultMetaData()
{
return new ResultMetaData.Builder() //
.addContext(new ContextProvider(Messages.FindLeaksQuery_LeakSuspect)
{
@Override
public IContextObject getContext(Object row)
{
return getLeakSuspect(row);
}
}) //
.addContext(new ContextProvider(Messages.FindLeaksQuery_AccumulationPoint)
{
@Override
public IContextObject getContext(Object row)
{
return getAccumulationPoint(row);
}
}) //
.build();
}
public Column[] getColumns()
{
return new Column[] { new Column(Messages.FindLeaksQuery_ColumnLeakSuspect), //
new Column(Messages.FindLeaksQuery_Column_NumObjects, Long.class), //
new Column(Messages.FindLeaksQuery_Column_SuspectRetainedHeap, Bytes.class), //
new Column(Messages.FindLeaksQuery_Column_SuspectPercent, Double.class).formatting(NumberFormat.getPercentInstance()), //
new Column(Messages.FindLeaksQuery_Column_AccumulationPoint), //
new Column(Messages.FindLeaksQuery_Column_AccPointRetainedHeap, Bytes.class), //
new Column(Messages.FindLeaksQuery_Column_AccPointPercent, Double.class).formatting(NumberFormat.getPercentInstance()) };
}
public int getRowCount()
{
return data.length;
}
public SuspectRecord getRow(int rowId)
{
return data[rowId];
}
public Object getColumnValue(Object row, int columnIndex)
{
SuspectRecord suspect = (SuspectRecord) row;
switch (columnIndex)
{
case 0:
return suspect.suspect.getTechnicalName();
case 1:
return suspect instanceof SuspectRecordGroupOfObjects ? ((SuspectRecordGroupOfObjects) suspect).suspectInstances.length
: 1;
case 2:
return suspect.suspectRetained;
case 3:
return Double.valueOf((double) suspect.getSuspectRetained() / (double) totalHeap);
case 4:
return suspect.accumulationPoint == null ? Messages.FindLeaksQuery_NotFound : suspect.accumulationPoint.getObject()
.getTechnicalName();
case 5:
return new Bytes(suspect.accumulationPoint == null ? 0 : suspect.accumulationPoint
.getRetainedHeapSize());
case 6:
return suspect.accumulationPoint == null ? 0 : Double.valueOf((double) suspect.accumulationPoint
.getRetainedHeapSize()
/ (double) totalHeap);
}
return null;
}
public IContextObject getContext(final Object row)
{
return new IContextObject()
{
public int getObjectId()
{
return ((SuspectRecord) row).suspect.getObjectId();
}
};
}
IContextObject getLeakSuspect(Object row)
{
final SuspectRecord suspect = (SuspectRecord) row;
if (suspect instanceof SuspectRecordGroupOfObjects)
{
return new IContextObjectSet()
{
public int getObjectId()
{
return suspect.suspect.getObjectId();
}
public int[] getObjectIds()
{
return ((SuspectRecordGroupOfObjects) suspect).suspectInstances;
}
public String getOQL()
{
return null;
}
};
}
else
{
return new IContextObject()
{
public int getObjectId()
{
return suspect.suspect.getObjectId();
}
};
}
}
IContextObject getAccumulationPoint(final Object row)
{
final SuspectRecord suspect = (SuspectRecord) row;
if (suspect.accumulationPoint == null)
return null;
return new IContextObject()
{
public int getObjectId()
{
return suspect.accumulationPoint.getObject().getObjectId();
}
};
}
public SuspectRecord[] getData()
{
return data;
}
public long getTotalHeap()
{
return totalHeap;
}
}
}