561460: More comparison queries

First version to find leaks between two snapshots.

Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=561460
diff --git a/plugins/org.eclipse.mat.api/META-INF/reports/overview2.xml b/plugins/org.eclipse.mat.api/META-INF/reports/overview2.xml
new file mode 100644
index 0000000..be50872
--- /dev/null
+++ b/plugins/org.eclipse.mat.api/META-INF/reports/overview2.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+    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 - comparison queries

+ -->

+<section name="%overview2.title System Overview comparison" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

+	xmlns="http://www.eclipse.org/mat/report.xsd" xsi:schemaLocation="http://www.eclipse.org/mat/report.xsd platform:/resource/org.eclipse.mat.report/schema/report.xsd">

+

+	<param key="html.collapsed" value="false" />

+	<param key="filename_suffix" value="%overview2.suffix Overview Delta" />

+

+	<query name="%overview.heap_dump_overview">

+		<param key="html.show_table_header" value="false" />

+		<command>simplecomparison -query heap_dump_overview -baseline ${baseline}</command>

+	</query>

+	<query name="%overview.system_properties">

+		<param key="html.separate_file" value="true" />

+		<param key="sort_column" value="#0=ASC" />

+		<param key="hide_column" value="#1,#2" />

+		<param key="limit" value="1000" />

+		<command>simplecomparison -query system_properties -options "-keycolumn 2" -baseline ${baseline}</command>

+	</query>

+	<query name="%overview.threads">

+		<param key="limit" value="1000" />

+		<param key="html.separate_file" value="true" />

+		<param key="html.render_details" value="false" />

+		<command>simplecomparison -query thread_overview -baseline ${baseline}</command>

+	</query>

+	<query name="%overview.class_histogram">

+		<param key="html.separate_file" value="true" />

+		<param key="sort_column" value="#6" />

+		<command>simplecomparison -query histogram -retained APPROXIMATE -options "-mode DIFF_TO_PREVIOUS" -baseline ${baseline}</command>

+	</query>

+	<!-- Expensive to calculate, so use version from LeaKHunter2

+	<query name="%overview2.dominator_tree Dominator tree">

+		<param key="html.separate_file" value="true" />

+		<param key="sort_column" value="#5" />

+		<param key="hide_column" value="#7,#8,#9" />

+		<command>simplecomparison -query dominator_tree -options "-mode DIFF_RATIO_TO_FIRST" -baseline ${baseline}</command>

+	</query>

+	-->

+</section>

diff --git a/plugins/org.eclipse.mat.api/plugin.properties b/plugins/org.eclipse.mat.api/plugin.properties
index 7a7d467..aec98ad 100644
--- a/plugins/org.eclipse.mat.api/plugin.properties
+++ b/plugins/org.eclipse.mat.api/plugin.properties
@@ -23,11 +23,21 @@
 overview.top_consumers = Top Consumers

 overview.class_histogram = Class Histogram

 

+overview2.title = System Overview of Snapshots 

+# Suffix used to build filename for report zip file - only translate if required

+overview2.suffix = Overview Delta

+overview2.dominator_tree = Dominator Tree

+

 suspects.title = Leak Suspects

 # Suffix used to build filename for report zip file - only translate if required

 suspects.suffix = Leak Suspects

 suspects.leaks = Leaks

 

+suspects2.title = Leak Suspects by Snapshot Comparison 

+# Suffix used to build filename for report zip file - only translate if required

+suspects2.suffix = Leak Suspects Delta

+suspects2.leaks = Leaks Compared to Baseline

+

 top_components.title = Top Components

 # Suffix used to build filename for report zip file - only translate if required

 top_components.suffix = Top Components

@@ -48,6 +58,12 @@
 report.compare.name = __hidden__/Compare Snapshots

 report.compare.help = Compare two snapshots and generate a histogram of differences in objects by class.

 

+report.suspects2.name = __hidden__/Leak Suspects by snapshot comparison

+report.suspects2.help = includes leak suspects and a system overview

+report.overview2.name = __hidden__/Heap Dump Overview comparison

+report.overview2.help = includes class histogram, system properties and top consumers.

+

+

 contentType.java_heap_dump = Java Heap Dump

 

 extension-point.factory.name = Snapshot Factory

diff --git a/plugins/org.eclipse.mat.api/plugin.xml b/plugins/org.eclipse.mat.api/plugin.xml
index 16d9c44..965e32d 100644
--- a/plugins/org.eclipse.mat.api/plugin.xml
+++ b/plugins/org.eclipse.mat.api/plugin.xml
@@ -29,6 +29,8 @@
 		<query impl="org.eclipse.mat.internal.snapshot.inspections.MultiplePath2GCRootsQuery"/>

 		<!-- comparisons -->

 		<query impl="org.eclipse.mat.internal.snapshot.inspections.CompareTablesQuery"/>

+		<query impl="org.eclipse.mat.internal.snapshot.inspections.GroupByQuery"/>

+		<query impl="org.eclipse.mat.inspections.SimpleComparison"/>

 		

 		<!-- core -->

 		<query impl="org.eclipse.mat.inspections.HistogramQuery"/>

@@ -55,7 +57,10 @@
 		

 		<!-- leak identification -->

 		<query impl="org.eclipse.mat.inspections.LeakHunterQuery"/>

+		<query impl="org.eclipse.mat.inspections.LeakHunterQuery2"/>

 		<query impl="org.eclipse.mat.inspections.FindLeaksQuery"/>

+		<query impl="org.eclipse.mat.inspections.FindLeaksQuery2"/>

+		<query impl="org.eclipse.mat.inspections.ComparisonReport"/>

 		<query impl="org.eclipse.mat.inspections.BigDropsQuery"/>

 		<query impl="org.eclipse.mat.inspections.DeltaHistogram"/>

 		<query impl="org.eclipse.mat.inspections.ReferenceLeakQuery"/>

@@ -149,6 +154,12 @@
 		<report id="compare" name="%report.compare.name Compare Snapshots"

 				description="%report.compare.help"

 				file="META-INF/reports/compare.xml" />

+		<report id="suspects2" name="%report.suspects2.name Leak Suspects by snapshot comparison"

+				description="%report.suspects2.help"

+				file="META-INF/reports/suspects2.xml" />

+		<report id="overview2" name="%report.overview2.name Heap Dump Overview comparison"

+				description="%report.overview.help"

+				file="META-INF/reports/overview2.xml" />

 	</extension>

 

     <extension point="org.eclipse.mat.api.collectionExtractorProvider">

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/ComparisonReport.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/ComparisonReport.java
new file mode 100644
index 0000000..1373c16
--- /dev/null
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/ComparisonReport.java
@@ -0,0 +1,48 @@
+/*******************************************************************************

+ * Copyright (c) 2020 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:

+ *    Andrew Johnson (IBM Corporation) - initial API and implementation

+ *******************************************************************************/

+package org.eclipse.mat.inspections;

+

+import java.util.Collections;

+import java.util.List;

+

+import org.eclipse.mat.query.IQuery;

+import org.eclipse.mat.query.IResult;

+import org.eclipse.mat.query.annotations.Argument;

+import org.eclipse.mat.query.annotations.Argument.Advice;

+import org.eclipse.mat.query.annotations.HelpUrl;

+import org.eclipse.mat.query.annotations.Icon;

+import org.eclipse.mat.snapshot.ISnapshot;

+import org.eclipse.mat.snapshot.SnapshotFactory;

+import org.eclipse.mat.snapshot.query.SnapshotQuery;

+import org.eclipse.mat.util.IProgressListener;

+

+@Icon("/META-INF/icons/compare.gif")

+@HelpUrl("/org.eclipse.mat.ui.help/tasks/comparingdata.html")

+public class ComparisonReport implements IQuery

+{

+    @Argument

+    public ISnapshot snapshot;

+

+    @Argument(advice = Advice.SECONDARY_SNAPSHOT)

+    public ISnapshot baseline;

+

+    @Argument

+    public String report = "org.eclipse.mat.api:suspects2"; //$NON-NLS-1$

+

+    public IResult execute(IProgressListener listener) throws Exception

+    {

+        SnapshotQuery queryc = SnapshotQuery.parse("default_report "+report, snapshot); //$NON-NLS-1$

+        List<String> params = Collections.singletonList("baseline="+baseline.getSnapshotInfo().getPath()); //$NON-NLS-1$

+        queryc.setArgument("params", params); //$NON-NLS-1$

+        IResult ret = queryc.execute(listener);

+        return ret;

+    }

+}

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery.java
index 84ec502..af7a290 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2018 SAP AG and IBM Corporation.

+ * 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

@@ -7,6 +7,7 @@
  *

  * Contributors:

  *    SAP AG - initial API and implementation

+ *    Andrew Johnson (IBM Corporation)- for comparisons

  *******************************************************************************/

 package org.eclipse.mat.inspections;

 

@@ -382,6 +383,11 @@
         {

             return object;

         }

+

+        public long getRetainedHeapSize()

+        {

+            return getObject().getRetainedHeapSize();

+        }

     }

 

     public static class AccumulationPointOfGroupOfObject extends AccumulationPoint

@@ -546,11 +552,11 @@
                     return suspect.accumulationPoint == null ? Messages.FindLeaksQuery_NotFound : suspect.accumulationPoint.getObject()

                                     .getTechnicalName();

                 case 5:

-                    return new Bytes(suspect.accumulationPoint == null ? 0 : suspect.accumulationPoint.getObject()

+                    return new Bytes(suspect.accumulationPoint == null ? 0 : suspect.accumulationPoint

                                     .getRetainedHeapSize());

                 case 6:

                     return suspect.accumulationPoint == null ? 0 : Double.valueOf((double) suspect.accumulationPoint

-                                    .getObject().getRetainedHeapSize()

+                                    .getRetainedHeapSize()

                                     / (double) totalHeap);

             }

 

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery2.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery2.java
new file mode 100644
index 0000000..1113d14
--- /dev/null
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/FindLeaksQuery2.java
@@ -0,0 +1,700 @@
+/*******************************************************************************

+ * 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) - version for comparing two snapshots

+ *******************************************************************************/

+package org.eclipse.mat.inspections;

+

+import java.io.File;

+import java.util.ArrayList;

+import java.util.Arrays;

+import java.util.Collection;

+import java.util.HashMap;

+import java.util.HashSet;

+import java.util.Iterator;

+import java.util.List;

+import java.util.Map;

+import java.util.Random;

+import java.util.Set;

+import java.util.regex.Pattern;

+

+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.collect.HashMapIntObject;

+import org.eclipse.mat.inspections.FindLeaksQuery.SuspectRecord;

+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.Column.SortDirection;

+import org.eclipse.mat.query.ContextProvider;

+import org.eclipse.mat.query.IContextObject;

+import org.eclipse.mat.query.IQuery;

+import org.eclipse.mat.query.IResult;

+import org.eclipse.mat.query.IResultTree;

+import org.eclipse.mat.query.ISelectionProvider;

+import org.eclipse.mat.query.annotations.Argument;

+import org.eclipse.mat.query.annotations.Argument.Advice;

+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.query.refined.RefinedResultBuilder;

+import org.eclipse.mat.query.refined.RefinedTree;

+import org.eclipse.mat.query.results.CompositeResult;

+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.snapshot.query.SnapshotQuery;

+import org.eclipse.mat.util.IProgressListener;

+import org.eclipse.mat.util.MessageUtil;

+

+import com.ibm.icu.text.NumberFormat;

+

+@CommandName("find_leaks2")

+@Category(Category.HIDDEN)

+@Icon("/META-INF/icons/leak.gif")

+public class FindLeaksQuery2 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(advice = Advice.SECONDARY_SNAPSHOT)

+    public ISnapshot baseline;

+

+    @Argument(isMandatory = false)

+    public int threshold_percent = 2;

+

+    @Argument(isMandatory = false)

+    public int max_paths = 10000;

+

+    // @Argument(isMandatory = false, flag = "big_drop_ratio")

+    public double big_drop_ratio = 0.7;

+

+    @Argument(isMandatory = false)

+    public String options = "-prefix"; //$NON-NLS-1$

+

+    @Argument(isMandatory = false)

+    public Pattern mask = Pattern.compile("\\s@ 0x[0-9a-f]+|^\\[[0-9]+\\]$|(?<=\\p{javaJavaIdentifierPart}\\[)\\d+(?=\\])"); //$NON-NLS-1$

+

+    @Argument(isMandatory = false, flag = "x")

+    public String[] extraReferences = new String[] {

+                    "java.util.HashMap$Node:key", //$NON-NLS-1$

+                    "java.util.Hashtable$Entry:key", //$NON-NLS-1$

+                    "java.util.WeakHashMap$Entry:referent", //$NON-NLS-1$

+                    "java.util.concurrent.ConcurrentHashMap$Node:key" //$NON-NLS-1$

+    };

+

+    @Argument(isMandatory = false, flag = "xfile")

+    public File extraReferencesListFile;

+

+    static final int retainedDiffCol = 5;

+    static final int simpleDiffCol = 2;

+

+    public IResult execute(IProgressListener listener) throws Exception

+    {

+        long totalHeap;

+

+        totalHeap = baseline.getSnapshotInfo().getUsedHeapSize();

+        long threshold = threshold_percent * totalHeap / 100;

+

+        IResultTree baseTree = callDominatorTree(listener, baseline);

+        IResultTree currTree = callDominatorTree(listener, snapshot);

+

+        String queryId = "comparetablesquery -mode DIFF_RATIO_TO_FIRST"; //$NON-NLS-1$

+        if (options != null && options.length() > 0)

+            queryId += " " + options; //$NON-NLS-1$

+

+        SnapshotQuery queryc = SnapshotQuery.parse(queryId, snapshot);

+

+        List<IResultTree> r = new ArrayList<IResultTree>();

+        r.add(baseTree);

+        r.add(currTree);

+        queryc.setArgument("tables", r); //$NON-NLS-1$

+        ArrayList<ISnapshot> snapshots = new ArrayList<ISnapshot>();

+        snapshots.add(baseline);

+        snapshots.add(snapshot);

+        queryc.setArgument("snapshots", snapshots); //$NON-NLS-1$

+

+        if (mask != null && mask.pattern().length() > 0)

+            queryc.setArgument("mask", mask); //$NON-NLS-1$

+        if (extraReferences != null && extraReferences.length > 0)

+            queryc.setArgument("extraReferences", Arrays.asList(extraReferences)); //$NON-NLS-1$

+        if (extraReferencesListFile != null)

+            queryc.setArgument("extraReferencesListFile", extraReferencesListFile); //$NON-NLS-1$

+

+        RefinedResultBuilder rbc = queryc.refine(listener);

+        rbc.setSortOrder(retainedDiffCol, SortDirection.DESC);

+        final RefinedTree compTree = (RefinedTree)rbc.build();

+        List<?>topDominators = compTree.getElements();

+

+        /*

+         * find suspect single objects

+         */

+        listener.subTask(Messages.FindLeaksQuery_SearchingSingleObjects);

+

+        List<ContextProvider> provs = compTree.getResultMetaData().getContextProviders();

+        // Get the last - in case the first is also this snapshot, so has a context provider

+        ContextProvider cp = provs.get(provs.size() - 1);

+        ArrayInt suspiciousObjects = new ArrayInt();

+        Set<String> suspectNames = new HashSet<String>();

+        int i = 0;

+        HashMapIntObject<ClassRecord>map = new HashMapIntObject<ClassRecord>();

+        final HashMapIntObject<Object>rowmap = new HashMapIntObject<Object>(); 

+        while (i < topDominators.size())

+        {

+            Object row = topDominators.get(i);

+            IContextObject ctx = cp.getContext(row);

+            if (ctx != null)

+            {

+                int objId = ctx.getObjectId();

+                if (objId >= 0)

+                {

+                    long deltaRetained = readCol(compTree, row, retainedDiffCol);

+                    IClass cls = snapshot.getClassOf(objId);

+                    if (deltaRetained > threshold)

+                    {

+                        suspiciousObjects.add(objId);

+                        suspectNames.add(cls.getName());

+                        rowmap.put(objId, row);

+                    }

+                    else

+                    {

+                        // Add to types

+                        ClassRecord cr = map.get(cls.getObjectId());

+                        if (cr == null)

+                        {

+                            cr = new ClassRecord(cls.getName(), cls.getObjectId());

+                            map.put(cls.getObjectId(), cr);

+                        }

+                        long deltaSimple = readCol(compTree, row, simpleDiffCol);

+                        cr.addObj(objId, deltaSimple, deltaRetained);

+                        rowmap.put(objId, row);

+                    }

+                }

+            }

+            i++;

+        }

+

+        if (listener.isCanceled())

+            throw new IProgressListener.OperationCanceledException();

+

+        /*

+         * Find suspect classes

+         */

+        listener.subTask(Messages.FindLeaksQuery_SearchingGroupsOfObjects);

+

+        ArrayList<ClassHistogramRecord> suspiciousClasses = new ArrayList<ClassHistogramRecord>();

+

+        for (Iterator<ClassRecord> it = map.values(); it.hasNext(); )

+        {

+            ClassRecord cr = it.next();

+            if (cr.retained > threshold)

+            {

+                ClassHistogramRecord chr = new ClassHistogramRecord(cr.name, cr.clsId, cr.objs.toArray(), cr.simple, cr.retained);

+                suspiciousClasses.add(chr);

+            }

+        }

+

+        if (listener.isCanceled())

+            throw new IProgressListener.OperationCanceledException();

+

+        /*

+         * build the results

+         */

+        final SuspectsResultTable ret = buildResult(suspiciousObjects, suspiciousClasses, totalHeap, compTree, rowmap, listener);

+

+        // Indicate some interesting rows

+        compTree.setSelectionProvider(new ISelectionProvider() {

+

+            public boolean isSelected(Object row)

+            {

+                if (row == null)

+                    return false;

+                List<ContextProvider> provs = compTree.getResultMetaData().getContextProviders();

+                IContextObject co = provs.get(provs.size() - 1).getContext(row);

+                if (co == null)

+                    return false;

+                int objId = co.getObjectId();

+                if (objId < 0)

+                    return false;

+                for (SuspectRecord sr : ret.getData())

+                {

+                    if (sr instanceof FindLeaksQuery.SuspectRecordGroupOfObjects)

+                    {

+                        // This is a group of objects, so check each one

+                        FindLeaksQuery.SuspectRecordGroupOfObjects srg = (FindLeaksQuery.SuspectRecordGroupOfObjects)sr;

+                        for (int o1 : srg.getSuspectInstances())

+                        {

+                            if (objId == o1)

+                                return true;

+                        }

+                        /*

+                         * Not required to check the suspect ID as that is the class

+                         * but the dominator tree will be instances.

+                         */

+                    }

+                    else

+                    {

+                        if (objId == sr.getSuspect().getObjectId())

+                            return true;

+                    }

+                    org.eclipse.mat.inspections.FindLeaksQuery.AccumulationPoint accumulationPoint = sr.getAccumulationPoint();

+                    if (accumulationPoint != null && objId == accumulationPoint.getObject().getObjectId())

+                        return true;

+                }

+                return false;

+            }

+

+            public boolean isExpanded(Object row)

+            {

+                /* How deep - HtmlOutputter has a limit of 100, screen 25 */

+                final int MAX_EXPAND = 30;

+                if (row == null)

+                    return false;

+                List<ContextProvider> provs = compTree.getResultMetaData().getContextProviders();

+                IContextObject co = provs.get(provs.size() - 1).getContext(row);

+                if (co == null)

+                    return false;

+                int objId = co.getObjectId();

+                if (objId < 0)

+                    return false;

+                for (SuspectRecord sr : ret.getData())

+                {

+                    if (sr instanceof FindLeaksQuery.SuspectRecordGroupOfObjects)

+                    {

+                        FindLeaksQuery.SuspectRecordGroupOfObjects srg = (FindLeaksQuery.SuspectRecordGroupOfObjects)sr;

+                        for (int i = 0; i < Math.min(srg.getCommonPath().length - 1, MAX_EXPAND); ++i)

+                        {

+                            if (objId == srg.getCommonPath()[i])

+                                return true;

+                        }

+                    }

+                    org.eclipse.mat.inspections.FindLeaksQuery.AccumulationPoint accumulationPoint = sr.getAccumulationPoint();

+                    if (accumulationPoint instanceof AccumulationPoint)

+                    {

+                        AccumulationPoint ap2 = (AccumulationPoint)accumulationPoint;

+                        for (int i = 0; i < Math.min(ap2.getPath().length - 1, MAX_EXPAND); ++i)

+                        {

+                            if (objId == ap2.getPath()[i])

+                                return true;

+                        }

+                    }

+                }

+                return false;

+            }

+

+        });

+        CompositeResult cr = new CompositeResult();

+        cr.addResult(Messages.FindLeaksQuery2_ComparedDominatorTrees, compTree);

+        cr.addResult(Messages.FindLeaksQuery2_Leaks, ret);

+        return cr;

+    }

+

+    /**

+     * Read a column cell from the compared tree.

+     * @param compTree

+     * @param row

+     * @param retainedDiffCol

+     * @return

+     */

+    private long readCol(IResultTree compTree, final Object row, final int retainedDiffCol)

+    {

+        Object colv = compTree.getColumnValue(row, retainedDiffCol);

+        long deltaRetained;

+        if (colv instanceof Bytes)

+            deltaRetained = ((Bytes)colv).getValue();

+        else if (colv instanceof Number)

+            deltaRetained = ((Number)colv).longValue();

+        else 

+            deltaRetained = 0;

+        return deltaRetained;

+    }

+

+    private static class ClassRecord

+    {

+        String name;

+        int clsId;

+        ArrayInt objs;

+        long simple;

+        long retained;

+        public ClassRecord(String name, int id)

+        {

+            this.name = name;

+            clsId = id;

+            objs = new ArrayInt();

+        }

+

+        public void addObj(int obj, long simple, long retained)

+        {

+            objs.add(obj);

+            this.simple += simple;

+            this.retained += retained;

+        }

+    }

+

+    private IResultTree callDominatorTree(IProgressListener listener, ISnapshot snapshot) throws Exception

+    {

+        return (IResultTree) SnapshotQuery.lookup("dominator_tree", snapshot) //$NON-NLS-1$

+                        .execute(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;

+    }

+

+    /**

+     * Find the accumulation point by big drops in the delta size

+     * @param bigObjectId

+     * @param tree

+     * @param row

+     * @return

+     * @throws SnapshotException

+     */

+    private FindLeaksQuery.AccumulationPoint findAccumulationPoint(int bigObjectId, IResultTree tree, Object row) throws SnapshotException

+    {

+        int dominator = bigObjectId;

+        ArrayInt path = new ArrayInt();

+        path.add(dominator);

+        List<?> rows = null;

+        long dominatorRetainedSize = readCol(tree, row, retainedDiffCol);

+

+        List<ContextProvider> provs = tree.getResultMetaData().getContextProviders();

+        ContextProvider cp = provs.get(provs.size() - 1);

+        int depth = 0;

+        while (tree.hasChildren(row) && (rows = tree.getChildren(row)) != null && rows.size() != 0 && depth < MAX_DEPTH)

+        {

+            long dominatedRetainedSize = this.readCol(tree, rows.get(0), retainedDiffCol); 

+            if ((double)dominatedRetainedSize / dominatorRetainedSize < big_drop_ratio)

+            {

+                return new AccumulationPoint(snapshot.getObject(dominator), dominatorRetainedSize, path.toArray()); 

+            }

+

+            dominatorRetainedSize = dominatedRetainedSize;

+            row = rows.get(0);

+            // Should be a context if there was a retained size

+            dominator = cp.getContext(row).getObjectId();

+            path.add(dominator);

+            depth++;

+        }

+

+        if (rows == null || rows.size() == 0)

+            return new AccumulationPoint(snapshot.getObject(dominator), dominatorRetainedSize, path.toArray());

+

+        return null;

+    }

+

+    private FindLeaksQuery.SuspectRecord buildSuspectRecordGroupOfObjects(ClassHistogramRecord record, IResultTree tree, HashMapIntObject<Object>rowmap, IProgressListener listener)

+                    throws SnapshotException

+    {

+        int[] objectIds = getRandomIds(record.getObjectIds());

+        IObject suspectClass = snapshot.getObject(record.getClassId());

+        List<ContextProvider> provs = tree.getResultMetaData().getContextProviders();

+

+        // 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);

+

+        Object row = null;

+        while (parentRecord.getCount() > threshold)

+        {

+            // System.out.println("count: " + parentRecord.getCount());

+            commonPath.add(parentRecord.getObjectId());

+

+            // Try to match path in dominator tree

+            if (row == null)

+            {

+                // possibly find in dominator tree

+                row = rowmap.get(parentRecord.getObjectId());

+            }

+

+            MultiplePathsFromGCRootsRecord[] children = parentRecord.nextLevel();

+            if (children == null || children.length == 0)

+            {

+                // reached the end ?! report the parent as it is big enough

+                int path[] = commonPath.toArray();

+                FindLeaksQuery.AccumulationPoint accPoint = new FindLeaksQuery.AccumulationPoint(snapshot.getObject(parentRecord.getObjectId()));

+                if (rowmap.get(parentRecord.getObjectId()) == row)

+                {

+                    // Row is current, so use delta size

+                    long deltaRetained = readCol(tree, row, retainedDiffCol);

+                    accPoint = new AccumulationPoint(snapshot.getObject(parentRecord.getObjectId()), deltaRetained, path);

+                }

+                FindLeaksQuery.SuspectRecordGroupOfObjects result = new FindLeaksQuery.SuspectRecordGroupOfObjects(suspectClass, record

+                                .getObjectIds(), record.getRetainedHeapSize(), accPoint, commonPath.toArray(), comp);

+                return result;

+            }

+            setRetainedSizesForMPaths(children, snapshot);

+            Arrays.sort(children, MultiplePathsFromGCRootsRecord.getComparatorByNumberOfReferencedObjects());

+

+            long childReferencedRetainedSize = children[0].getReferencedRetainedSize();

+

+            // Match child?

+            if (row != null && tree.hasChildren(row))

+            {

+                //System.out.println("Finding ");

+                for (Object row2 : tree.getChildren(row))

+                {

+                    IContextObject co = provs.get(provs.size() - 1).getContext(row2);

+                    if (co != null && co.getObjectId() == children[0].getObjectId())

+                    {

+                        // Found again

+                        row = row2;

+                        //System.out.println("Found again "+parentRecord.getObjectId() + " " + row);

+                        break;

+                    }

+                }

+            }

+            if ((double) childReferencedRetainedSize / (double) parentRecord.getReferencedRetainedSize() < big_drop_ratio)

+            {

+                // there is a big drop here - return the parent

+                int path[] = commonPath.toArray();

+                FindLeaksQuery.AccumulationPoint accPoint = new FindLeaksQuery.AccumulationPoint(snapshot.getObject(parentRecord.getObjectId()));

+                if (rowmap.get(parentRecord.getObjectId()) == row)

+                {

+                    // Row is current, so use delta size

+                    long deltaRetained = readCol(tree, row, retainedDiffCol);

+                    accPoint = new AccumulationPoint(snapshot.getObject(parentRecord.getObjectId()), deltaRetained, path);

+                }

+                FindLeaksQuery.SuspectRecordGroupOfObjects result = new FindLeaksQuery.SuspectRecordGroupOfObjects(suspectClass, record

+                                .getObjectIds(), record.getRetainedHeapSize(), accPoint, path, 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 FindLeaksQuery.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 SuspectsResultTable buildResult(ArrayInt suspiciousObjects, ArrayList<ClassHistogramRecord> suspiciousClasses,

+                    long totalHeap, IResultTree tree, HashMapIntObject<Object>rowmap, IProgressListener listener) throws SnapshotException

+    {

+        FindLeaksQuery.SuspectRecord[] allSuspects = new FindLeaksQuery.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);

+            FindLeaksQuery.AccumulationPoint accPoint = findAccumulationPoint(objectId, tree, rowmap.get(objectId));

+            long suspectObjectRetained = suspectObject.getRetainedHeapSize();

+            Object row = rowmap.get(objectId);

+            long deltaRetained = readCol(tree, row, retainedDiffCol);

+            suspectObjectRetained = deltaRetained;

+            FindLeaksQuery.SuspectRecord r = new FindLeaksQuery.SuspectRecord(suspectObject, suspectObjectRetained, accPoint);

+

+            allSuspects[j++] = r;

+        }

+

+        for (ClassHistogramRecord record : suspiciousClasses)

+        {

+            if (listener.isCanceled())

+                throw new IProgressListener.OperationCanceledException();

+

+            FindLeaksQuery.SuspectRecord r = buildSuspectRecordGroupOfObjects(record, /*

+             * (long)

+             * (

+             * threshold

+             * 0.7),

+             */tree, rowmap, 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 extends FindLeaksQuery.AccumulationPoint

+    {

+        long retainedSize;

+        int path[];

+

+        public AccumulationPoint(IObject object, long retainedSize, int path[])

+        {

+            super(object);

+            this.retainedSize = retainedSize;

+            this.path = path;

+        }

+

+        public long getRetainedHeapSize()

+        {

+            return this.retainedSize;

+        }

+

+        public int[] getPath()

+        {

+            return path;

+        }

+    }

+

+    public static class SuspectsResultTable extends FindLeaksQuery.SuspectsResultTable

+    {

+

+        public SuspectsResultTable(SuspectRecord[] data, long totalHeap)

+        {

+            super(data, totalHeap);

+        }

+

+        public Column[] getColumns()

+        {

+            return new Column[] { new Column(Messages.FindLeaksQuery_ColumnLeakSuspect), //

+                            new Column(Messages.FindLeaksQuery_Column_NumObjects, Long.class), //

+                            new Column(Messages.FindLeaksQuery2_Column_SuspectRetainedHeap, Bytes.class), //

+                            new Column(Messages.FindLeaksQuery_Column_SuspectPercent, Double.class).formatting(NumberFormat.getPercentInstance()), //

+                            new Column(Messages.FindLeaksQuery_Column_AccumulationPoint), //

+                            new Column(Messages.FindLeaksQuery2_Column_AccPointRetainedHeap, Bytes.class), //

+                            new Column(Messages.FindLeaksQuery_Column_AccPointPercent, Double.class).formatting(NumberFormat.getPercentInstance()) };

+        }

+

+    }

+

+}

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
index 69228bf..50cde92 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
@@ -189,7 +189,7 @@
         }

     }

 

-    private FindLeaksQuery.SuspectsResultTable callFindLeaks(IProgressListener listener) throws Exception

+    FindLeaksQuery.SuspectsResultTable callFindLeaks(IProgressListener listener) throws Exception

     {

         return (FindLeaksQuery.SuspectsResultTable) SnapshotQuery.lookup("find_leaks", snapshot) //$NON-NLS-1$

                         .setArgument("threshold_percent", threshold_percent) //$NON-NLS-1$

@@ -348,7 +348,7 @@
                 objectsForTroubleTicketInfo.add(accPointClassloader);

 

                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

-                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedBy, classloaderName));

+                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedBy, classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

 

             }

             else if (snapshot.isClass(accumulationPointId))

@@ -362,7 +362,7 @@
                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

 

                 overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByLoadedBy, HTMLUtils.escapeText(clazz.getName()),
-                                classloaderName));

+                                classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

             else

             {

@@ -376,7 +376,7 @@
 

                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

                 overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByInstance, HTMLUtils.escapeText(className),
-                                classloaderName));

+                                classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

         }

 

@@ -507,7 +507,7 @@
                 involvedClassLoaders.add((IClassLoader) suspect.getAccumulationPoint().getObject());

                 classloaderName = getClassLoaderName(suspect.getAccumulationPoint().getObject(), keywords);

                 builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedFromClassLoader,

-                                classloaderName));

+                               classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

             else if (snapshot.isClass(accumulationPointId))

             {

@@ -521,7 +521,7 @@
                 classloaderName = getClassLoaderName(accPointClassloader, keywords);

 

                 builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedFromClass, className,

-                                classloaderName));

+                                classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

             else

             {

@@ -536,7 +536,7 @@
                 classloaderName = getClassLoaderName(accPointClassloader, keywords);

 

                 builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedFromInstance, className,

-                                classloaderName));

+                                classloaderName, formatRetainedHeap(suspect.getAccumulationPoint().getRetainedHeapSize(), totalHeap)));

             }

         }

         builder.append("<br><br>"); //$NON-NLS-1$

@@ -662,7 +662,7 @@
         try

         {

             Collection<IClass> classesByName = ic.getSnapshot().getClassesByName(className, false);

-            if (className.matches("[\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}.]+") && classesByName != null && classesByName.size() == 1) //$NON-NLS-1$

+            if (className.matches("\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}.]*") && classesByName != null && classesByName.size() == 1) //$NON-NLS-1$

             {

                 return className;

             }

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery2.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery2.java
new file mode 100644
index 0000000..54397d2
--- /dev/null
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery2.java
@@ -0,0 +1,151 @@
+/*******************************************************************************

+ * Copyright (c) 2020 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:

+ *    Andrew Johnson (IBM Corporation) - initial API and implementation

+ *******************************************************************************/

+package org.eclipse.mat.inspections;

+

+import java.io.File;

+import java.text.Format;

+import java.util.Arrays;

+import java.util.regex.Pattern;

+

+import org.eclipse.mat.query.BytesFormat;

+import org.eclipse.mat.query.IResult;

+import org.eclipse.mat.query.annotations.Argument;

+import org.eclipse.mat.query.annotations.Argument.Advice;

+import org.eclipse.mat.query.annotations.CommandName;

+import org.eclipse.mat.query.annotations.Icon;

+import org.eclipse.mat.query.results.CompositeResult;

+import org.eclipse.mat.report.Params;

+import org.eclipse.mat.report.QuerySpec;

+import org.eclipse.mat.report.SectionSpec;

+import org.eclipse.mat.snapshot.Histogram;

+import org.eclipse.mat.snapshot.ISnapshot;

+import org.eclipse.mat.snapshot.query.SnapshotQuery;

+import org.eclipse.mat.util.IProgressListener;

+

+/**

+ * Looks for leaks based on a delta in retained sizes of

+ * the dominator tree from two snapshots. 

+ */

+@CommandName("leakhunter2")

+@Icon("/META-INF/icons/leak.gif")

+public class LeakHunterQuery2 extends LeakHunterQuery

+{

+

+    @Argument(advice = Advice.SECONDARY_SNAPSHOT)

+    public ISnapshot baseline;

+

+    @Argument(isMandatory = false)

+    public String options = "-prefix"; //$NON-NLS-1$

+

+    @Argument(isMandatory = false)

+    public Pattern mask = Pattern.compile("\\s@ 0x[0-9a-f]+|^\\[[0-9]+\\]$|(?<=\\p{javaJavaIdentifierPart}\\[)\\d+(?=\\])"); //$NON-NLS-1$

+

+    @Argument(isMandatory = false, flag = "x")

+    public String[] extraReferences = new String[] {

+                    "java.util.HashMap$Node:key", //$NON-NLS-1$

+                    "java.util.Hashtable$Entry:key", //$NON-NLS-1$

+                    "java.util.WeakHashMap$Entry:referent", //$NON-NLS-1$

+                    "java.util.concurrent.ConcurrentHashMap$Node:key" //$NON-NLS-1$

+    };

+

+    @Argument(isMandatory = false, flag = "xfile")

+    public File extraReferencesListFile;

+

+    public IResult execute(IProgressListener listener) throws Exception

+    {

+        // Get a signed bytes formatter

+        Histogram dummy = snapshot.getHistogram(new int[0], listener);

+        dummy = dummy.diffWithBaseline(dummy);

+        Format f = dummy.getColumns()[2].getFormatter();

+        if (f instanceof BytesFormat)

+            bytesFormatter = (BytesFormat)f;

+

+        IResult res = super.execute(listener);

+        if (res instanceof SectionSpec)

+        {

+            // Add in saved dominator tree

+            QuerySpec spec = new QuerySpec(savedResult.getName());

+            spec.setResult(savedResult.getResult());

+            spec.setCommand(savedcmd);

+            spec.set(Params.Html.COLLAPSED, Boolean.TRUE.toString());

+            spec.set(Params.Rendering.SORT_COLUMN, "#5"); //$NON-NLS-1$

+            spec.set(Params.Rendering.HIDE_COLUMN, "#7,#8,#9"); //$NON-NLS-1$

+            ((SectionSpec) res).add(spec);

+        }

+        return res;

+    }

+

+    CompositeResult.Entry savedResult;

+    String savedcmd;

+

+    FindLeaksQuery.SuspectsResultTable callFindLeaks(IProgressListener listener) throws Exception

+    {

+        String querycmd = "find_leaks2"; //$NON-NLS-1$

+        if (options != null)

+            querycmd += " -options " + options; //$NON-NLS-1$

+        StringBuilder cmd = new StringBuilder(querycmd);

+        SnapshotQuery query = SnapshotQuery.parse(querycmd, snapshot)

+                        .setArgument("threshold_percent", threshold_percent) //$NON-NLS-1$

+                        .setArgument("max_paths", max_paths) //$NON-NLS-1$

+                        .setArgument("baseline", baseline); //$NON-NLS-1$

+

+        cmd.append(" -threshold_percent ").append(threshold_percent); //$NON-NLS-1$

+        cmd.append(" -max_paths ").append(max_paths); //$NON-NLS-1$

+        cmd.append(" -baseline ").append(baseline.getSnapshotInfo().getPath()); //$NON-NLS-1$

+        if (mask != null)

+        {

+            query.setArgument("mask", mask); //$NON-NLS-1$

+            cmd.append(" ").append("-mask ").append(escape(mask.pattern())); //$NON-NLS-1$ //$NON-NLS-2$

+        }

+        if (extraReferences != null)

+        {

+            query.setArgument("extraReferences", Arrays.asList(extraReferences)); //$NON-NLS-1$

+            cmd.append(" ").append("-x "); //$NON-NLS-1$ //$NON-NLS-2$

+            for (String e : extraReferences)

+                cmd.append(" ").append(escape(e)); //$NON-NLS-1$

+        }

+        if (extraReferencesListFile != null)

+        {

+            query.setArgument("extraReferencesListFile", extraReferencesListFile); //$NON-NLS-1$

+            cmd.append(" -xfile ").append(escape(extraReferencesListFile.getAbsolutePath())); //$NON-NLS-1$

+        }

+

+        savedcmd = cmd.toString();

+        IResult ret = query.execute(listener);

+        if (ret instanceof CompositeResult)

+        {

+            CompositeResult cr = (CompositeResult)ret;

+            // The delta dominator tree, save for later.

+            savedResult = cr.getResultEntries().get(0);

+            // The leaks, pass back to LinkHunterQuery.

+            return (FindLeaksQuery.SuspectsResultTable)cr.getResultEntries().get(1).getResult();

+        }

+        else

+        {

+            return (FindLeaksQuery.SuspectsResultTable)ret;

+        }

+    }

+

+    static String escape(String s)

+    {

+        /*

+         * abc\def -> abc\def

+         * abc"def -> abc\"def

+         * abc\\def -> abc\\\def

+         * abc\"def -> abc\\\"def

+         */

+        s = s.replaceAll("\\\\(?=\\\\)|\\\\(?=\")|\"", "\\\\$0"); //$NON-NLS-1$ //$NON-NLS-2$

+        if (s.indexOf(' ') >= 0)

+            return "\"" + s + "\""; //$NON-NLS-1$ //$NON-NLS-2$

+        return s;

+    }

+

+}

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/SimpleComparison.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/SimpleComparison.java
new file mode 100644
index 0000000..40302f4
--- /dev/null
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/SimpleComparison.java
@@ -0,0 +1,101 @@
+/*******************************************************************************

+ * Copyright (c) 2020 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:

+ *    Andrew Johnson (IBM Corporation) - initial API and implementation

+ *******************************************************************************/

+package org.eclipse.mat.inspections;

+

+import java.util.ArrayList;

+import java.util.List;

+

+import org.eclipse.mat.query.IQuery;

+import org.eclipse.mat.query.IResult;

+import org.eclipse.mat.query.IStructuredResult;

+import org.eclipse.mat.query.annotations.Argument;

+import org.eclipse.mat.query.annotations.Argument.Advice;

+import org.eclipse.mat.query.annotations.HelpUrl;

+import org.eclipse.mat.query.annotations.Icon;

+import org.eclipse.mat.query.refined.RefinedResultBuilder;

+import org.eclipse.mat.snapshot.ISnapshot;

+import org.eclipse.mat.snapshot.query.RetainedSizeDerivedData;

+import org.eclipse.mat.snapshot.query.SnapshotQuery;

+import org.eclipse.mat.util.IProgressListener;

+

+/**

+ * A simple comparison of the results of running a query on two

+ * different snapshots.

+ */

+@Icon("/META-INF/icons/compare.gif")

+@HelpUrl("/org.eclipse.mat.ui.help/tasks/comparingdata.html")

+public class SimpleComparison implements IQuery

+{

+    @Argument

+    public ISnapshot snapshot;

+

+    @Argument(advice = Advice.SECONDARY_SNAPSHOT)

+    public ISnapshot baseline;

+

+    @Argument

+    public String query;

+

+    @Argument(isMandatory = false)

+    public String options;

+

+    @Argument(isMandatory = false)

+    public String defaultoptions = "-mask \"\\s@ 0x[0-9a-f]+|^\\[[0-9]+\\]$|(?<=\\p{javaJavaIdentifierPart}\\[)\\d+(?=\\])\" -x java.util.HashMap$Node:key java.util.Hashtable$Entry:key java.util.WeakHashMap$Entry:referent java.util.concurrent.ConcurrentHashMap$Node:key"; //$NON-NLS-1$

+

+    public enum Retained {

+        APPROXIMATE,

+        PRECISE

+    };

+

+    @Argument(isMandatory = false)

+    public Retained retained;

+

+    public IResult execute(IProgressListener listener) throws Exception

+    {

+        IStructuredResult baseTable = callQuery(listener, baseline);

+        IStructuredResult currTable = callQuery(listener, snapshot);

+

+        String queryId = "comparetablesquery"; //$NON-NLS-1$

+        if (options != null && options.length() > 0)

+            queryId += " " + options; //$NON-NLS-1$

+        if (defaultoptions != null && defaultoptions.length() > 0)

+            queryId += " " + defaultoptions; //$NON-NLS-1$

+

+        SnapshotQuery queryc = SnapshotQuery.parse(queryId, snapshot);

+

+        List<IStructuredResult> r = new ArrayList<IStructuredResult>();

+        r.add(baseTable);

+        r.add(currTable);

+        queryc.setArgument("tables", r); //$NON-NLS-1$

+        ArrayList<ISnapshot> snapshots = new ArrayList<ISnapshot>();

+        snapshots.add(baseline);

+        snapshots.add(snapshot);

+        queryc.setArgument("snapshots", snapshots); //$NON-NLS-1$

+        IResult ret = queryc.execute(listener);

+        listener.done();

+        return ret;

+    }

+

+    private IStructuredResult callQuery(IProgressListener listener, ISnapshot snapshot) throws Exception

+    {

+        if (retained != null)

+        {

+            RefinedResultBuilder rb1 = SnapshotQuery.parse(query, snapshot).refine(listener);

+            rb1.setInlineRetainedSizeCalculation(true);

+            rb1.addDefaultContextDerivedColumn(retained == Retained.PRECISE ? RetainedSizeDerivedData.PRECISE : RetainedSizeDerivedData.APPROXIMATE);

+            return rb1.build();

+        }

+        else

+        {

+            return (IStructuredResult) SnapshotQuery.parse(query, snapshot)

+                            .execute(listener);

+        }

+    }

+}

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/annotations.properties b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/annotations.properties
index 0e270c3..4837a26 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/annotations.properties
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/annotations.properties
@@ -40,6 +40,14 @@
 ClassReferrersQuery.inbound.help = When a class group is expanded find all the objects pointing to any object in the group \
 and then group those objects by class.
 
+ComparisonReport.name = Compare Snapshots Leak Report
+ComparisonReport.category = Leak Identification
+ComparisonReport.help = Runs a report designed to compare two snapshots.\
+The default report includes leak suspects based on the growth in memory usage \
+and a system overview comparison.
+ComparisonReport.report.help = The report identifier.
+ComparisonReport.baseline.help = The baseline snapshot filename.
+
 CustomizedRetainedSetQuery.name = Customized Retained Set
 CustomizedRetainedSetQuery.category = Java Basics
 CustomizedRetainedSetQuery.help = Calculate the retained set of objects excluding references via given fields.\n\n\
@@ -69,7 +77,7 @@
 
 DeltaHistogram.name = Compare Snapshots
 DeltaHistogram.help = Compare the histograms of two snapshots by generating a difference histogram.
-DeltaHistogram.snapshot2.help = The second snapshot file.
+DeltaHistogram.snapshot2.help = The baseline snapshot file.
 
 DuplicatedClassesQuery.name = Duplicate Classes
 DuplicatedClassesQuery.category = Java Basics
@@ -105,6 +113,10 @@
 HeapDumpInfoQuery.name = Heap Dump Overview
 HeapDumpInfoQuery.help = Displays heap dump details: number of objects, etc.
 
+HeapDumpInfoQuery2.name = Heap Dump Overview comparison
+HeapDumpInfoQuery2.help = Displays heap dump details: number of objects, etc.
+HeapDumpInfoQuery2.baseline.help = The baseline snapshot file.
+
 HistogramQuery.name = Show As Histogram
 HistogramQuery.category = Java Basics
 HistogramQuery.help = Create a histogram from an arbitrary set of objects. The objects are grouped by class, superclass, classloader or package.\n\n\
@@ -135,6 +147,20 @@
 LeakHunterQuery.threshold_percent.help = Big memory chunk size in percent of the heap. (Default value is 10%.)
 LeakHunterQuery.max_paths.help = Number of paths to garbage collection roots. (Default value is 10000.)
 
+LeakHunterQuery2.name = Find Leaks between Snapshots
+LeakHunterQuery2.category = Leak Identification
+LeakHunterQuery2.help = Report potential memory leaks.\n\n\
+The query compares the dominator trees from two snapshots and searches for big \
+increases in memory chunks \
+(by default more than 10% of the total baseline heap). These could be single objects \
+or groups of objects from the same class. Then it tries to automatically find the exact \
+accumulation point - usually an array or a collection.
+LeakHunterQuery2.baseline.help = The baseline snapshot file.
+LeakHunterQuery2.options.help = Extra options to use to compare dominator trees.
+LeakHunterQuery2.mask.help = The 'mask' option for the compare.
+LeakHunterQuery2.extraReferences.help = The 'x' option for the compare.
+LeakHunterQuery2.extraReferencesListFile.help = The 'xfile' option for the compare.
+
 ObjectListQuery.name = List Objects
 ObjectListQuery.menu.0.category = 1|List objects
 ObjectListQuery.menu.0.label = 1|with outgoing references
@@ -189,6 +215,17 @@
 RetainedSetQuery.fieldNames.help = List of field names
 RetainedSetQuery.objects.help = Set of objects to include in the analysis.
 
+SimpleComparison.name = Simple Comparison
+SimpleComparison.category = Java Basics
+SimpleComparison.help = A comparison of the results of running a query on two snapshots.
+SimpleComparison.baseline.help = The baseline snapshot file.
+SimpleComparison.query.help = The query to run on each snapshot. Can include options.
+SimpleComparison.options.help = The options to pass to the comparison query.
+SimpleComparison.defaultoptions.help = The default options passed to the comparison query. \
+Can be overridden but the default ignores object addresses and array sizes in the comparison.
+SimpleComparison.retained.help = Whether to add approximate or precise retained sizes to the individual queries, \
+ready for the comparison.
+
 SoftReferenceStatQuery.name = Soft References Statistics
 SoftReferenceStatQuery.category = Java Basics/References
 SoftReferenceStatQuery.help = Statistics for Soft References. \
diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/Messages.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/Messages.java
index cd57429..f1ebccb 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/Messages.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/Messages.java
@@ -285,6 +285,13 @@
     public static String FindLeaksQuery_SearchingSingleObjects;

     public static String FindLeaksQuery_TooManySuspects;

 

+    public static String FindLeaksQuery2_Column_AccPointRetainedHeap;

+    public static String FindLeaksQuery2_Column_SuspectRetainedHeap;

+

+    public static String FindLeaksQuery2_ComparedDominatorTrees;

+

+    public static String FindLeaksQuery2_Leaks;

+

     public static String FindStringsQuery_SearchingStrings;

 

     public static String GCRootInfo_BusyMonitor;

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties
index cb3e6e2..6edb9e9 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties
@@ -258,6 +258,10 @@
 FindLeaksQuery_SearchingGroupsOfObjects=Searching suspicious groups of objects ...

 FindLeaksQuery_SearchingSingleObjects=Searching suspicious single objects ...

 FindLeaksQuery_TooManySuspects=Too many suspect instances ({0}). Will use {1} random from them to search for common path.

+FindLeaksQuery2_ComparedDominatorTrees=Compared Dominator Trees

+FindLeaksQuery2_Leaks=Leaks

+FindLeaksQuery2_Column_AccPointRetainedHeap=Acc. Point Retained Heap Delta

+FindLeaksQuery2_Column_SuspectRetainedHeap=Suspect's Retained Heap Delta

 FindStringsQuery_SearchingStrings=Searching Strings...

 GCRootInfo_BusyMonitor=Busy Monitor

 GCRootInfo_Finalizable=Finalizable

@@ -371,9 +375,9 @@
 LeakHunterQuery_Hint=Hint {0}

 LeakHunterQuery_Keywords=Keywords

 LeakHunterQuery_LeakHunter=Leak Hunter

-LeakHunterQuery_Msg_AccumulatedBy=The memory is accumulated in classloader/component <b>&quot;{0}&quot;</b>.

-LeakHunterQuery_Msg_AccumulatedByInstance=The memory is accumulated in one instance of <b>&quot;{0}&quot;</b> loaded by <b>&quot;{1}&quot;</b>.

-LeakHunterQuery_Msg_AccumulatedByLoadedBy=The memory is accumulated in class <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>.

+LeakHunterQuery_Msg_AccumulatedBy=The memory is accumulated in classloader/component <b>&quot;{0}&quot;</b> which occupies <b>{1}</b> bytes.

+LeakHunterQuery_Msg_AccumulatedByInstance=The memory is accumulated in one instance of <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>, which occupies <b>{2}</b> bytes.

+LeakHunterQuery_Msg_AccumulatedByLoadedBy=The memory is accumulated in class <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>, which occupies <b>{2}</b> bytes.

 LeakHunterQuery_Msg_Bytes={0} bytes. 

 LeakHunterQuery_Msg_Class=The class <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>, occupies <b>{2}</b> bytes. 

 LeakHunterQuery_Msg_ClassLoader=The classloader/component <b>&quot;{0}&quot;</b> occupies <b>{1}</b> bytes. 

@@ -382,9 +386,9 @@
 LeakHunterQuery_Msg_ReferencedBy=The instance is referenced by classloader/component. <b>&quot;{0}&quot;</b>. 

 LeakHunterQuery_Msg_ReferencedByClass=The instance is referenced by class <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>. 

 LeakHunterQuery_Msg_ReferencedByInstance=The instance is referenced by <b>{0}</b>&nbsp;, loaded by <b>&quot;{1}&quot;</b>. 

-LeakHunterQuery_Msg_ReferencedFromClass=These instances are referenced from the class <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>

-LeakHunterQuery_Msg_ReferencedFromClassLoader=These instances are referenced from classloader/component <b>&quot;{0}&quot;</b>

-LeakHunterQuery_Msg_ReferencedFromInstance=These instances are referenced from one instance of <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>

+LeakHunterQuery_Msg_ReferencedFromClass=These instances are referenced from the class <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>, which occupies <b>{2}</b> bytes.

+LeakHunterQuery_Msg_ReferencedFromClassLoader=These instances are referenced from classloader/component <b>&quot;{0}&quot;</b> which occupies <b>{1}</b> bytes.

+LeakHunterQuery_Msg_ReferencedFromInstance=These instances are referenced from one instance of <b>&quot;{0}&quot;</b>, loaded by <b>&quot;{1}&quot;</b>, which occupies <b>{2}</b> bytes.

 LeakHunterQuery_Msg_SuspectsRelated=The problem suspects {0} and {1} may be related, because the reference chains to them have a common beginning.

 LeakHunterQuery_Msg_Thread=The thread <b>{0}</b> keeps local variables with total size <b>{1}</b> bytes.

 LeakHunterQuery_NothingFound=No leak suspect was found

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/CompareTablesQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/CompareTablesQuery.java
index e687d33..8173cf7 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/CompareTablesQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/CompareTablesQuery.java
@@ -62,6 +62,8 @@
 import org.eclipse.mat.snapshot.ISnapshot;

 import org.eclipse.mat.snapshot.OQL;

 import org.eclipse.mat.snapshot.model.IObject;

+import org.eclipse.mat.snapshot.model.IObjectArray;

+import org.eclipse.mat.snapshot.model.NamedReference;

 import org.eclipse.mat.snapshot.query.Icons;

 import org.eclipse.mat.util.IProgressListener;

 import org.eclipse.mat.util.MessageUtil;

@@ -111,6 +113,9 @@
     @Argument(isMandatory = false)

     public boolean suffix;

 

+    // @Argument(isMandatory = false)

+    public boolean addrefs = true;

+

     @Argument(isMandatory = false, flag = "x")

     public String[] extraReferences;

 

@@ -247,7 +252,7 @@
         IObject obj;

         try

         {

-            obj = snapshots[table].getObject(ctx.getObjectId());

+            obj = snapshots[table].getObject(objId);

         }

         catch (SnapshotException e)

         {

@@ -319,6 +324,92 @@
     }

 

     /**

+     * Calculate extra prefix for the key.

+     * Look for the immediate dominator and see if any fields point to this.

+     * Also do this for small object arrays.

+     * @param table index

+     * @param row

+     * @return a String or null

+     */

+    String extraPrefix(int table, Object row)

+    {

+        // Only generate prefixes for small dominator arrays

+        final int SMALL_ARRAY_SIZE = 512;

+        final String noextra = null;

+        if (!addrefs)

+            return noextra;

+        if (snapshots[table] == null)

+            return noextra;

+        IContextObject ctx = tables[table].getContext(row);

+        if (ctx == null)

+            return noextra;

+        int objId = ctx.getObjectId();

+        if (objId == -1)

+            return noextra;

+        IObject obj;

+        try

+        {

+            obj = snapshots[table].getObject(objId);

+        }

+        catch (SnapshotException e)

+        {

+            return noextra;

+        }

+        StringBuilder sb = new StringBuilder();

+        try

+        {

+            int immdom = snapshots[table].getImmediateDominatorId(obj.getObjectId());

+            if (immdom >= 0)

+            {

+                if (!snapshots[table].isArray(immdom))

+                {

+                    IObject immobj = snapshots[table].getObject(immdom);

+                    for (NamedReference ref : immobj.getOutboundReferences())

+                    {

+                        if (ref.getObjectId() == objId)

+                        {

+                            if (sb.length() > 0)

+                                sb.append(',');

+                            sb.append(ref.getName());

+                        }

+                    }

+                }

+                else

+                {

+                    if (snapshots[table].getClassOf(immdom).getObjectId() == obj.getObjectId())

+                        sb.append("<class>"); //$NON-NLS-1$

+                    // Big arrays could be expensive to read

+                    if (snapshots[table].getHeapSize(immdom) < SMALL_ARRAY_SIZE)

+                    {

+                        IObject immobj = snapshots[table].getObject(immdom);

+                        // Don't get named references for object array - expensive

+                        if (immobj instanceof IObjectArray)

+                        {

+                            IObjectArray immarr = (IObjectArray)immobj;

+                            long arr[] = immarr.getReferenceArray();

+                            for (int j = 0; j < arr.length; ++j)

+                            {

+                                if (arr[j] == obj.getObjectAddress())

+                                {

+                                    if (sb.length() > 0)

+                                        sb.append(',');

+                                    sb.append('[').append(j).append(']');

+                                }

+                            }

+                        }

+                    }

+                }

+            }

+        }

+        catch (SnapshotException e1)

+        {

+        }

+        if (sb.length() > 0)

+            return sb.toString();

+        return noextra;

+    }

+

+    /**

      * Collect the extra type+field references.

      */

     void collectExtraRefs() throws IOException

@@ -481,12 +572,12 @@
                     rows = new Object[tables.length];

                     map.put(key, rows);

                 }

-                int ii = i;

+                int ii = 0;

                 if (lastcache.containsKey(key))

                 {

                     ii = lastcache.get(key);

                 }

-                while (rows[ii] != null)

+                while (rows[ii + i] != null)

                 {

                     /*

                      * Normally:

@@ -500,17 +591,21 @@
                     if (ii >= rows.length)

                     {

                         sortwork -= rows.length * rows.length;

-                        // Add a placeholder so that a row goes here

-                        map.put(new PlaceHolder(key, rows.length), new Object[0]);

                         // Grow the row array

-                        rows = Arrays.copyOf(rows, rows.length + (rows.length / tables.length / 2 + 1) * tables.length);

+                        int spare = rows.length / tables.length / 2;

+                        rows = Arrays.copyOf(rows, rows.length + (spare + 1) * tables.length);

                         map.put(key, rows);

                         sortwork += rows.length * rows.length;

                     }

+                }

+                rows[ii + i] = row;

+                if (ii > 0)

+                {

                     // With many duplicates it can take a long time to find a free slot, so cache the last used

                     lastcache.put(key, ii);

+                    // Add a placeholder so that a row goes here

+                    map.put(new PlaceHolder(key, ii), new Object[0]);

                 }

-                rows[ii] = row;

                 if (listener.isCanceled())

                     throw new IProgressListener.OperationCanceledException();

                 listener.worked(1);

@@ -592,25 +687,39 @@
         {

             Column c = tables[i].getColumns()[keyColumn - 1];

             IDecorator id = c.getDecorator();

-            if (id != null)

+            String pfx;

+            if (prefix)

             {

-                String pfx = prefix ? id.prefix(row) : null;

-                String sfx = suffix ? id.suffix(row) : null;

-                if (pfx != null)

+                if (id != null)

                 {

-                    pfx = pfx.replaceAll(mask.pattern(), replace == null ? "" : replace); //$NON-NLS-1$

-                    if (pfx.length() == 0)

-                        pfx = null;

+                    pfx = id.prefix(row);

+                    if (pfx == null)

+                        pfx = extraPrefix(i, row);

                 }

-                if (sfx != null)

+                else

                 {

-                    sfx = mask.matcher(sfx).replaceAll(replace == null ? "" : replace); //$NON-NLS-1$

-                    if (sfx.length() == 0)

-                        sfx = null;

+                    pfx = extraPrefix(i, row);

                 }

-                if (pfx != null || sfx != null)

-                    key = new ComparedRow(pfx, key, sfx, null);

             }

+            else

+            {

+                pfx = null;

+            }

+            String sfx = suffix && id != null ? id.suffix(row) : null;

+            if (mask != null && pfx != null)

+            {

+                pfx = mask.matcher(pfx).replaceAll(replace == null ? "" : replace); //$NON-NLS-1$

+                if (pfx.length() == 0)

+                    pfx = null;

+            }

+            if (mask != null && sfx != null)

+            {

+                sfx = mask.matcher(sfx).replaceAll(replace == null ? "" : replace); //$NON-NLS-1$

+                if (sfx.length() == 0)

+                    sfx = null;

+            }

+            if (pfx != null || sfx != null)

+                key = new ComparedRow(pfx, key, sfx, null);

         }

         return key;

     }

@@ -1945,6 +2054,9 @@
                             return DeltaEncoding.GE;

                         else if (fv.startsWith(Messages.CompareTablesQuery_GE))

                             return DeltaEncoding.GE;

+                        else if (fv.startsWith("<= ")) //$NON-NLS-1$

+                            // E.g. Quantize_LessEq_Prefix

+                            return DeltaEncoding.LE;

                         else if (fv.startsWith(Messages.CompareTablesQuery_LE))

                             return DeltaEncoding.LE;

                         else if (fv.startsWith(Messages.CompareTablesQuery_APPROX))

@@ -2026,6 +2138,13 @@
                     return encodeResult(ret, returnBytes, approxValue, approxPreviousValue, columnIdx);

                 }

             }

+            else

+            {

+                if (ratio)

+                    return null;

+                if (previousTableValue == null || !previousTableValue.equals(value))

+                    return value;

+            }

             return null;

         }

 

diff --git a/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/SnapshotImpl.java b/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/SnapshotImpl.java
index 1a178a5..fd7fc4d 100644
--- a/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/SnapshotImpl.java
+++ b/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/SnapshotImpl.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2018 SAP AG, IBM Corporation and others.

+ * Copyright (c) 2008, 2020 SAP AG, IBM Corporation 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

@@ -17,10 +17,10 @@
 import java.io.FileInputStream;

 import java.io.FileOutputStream;

 import java.io.IOException;

-import java.io.InvalidClassException;
+import java.io.InvalidClassException;

 import java.io.ObjectInputStream;

 import java.io.ObjectOutputStream;

-import java.io.ObjectStreamClass;
+import java.io.ObjectStreamClass;

 import java.util.ArrayList;

 import java.util.Arrays;

 import java.util.Collection;

@@ -1600,6 +1600,17 @@
             throw new RuntimeException(error);

     }

 

+    /**

+     * Tidy up and close files so indices could be deleted if required.

+     * Normally is disposed, but secondary snapshots are not.

+     * Shouldn't really be needed as files should have finalizers,

+     * but deleting indices does not work.

+     */

+    public void finalize()

+    {

+        dispose();

+    }

+

     // //////////////////////////////////////////////////////////////

     // internal stuff

     // //////////////////////////////////////////////////////////////

diff --git a/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/oql/OQLQueryImpl.java b/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/oql/OQLQueryImpl.java
index a88d73b..981c32a 100644
--- a/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/oql/OQLQueryImpl.java
+++ b/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/oql/OQLQueryImpl.java
@@ -253,8 +253,13 @@
         public RowMap get(int index)

         {

             // Always return a map not a single item for consistency.

-            //if (false && getColumns().length == 1)

-            //    return getColumnValue(getRow(index), 0);

+            // Except if the alias is "" as this used for CompareTablesQuery.

+            if (getColumns().length == 1 && getColumns()[0].getLabel().length() == 0)

+            {

+                Object ret = getColumnValue(getRow(index), 0);

+                if (ret instanceof RowMap)

+                    return (RowMap)ret;

+            }

             // Delay resolving columns until needed

             return new RowMap(this, index);

         }

diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueriesTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueriesTest.java
index d3b1fe3..ee278e6 100644
--- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueriesTest.java
+++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueriesTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2011, 2019 IBM Corporation.

+ * Copyright (c) 2011, 2020 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

@@ -13,8 +13,10 @@
 

 import static org.hamcrest.core.AllOf.allOf;

 import static org.hamcrest.core.IsEqual.equalTo;

+import static org.hamcrest.core.IsInstanceOf.instanceOf;

 import static org.hamcrest.core.IsNot.not;

 import static org.hamcrest.core.StringContains.containsString;

+import static org.hamcrest.core.StringStartsWith.startsWith;

 import static org.hamcrest.number.OrderingComparison.greaterThan;

 import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;

 import static org.hamcrest.number.OrderingComparison.lessThan;

@@ -45,6 +47,12 @@
 import org.eclipse.mat.query.refined.RefinedResultBuilder;

 import org.eclipse.mat.query.refined.RefinedTable;

 import org.eclipse.mat.query.refined.RefinedTree;

+import org.eclipse.mat.query.results.CompositeResult;

+import org.eclipse.mat.query.results.DisplayFileResult;

+import org.eclipse.mat.query.results.TextResult;

+import org.eclipse.mat.report.QuerySpec;

+import org.eclipse.mat.report.SectionSpec;

+import org.eclipse.mat.report.Spec;

 import org.eclipse.mat.snapshot.ClassHistogramRecord;

 import org.eclipse.mat.snapshot.Histogram;

 import org.eclipse.mat.snapshot.ISnapshot;

@@ -1174,5 +1182,162 @@
         SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.api:compare -params snapshot2="+snapshot2.getSnapshotInfo().getPath(), snapshot);

         IResult t = query.execute(new VoidProgressListener());

         assertNotNull(t);

+        if (t instanceof DisplayFileResult)

+        {

+            // If an error occurred the file might be short

+            assertThat(((DisplayFileResult) t).getFile().length(), greaterThan(2000L));

+        }

+    }

+

+    /**

+     * Test running the overview compare snapshots report defined in a plugin, with parameters.

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testOverview2Report() throws SnapshotException, IOException

+    {

+        ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.SUN_JDK6_18_64BIT, false); // Do not dispose this as shared

+        SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.api:overview2 -params baseline="+snapshot.getSnapshotInfo().getPath(), snapshot2);

+        IResult t = query.execute(new VoidProgressListener());

+        assertNotNull(t);

+        if (t instanceof DisplayFileResult)

+        {

+            // If an error occurred the file might be short

+            assertThat(((DisplayFileResult) t).getFile().length(), greaterThan(2000L));

+        }

+    }

+

+    /**

+     * Test running the snapshot compare snapshots report defined in a plugin, with parameters.

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testSuspects2Report() throws SnapshotException, IOException

+    {

+        ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.SUN_JDK6_18_64BIT, false); // Do not dispose this as shared

+        SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.api:suspects2 -params baseline="+snapshot.getSnapshotInfo().getPath(), snapshot2);

+        IResult t = query.execute(new VoidProgressListener());

+        assertNotNull(t);

+    }

+

+    /**

+     * Comparison of two JDK6 dumps

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testLeakHunter2ReportJDK6() throws SnapshotException, IOException

+    {

+        ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.SUN_JDK6_18_64BIT, false); // Do not dispose this as shared

+        testLeakHunter2Report(snapshot, snapshot2, 4, 1, 12);

+    }

+

+    /**

+     * Comparison of dump with itself - should not be a leak

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testLeakHunter2ReportJDK6none() throws SnapshotException, IOException

+    {

+        testLeakHunter2Report(snapshot, snapshot, 0, 0, 0);

+    }

+

+    /**

+     * Comparison of two JDK11 dumps

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testLeakHunter2ReportIBM8_7() throws SnapshotException, IOException

+    {

+        ISnapshot snapshot1 = TestSnapshots.getSnapshot(TestSnapshots.IBM_JDK7_64BIT_SYSTEM, false); // Do not dispose this as shared

+        ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.IBM_JDK8_64BIT_SYSTEM, false); // Do not dispose this as shared

+        testLeakHunter2Report(snapshot1, snapshot2, 6, 1, 18);

+    }

+

+    /**

+     * Comparison of two JDK11 dumps

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testLeakHunter2ReportJDK11() throws SnapshotException, IOException

+    {

+        ISnapshot snapshot1 = TestSnapshots.getSnapshot(TestSnapshots.ADOPTOPENJDK_HOTSPOT_JDK11_0_4_11_64BIT, false); // Do not dispose this as shared

+        ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.OPENJDK_JDK11_04_64BIT, false); // Do not dispose this as shared

+        testLeakHunter2Report(snapshot1, snapshot2, 9, 1, 22);

+    }

+

+    /**

+     * Reverse comparison - should not be a leak

+     * @throws SnapshotException

+     * @throws IOException

+     */

+    @Test

+    public void testLeakHunter2ReportJDK11none() throws SnapshotException, IOException

+    {

+        ISnapshot snapshot1 = TestSnapshots.getSnapshot(TestSnapshots.ADOPTOPENJDK_HOTSPOT_JDK11_0_4_11_64BIT, false); // Do not dispose this as shared

+        ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.OPENJDK_JDK11_04_64BIT, false); // Do not dispose this as shared

+        testLeakHunter2Report(snapshot2, snapshot1, 0, 0, 0);

+    }

+

+    public void testLeakHunter2Report(ISnapshot snapshot1, ISnapshot snapshot2, int expectedProbs, int expectedDomTree, int expectedSubCommands) throws SnapshotException, IOException

+    {

+        SnapshotQuery query = SnapshotQuery.parse("leakhunter2 -baseline "+snapshot1.getSnapshotInfo().getPath(), snapshot2);

+        IResult t = query.execute(new VoidProgressListener());

+        assertNotNull(t);

+        if (expectedProbs == 0)

+        {

+            assertThat(t, instanceOf(TextResult.class));

+            return;

+        }

+        // Should be 4 suspects

+        SectionSpec ss = (SectionSpec)t;

+        int probs = 0;

+        int domtree = 0;

+        int subcommands = 0;

+        for (Spec s : ss.getChildren())

+        {

+            if (s.getName().contains("Problem"))

+                ++probs;

+            if (s instanceof QuerySpec)

+            {

+                // This is the find_leaks2 query

+                QuerySpec qs = (QuerySpec)s;

+                if (qs.getName().equals("Compared Dominator Trees"))

+                {

+                    assertThat(qs.getCommand(), startsWith("find_leaks2 "));

+                    SnapshotQuery query2 = SnapshotQuery.parse(qs.getCommand(), snapshot2);

+                    IResult t2 = query2.execute(new VoidProgressListener());

+                    assertNotNull(t2);

+                    ++domtree;

+                }

+                IResult t3 = qs.getResult();

+                if (t3 instanceof CompositeResult)

+                {

+                    CompositeResult cr = (CompositeResult)t3;

+                    for (CompositeResult.Entry e : cr.getResultEntries())

+                    {

+                        if (e.getResult() instanceof QuerySpec)

+                        {

+                            QuerySpec qs2 = (QuerySpec)e.getResult();

+                            if (qs2.getCommand() != null)

+                            {

+                                SnapshotQuery query2 = SnapshotQuery.parse(qs2.getCommand(), snapshot2);

+                                IResult t2 = query2.execute(new VoidProgressListener());

+                                assertNotNull(t2);

+                                ++subcommands;

+                            }

+                        }

+                    }

+                }

+            }

+        }

+        assertThat("Expect problems section", probs, equalTo(expectedProbs));

+        assertThat("Expected dominator tree section", domtree, equalTo(expectedDomTree));

+        assertThat("Expected subqueries with commands", subcommands, equalTo(expectedSubCommands));

     }

 }

diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueryLookupTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueryLookupTest.java
index 8705415..f7e3d8b 100644
--- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueryLookupTest.java
+++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/QueryLookupTest.java
@@ -482,7 +482,7 @@
 

     public void testCompareDiffRatioPreviousRetained(String query) throws SnapshotException, ParseException

     {

-        boolean verbose = true;

+        boolean verbose = false;

         String queryId = "comparetablesquery";

         ISnapshot snapshot1 = TestSnapshots.getSnapshot(TestSnapshots.SUN_JDK6_18_64BIT, false);

         ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.SUN_JDK6_18_32BIT, false);