347648: Retained size column is not compared

Use RefinedStructuredResult, and have approximation
symbols.

Change-Id: Ib772a245a4087aa1c0427bcbde31f2f193350a24
Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=347648
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 347df74..cd57429 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
@@ -96,6 +96,8 @@
     public static String Column_RetainedHeap;

     public static String Column_ShallowHeap;

 

+    public static String CompareTablesQuery_APPROX;

+

     public static String CompareTablesQuery_Comparing;

     public static String CompareTablesQuery_Initial;

 

@@ -109,11 +111,15 @@
     public static String CompareTablesQuery_DifferenceLast;

     public static String CompareTablesQuery_DifferenceMiddle;

     public static String CompareTablesQuery_DifferenceOf2;

+

+    public static String CompareTablesQuery_GE;

     public static String CompareTablesQuery_IntersectionFirst;

     public static String CompareTablesQuery_IntersectionLast;

     public static String CompareTablesQuery_IntersectionMiddle;

     public static String CompareTablesQuery_IntersectionOf2;

 

+    public static String CompareTablesQuery_LE;

+

     public static String CompareTablesQuery_MissingKeyColumn;

     public static String CompareTablesQuery_Table;

     public static String CompareTablesQuery_SymmetricDifferenceFirst;

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 62aa867..cb3e6e2 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
@@ -75,6 +75,7 @@
 Column_Percentage=Percentage

 Column_RetainedHeap=Retained Heap

 Column_ShallowHeap=Shallow Heap

+CompareTablesQuery_APPROX=\u2248\u0020

 CompareTablesQuery_Comparing=Comparing

 CompareTablesQuery_Initial=Initial comparison

 CompareTablesQuery_InitialComparisonForTable=Initial comparison for table {0}

@@ -105,10 +106,12 @@
 CompareTablesQuery_DifferenceLast={0} and Table {1}

 CompareTablesQuery_DifferenceMiddle={0}, Table {1}

 CompareTablesQuery_DifferenceOf2=Table {0} without Table {1}

+CompareTablesQuery_GE=\u2265\u0020

 CompareTablesQuery_IntersectionFirst=Intersection of Table {0}, Table {1}

 CompareTablesQuery_IntersectionLast={0} and Table {1}

 CompareTablesQuery_IntersectionMiddle={0}, Table {1}

 CompareTablesQuery_IntersectionOf2=Intersection of Table {0} and Table {1}

+CompareTablesQuery_LE=\u2264\u0020

 CompareTablesQuery_MissingKeyColumn=Missing key column {0} for table \#{1}

 CompareTablesQuery_Table=Table {0}

 CompareTablesQuery_SymmetricDifferenceFirst=Symmetric difference of Table {0}, Table {1}

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 216d8bd..e687d33 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
@@ -16,10 +16,14 @@
 import java.io.IOException;

 import java.net.URISyntaxException;

 import java.net.URL;

+import java.text.FieldPosition;

 import java.text.Format;

+import java.text.ParsePosition;

 import java.util.ArrayList;

 import java.util.Arrays;

 import java.util.Collection;

+import java.util.Comparator;

+import java.util.HashMap;

 import java.util.HashSet;

 import java.util.LinkedHashMap;

 import java.util.LinkedList;

@@ -54,6 +58,7 @@
 import org.eclipse.mat.query.annotations.Icon;

 import org.eclipse.mat.query.annotations.Menu;

 import org.eclipse.mat.query.annotations.Menu.Entry;

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

 import org.eclipse.mat.snapshot.ISnapshot;

 import org.eclipse.mat.snapshot.OQL;

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

@@ -71,6 +76,8 @@
 @HelpUrl("/org.eclipse.mat.ui.help/tasks/comparingdata.html")

 @Menu({ @Entry(options = "-setop ALL")

 ,@Entry(options = "-mode DIFF_TO_PREVIOUS -prefix -mask \"\\s@ 0x[0-9a-f]+|^\\[[0-9]+\\]$\" -x java.util.HashMap$Node:key java.util.Hashtable$Entry:key java.util.WeakHashMap$Entry:referent java.util.concurrent.ConcurrentHashMap$Node:key")

+,@Entry(options = "-mode DIFF_TO_PREVIOUS -prefix -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")

+,@Entry(options = "-mode DIFF_TO_PREVIOUS -prefix -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 -setop ALL")

 })

 public class CompareTablesQuery implements IQuery

 {

@@ -175,10 +182,30 @@
         {

             if (i == keyColumn-1)

                 continue;

+            // Check for duplicate column names

+            int prevDuplicateCol = -1;

+            for (int k = 0; k < i - 1; ++k)

+            {

+                if (baseColumns[i].getLabel().equals(baseColumns[k].getLabel()))

+                    prevDuplicateCol = k > keyColumn - 1 ? k - 1 : k; // Adjust for key column

+            }

             int[] indexes = new int[tables.length];

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

             {

-                indexes[j] = getColumnIndex(baseColumns[i].getLabel(), tables[j]);

+                if (prevDuplicateCol >= 0)

+                {

+                    // Start search after previous found column

+                    int pc = attributes.get(prevDuplicateCol).getColumnIndexes()[j];

+                    int ci = getColumnIndex(baseColumns[i].getLabel(), tables[j], pc + 1);

+                    // Not found, so duplicate the last column

+                    if (ci == -1)

+                        ci = pc;

+                    indexes[j] = ci;

+                }

+                else

+                {

+                    indexes[j] = getColumnIndex(baseColumns[i].getLabel(), tables[j], 0);

+                }

             }

             attributes.add(new ComparedColumn(baseColumns[i], indexes, true));

         }

@@ -188,10 +215,10 @@
                         : new ComparisonResultTable(mergeKeys(null, listener), key, attributes, mode, setOp);

     }

 

-    private int getColumnIndex(String name, IStructuredResult table)

+    private int getColumnIndex(String name, IStructuredResult table, int colstart)

     {

         Column[] columns = table.getColumns();

-        for (int i = 0; i < columns.length; i++)

+        for (int i = colstart; i < columns.length; i++)

         {

             if (columns[i].getLabel().equals(name)) return i;

         }

@@ -350,7 +377,7 @@
     /**

      * Hold a place for a row when the key is a duplicate.

      */

-    class PlaceHolder

+    static class PlaceHolder

     {

         Object key;

         int pos;

@@ -429,6 +456,7 @@
             {

                 treeRows = null;

             }

+            Map<Object, Integer>lastcache = new HashMap<Object,Integer>();

             for (int j = 0; j < size; j++)

             {

 

@@ -454,6 +482,10 @@
                     map.put(key, rows);

                 }

                 int ii = i;

+                if (lastcache.containsKey(key))

+                {

+                    ii = lastcache.get(key);

+                }

                 while (rows[ii] != null)

                 {

                     /*

@@ -471,10 +503,12 @@
                         // 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 + tables.length);

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

                         map.put(key, rows);

                         sortwork += rows.length * rows.length;

                     }

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

+                    lastcache.put(key, ii);

                 }

                 rows[ii] = row;

                 if (listener.isCanceled())

@@ -547,7 +581,7 @@
         if (mask != null && key != null)

         {

             String keystr = key.toString();

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

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

             if (!keystr.equals(keystr2))

             {

                 key = keystr2;

@@ -570,7 +604,7 @@
                 }

                 if (sfx != null)

                 {

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

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

                     if (sfx.length() == 0)

                         sfx = null;

                 }

@@ -700,7 +734,7 @@
      * 3 3

      * 4

      * </pre>

-     * 

+     *

      * not

      *

      * <pre>

@@ -856,7 +890,7 @@
         }

     }

 

-    public class ComparedColumn

+    public static class ComparedColumn

     {

         Column description;

         int[] columnIndexes;

@@ -1028,6 +1062,30 @@
         }

     }

 

+    /**

+     * Types of delta retained size.

+     */

+    enum DeltaEncoding {

+        /**

+         * The size delta is exact.

+         */

+        EXACT,

+        /**

+         * The size delta is at least this much.

+         */

+        GE,

+        /**

+         * The size delta is not more than this.

+         */

+        LE,

+        /**

+         * The size delta is uncertain, but this is an

+         * estimate. For example the difference between

+         * two sizes given as greater than or equal to.

+         */

+        APPROX

+    }

+

     public class TableComparisonResult implements IStructuredResult, IIconProvider

     {

         private Column key;

@@ -1086,13 +1144,13 @@
                 case ABSOLUTE:

                     return getAbsoluteValue(cr, comparedColumnIdx, tableIdx);

                 case DIFF_TO_FIRST:

-                    return getDiffToFirst(cr, comparedColumnIdx, tableIdx, false);

+                    return getDiffToPrevious(cr, columnIndex, comparedColumnIdx, tableIdx, 0, false);

                 case DIFF_TO_PREVIOUS:

-                    return getDiffToPrevious(cr, comparedColumnIdx, tableIdx, false);

+                    return getDiffToPrevious(cr, columnIndex, comparedColumnIdx, tableIdx, tableIdx - 1, false);

                 case DIFF_RATIO_TO_FIRST:

-                    return getDiffToFirst(cr, comparedColumnIdx, (tableIdx + 1) / 2, tableIdx % 2 == 0);

+                    return getDiffToPrevious(cr, columnIndex, comparedColumnIdx, (tableIdx + 1) / 2, 0, tableIdx % 2 == 0);

                 case DIFF_RATIO_TO_PREVIOUS:

-                    return getDiffToPrevious(cr, comparedColumnIdx, (tableIdx + 1) / 2, tableIdx % 2 == 0);

+                    return getDiffToPrevious(cr, columnIndex, comparedColumnIdx, (tableIdx + 1) / 2, (tableIdx + 1) / 2 - 1, tableIdx % 2 == 0);

 

                 default:

                     break;

@@ -1769,51 +1827,148 @@
             }

         }

 

-        private Object getDiffToFirst(ComparedRow cr, int comparedColumnIdx, int tableIdx, boolean ratio)

+        /**

+         * Convert a encoded value from a cell.

+         * The value can be encoded, and needs to be

+         * decoded before arithmetic or filtering.

+         * The formatter can handle the encoded

+         * value.

+         * @param value

+         * @param tableIdx

+         * @param comparedColumnIdx

+         * @return the converted value, of the same type

+         * as the original.

+         */

+        private Object valueConvert(Object value, int tableIdx, int comparedColumnIdx)

         {

-            Object tableRow = cr.getRows()[tableIdx];

-            if (tableRow == null) return null;

-

+            // Optimization, presume no converter will change null

+            if (value == null)

+                return null;

             int tableColumnIdx = displayedColumns.get(comparedColumnIdx).getColumnIndexes()[tableIdx];

-            if (tableColumnIdx == -1) return null;

-

-            Object value = tables[tableIdx].getColumnValue(tableRow, tableColumnIdx);

-            Object firstTableValue = getAbsoluteValue(cr, comparedColumnIdx, 0);

-

-            if (value == null && firstTableValue == null) return null;

-

-            if (value == null && (firstTableValue instanceof Number || firstTableValue instanceof Bytes)) return null;

-

-            if ((value instanceof Number || value instanceof Bytes) && firstTableValue == null)

+            if (tableColumnIdx == -1)

+                return value;

+            Filter.ValueConverter vc = (Filter.ValueConverter) tables[tableIdx].getColumns()[tableColumnIdx]

+                            .getData(Filter.ValueConverter.class);

+            if (vc != null)

             {

-                return ratio ? null : value;

-            }

-

-            boolean returnBytes = value instanceof Bytes && firstTableValue instanceof Bytes;

-            if (value instanceof Bytes)

-                value = Long.valueOf(((Bytes)value).getValue());

-

-            if (firstTableValue instanceof Bytes)

-                firstTableValue = Long.valueOf(((Bytes)firstTableValue).getValue());

-

-            if (value instanceof Number && firstTableValue instanceof Number)

-            {

-                Object ret = computeDiff((Number) firstTableValue, (Number) value);

-                if (ratio && ret instanceof Number)

+                if (value instanceof Bytes)

                 {

-                    return percentDivide((Number)ret, (Number)firstTableValue);

+                    double v0 = ((Bytes) value).getValue();

+                    double v = vc.convert(v0);

+                    if (v != v0)

+                        value = new Bytes(Double.valueOf(v).longValue());

                 }

-                else

+                else if (value instanceof Number)

                 {

-                    if (returnBytes)

-                        return new Bytes(((Number)ret).longValue());

-                    return ret;

+                    double v0 = ((Number) value).doubleValue();

+                    double v = vc.convert(v0);

+                    if (v != v0)

+                    {

+                        // Try to convert back to original type

+                        if (value instanceof Long)

+                        {

+                            long v1 = (long)v;

+                            if (v1 == v)

+                                value = v1;

+                            else

+                                value = v;

+                        }

+                        else if (value instanceof Integer)

+                        {

+                            int v1 = (int)v;

+                            if (v1 == v)

+                                value = v1;

+                            else

+                                value = v;

+                        }

+                        else if (value instanceof Short)

+                        {

+                            short v1 = (short)v;

+                            if (v1 == v)

+                                value = v1;

+                            else

+                                value = v;

+                        }

+                        else if (value instanceof Byte)

+                        {

+                            byte v1 = (byte)v;

+                            if (v1 == v)

+                                value = v1;

+                            else

+                                value = v;

+                        }

+                        else if (value instanceof Double)

+                        {

+                            double v1 = (double)v;

+                            if (v1 == v)

+                                value = v1;

+                            else

+                                value = v;

+                        }

+                        else if (value instanceof Float)

+                        {

+                            float v1 = (float)v;

+                            if (v1 == v)

+                                value = v1;

+                            else

+                                value = v;

+                        }

+                        else

+                            value = v;

+                    }

                 }

             }

-            return null;

+            return value;

         }

 

-        private Object getDiffToPrevious(ComparedRow cr, int comparedColumnIdx, int tableIdx, boolean ratio)

+        /**

+         * Did the converted value look like an approximate value?

+         * @param value original value

+         * @param value2 converted value

+         * @param tableIdx the table

+         * @param comparedColumnIdx the column index

+         * @return the approximation type

+         */

+        private DeltaEncoding approxValue(Object value, Object value2, int tableIdx, int comparedColumnIdx)

+        {

+            if (value != null && !value.equals(value2))

+            {

+                int tableColumnIdx = displayedColumns.get(comparedColumnIdx).getColumnIndexes()[tableIdx];

+                if (tableColumnIdx == -1)

+                    return DeltaEncoding.EXACT;

+                try

+                {

+                    String fv = tables[tableIdx].getColumns()[tableColumnIdx].getFormatter().format(value);

+                    if (fv != null)

+                    {

+                        if (fv.startsWith(Messages.RetainedSizeDerivedData_Approximate))

+                            return DeltaEncoding.GE;

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

+                            return DeltaEncoding.GE;

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

+                            return DeltaEncoding.LE;

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

+                            return DeltaEncoding.APPROX;

+                    }

+                }

+                catch (IllegalArgumentException e)

+                {}

+            }

+            return DeltaEncoding.EXACT;

+        }

+

+        /**

+         * Get the value for a row and column which is a difference to a previous table

+         * (either the first or the immediately previous table).

+         * @param cr the row of created compared table/tree

+         * @param columnIdx the column index of the created compared table/tree

+         * @param comparedColumnIdx the index for the set of columns of the same name which are compared

+         * @param tableIdx the table to read

+         * @param prevTableIdx the previous table to compare to

+         * @param ratio calculate a ration, not a difference

+         * @return

+         */

+        private Object getDiffToPrevious(ComparedRow cr, int columnIdx, int comparedColumnIdx, int tableIdx, int prevTableIdx, boolean ratio)

         {

             Object tableRow = cr.getRows()[tableIdx];

             if (tableRow == null) return null;

@@ -1822,7 +1977,13 @@
             if (tableColumnIdx == -1) return null;

 

             Object value = tables[tableIdx].getColumnValue(tableRow, tableColumnIdx);

-            Object previousTableValue = getAbsoluteValue(cr, comparedColumnIdx, tableIdx - 1);

+            Object previousTableValue = getAbsoluteValue(cr, comparedColumnIdx, prevTableIdx);

+            Object value2 = valueConvert(value, tableIdx, comparedColumnIdx);

+            DeltaEncoding approxValue = approxValue(value, value2, tableIdx, comparedColumnIdx);

+            value = value2;

+            value2 = valueConvert(previousTableValue, prevTableIdx, comparedColumnIdx);

+            DeltaEncoding approxPreviousValue = approxValue(previousTableValue, value2, prevTableIdx, comparedColumnIdx);

+            previousTableValue = value2;

 

             if (value == null && previousTableValue == null) return null;

 

@@ -1830,7 +1991,20 @@
 

             if ((value instanceof Number || value instanceof Bytes) && previousTableValue == null)

             {

-                return ratio ? null : value;

+                if (ratio)

+                    return null;

+                /*

+                 * Fix up encoding of single value

+                 * The source value has a difference formatter to the output so could need conversion.

+                 */

+                if (approxValue != DeltaEncoding.EXACT)

+                {

+                    if (value instanceof Bytes)

+                        return encodeResult(((Bytes)value).getValue(), true, approxValue, approxPreviousValue, columnIdx);

+                    else

+                        return encodeResult(value, false, approxValue, approxPreviousValue, columnIdx);

+                }

+                return value;

             }

 

             boolean returnBytes = value instanceof Bytes && previousTableValue instanceof Bytes;

@@ -1849,14 +2023,48 @@
                 }

                 else

                 {

-                    if (returnBytes)

-                        return new Bytes(((Number)ret).longValue());

-                    return ret;

+                    return encodeResult(ret, returnBytes, approxValue, approxPreviousValue, columnIdx);

                 }

             }

             return null;

         }

 

+        /**

+         * Encode the result for a delta retained size formatter.

+         * @param ret

+         * @param returnBytes

+         * @param approxValue

+         * @param approxPreviousValue

+         * @param columnIdx

+         * @return

+         */

+        private Object encodeResult(Object ret, boolean returnBytes, DeltaEncoding approxValue, DeltaEncoding approxPreviousValue,

+                        int columnIdx)

+        {

+            if (returnBytes || ret instanceof Long)

+            {

+                long val = ((Number)ret).longValue();

+                Format fmt = columns[columnIdx].getFormatter();

+                if (fmt instanceof DeltaRetainedBytesFormat)

+                {

+                    DeltaRetainedBytesFormat dfmt = (DeltaRetainedBytesFormat)fmt;

+                    if (approxValue == DeltaEncoding.EXACT && approxPreviousValue == DeltaEncoding.EXACT)

+                        ;

+                    else if ((approxValue == DeltaEncoding.GE || approxValue == DeltaEncoding.EXACT) && (approxPreviousValue == DeltaEncoding.EXACT || approxPreviousValue == DeltaEncoding.LE))

+                        val = dfmt.encodege(val);

+                    else if ((approxValue == DeltaEncoding.LE || approxValue == DeltaEncoding.EXACT) && (approxPreviousValue == DeltaEncoding.EXACT || approxPreviousValue == DeltaEncoding.GE))

+                        val = dfmt.encodele(val);

+                    else if (approxValue != DeltaEncoding.EXACT || approxPreviousValue != DeltaEncoding.EXACT)

+                        val = dfmt.encodeun(val);

+                }

+                if (returnBytes)

+                    return new Bytes(val);

+                else

+                    return val;

+            }

+            return ret;

+        }

+

         private Object computeDiff(Number o1, Number o2)

         {

             if (o1 instanceof Long && o2 instanceof Long) return (o2.longValue() - o1.longValue());

@@ -1955,6 +2163,224 @@
             }

         }

 

+        /**

+         * A class to format the difference between two retained sizes.

+         * Similar to {@link org.eclipse.mat.snapshot.query.RetainedSizeDerivedData.RetainedSizeFormat}.

+         * Sorting should use {@link Filter.ValueConverter} so that a dedicated comparator is

+         * not required. See {@link org.eclipse.mat.query.refined.RefinedStructuredResult.NaturalComparator}.

+         */

+        private class DeltaRetainedBytesFormat extends BytesFormat

+        {

+            /**

+             *

+             */

+            private static final long serialVersionUID = 1L;

+            /*

+             * encode >=, <= for +ve -ve byte values

+             * convert long to double for Filter.ValueConverter

+             * > 1,000,000,000,000,000 means >=

+             * <-1,000,000,000,000,000 means <=

+             * <-3,000,000,000,000,000 means ~

+             *

+             * e.g.

+             * ~=   : 39

+             * ~=3  : 33

+             * ~=0  : 30

+             * >=9  : 29

+             * >=3  : 23

+             * >=-3 : 17

+             * >=-10: 10

+             *

+             * <=9  : -11

+             * <=3  : -17

+             * <=-3 : -23

+             * <=-10: -30

+             *

+             * ~-3  : -33

+             * ~-9  : -39

+             */

+            /**

+             * Break point for special encoding.

+             * Chosen to be big, but to fit precisely in a double

+             * as well as a long.

+             */

+            private long SPECIAL = 1000000000000000L;

+            /**

+             * How much to adjust a value to move it into a different range.

+             */

+            private long SPECIAL2 = SPECIAL * 2;

+            /**

+             * Convert the encoded value to a normal value.

+             */

+            final Filter.ValueConverter converter = new Filter.ValueConverter()

+            {

+                public double convert(double source)

+                {

+                    if (source >= SPECIAL + SPECIAL2)

+                        return source - SPECIAL - SPECIAL2; // +ve approx

+                    else if (source >= SPECIAL)

+                        return source - SPECIAL2; // >=

+                    else if (source < -SPECIAL - SPECIAL2)

+                        return source + SPECIAL + SPECIAL2; // -ve approx

+                    else if (source < -SPECIAL)

+                        return source + SPECIAL2; // <=

+                    return source;

+                }

+            };

+

+            /**

+             * Encode a value as greater than or equal.

+             * @param l the raw value

+             * @return the encoded value

+             */

+            private long encodege(long l)

+            {

+                if (l >= SPECIAL)

+                    return SPECIAL - 1 + SPECIAL2; // saturate

+                else if (l < -SPECIAL)

+                    return encodeun(l); // can't be encoded as GE

+                else

+                    return l + SPECIAL2;

+            }

+

+            /**

+             * Encode a value as less than or equal.

+             * @param l the raw value

+             * @return the encoded value

+             */

+            private long encodele(long l)

+            {

+                if (l >= SPECIAL)

+                    return encodeun(l); // can't be coded as LE

+                else if (l < -SPECIAL)

+                    return -SPECIAL - SPECIAL2; // saturate

+                else

+                    return l - SPECIAL2;

+            }

+

+            /**

+             * Encode a value as inexact.

+             * @param l the raw value

+             * @return the encoded value

+             */

+            private long encodeun(long l)

+            {

+                if (l >= 0)

+                    if (l >= Long.MAX_VALUE - SPECIAL - SPECIAL2)

+                        return Long.MAX_VALUE; // saturate

+                    else

+                        return l + SPECIAL2 + SPECIAL;

+                else if (l <= Long.MIN_VALUE + SPECIAL + SPECIAL2)

+                    return Long.MIN_VALUE; // saturate

+                else

+                    return l - SPECIAL2 - SPECIAL;

+            }

+

+            /**

+             * Create a formatter for the difference between two retained sizes.

+             * @param encapsulatedNumberFormat

+             * @param encapsulatedDecimalFormat

+             */

+            public DeltaRetainedBytesFormat(Format encapsulatedNumberFormat, Format encapsulatedDecimalFormat)

+            {

+                super(encapsulatedNumberFormat, encapsulatedDecimalFormat);

+            }

+

+            @Override

+            public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos)

+            {

+                Number v;

+                if (obj instanceof Bytes)

+                    v = ((Bytes)obj).getValue();

+                else

+                    v = (Number) obj;

+

+

+                if (v.longValue() >= SPECIAL)

+                {

+                    if (v.longValue() >= SPECIAL + SPECIAL2)

+                    {

+                        String approx = Messages.CompareTablesQuery_APPROX;

+                        toAppendTo.append(approx);

+                        return super.format(new Bytes(v.longValue() - SPECIAL - SPECIAL2), toAppendTo, pos);

+                    }

+                    else

+                    {

+                        String approx = Messages.CompareTablesQuery_GE;

+                        toAppendTo.append(approx);

+                        return super.format(new Bytes(v.longValue() - SPECIAL2), toAppendTo, pos);

+                    }

+                }

+                else if (v.longValue() < -SPECIAL)

+                {

+                    if (v.longValue() < -SPECIAL - SPECIAL2)

+                    {

+                        String approx = Messages.CompareTablesQuery_APPROX;

+                        toAppendTo.append(approx);

+                        return super.format(new Bytes(v.longValue() + SPECIAL + SPECIAL2), toAppendTo, pos);

+                    }

+                    else

+                    {

+                        String approx = Messages.CompareTablesQuery_LE;

+                        toAppendTo.append(approx);

+                        return super.format(new Bytes(v.longValue() + SPECIAL2), toAppendTo, pos);

+                    }

+                }

+                else

+                {

+                    return super.format(new Bytes(v.longValue()), toAppendTo, pos);

+                }

+            }

+

+            @Override

+            public Object parseObject(String source, ParsePosition pos)

+            {

+                Object ret;

+                for (String match : new String[] {Messages.CompareTablesQuery_GE, Messages.CompareTablesQuery_LE, Messages.CompareTablesQuery_APPROX})

+                {

+                    if (source.regionMatches(pos.getIndex(), match, 0, match.length()))

+                    {

+                        int pi = pos.getIndex();

+                        pos.setIndex(pi + match.length());

+                        ret = super.parseObject(source, pos);

+                        if (ret != null)

+                        {

+                            long v;

+                            if (ret instanceof Bytes)

+                            {

+                                v = ((Bytes)ret).getValue();

+                                if (match == Messages.CompareTablesQuery_GE)

+                                    v = encodege(v);

+                                else if (match == Messages.CompareTablesQuery_LE)

+                                    v = encodele(v);

+                                else if (match == Messages.CompareTablesQuery_APPROX)

+                                    v = encodeun(v);

+                                return new Bytes(v);

+                            }

+                            else if (ret instanceof Number)

+                            {

+                                v = ((Number)ret).longValue();

+                                if (match == Messages.CompareTablesQuery_GE)

+                                    v = encodege(v);

+                                else if (match == Messages.CompareTablesQuery_LE)

+                                    v = encodele(v);

+                                else if (match == Messages.CompareTablesQuery_APPROX)

+                                    v = encodeun(v);

+                                return new Bytes(v);

+                            }

+                        }

+                        // >= in front of something else

+                        pos.setErrorIndex(pi + match.length());

+                        pos.setIndex(pi);

+                        ret = null;

+                        return ret;

+                    }

+                }

+                ret = super.parseObject(source, pos);

+                return ret;

+            }

+        }

+

         private void setFormatter()

         {

             int i = 1;

@@ -1977,6 +2403,7 @@
                 bcf = bcf2;

             }

             BytesFormat bfm = new BytesFormat(formatter, bcf);

+            DeltaRetainedBytesFormat drbfm = new DeltaRetainedBytesFormat(formatter, bcf);

 

             for (ComparedColumn comparedColumn : displayedColumns)

             {

@@ -1994,6 +2421,10 @@
                                             columns[i].getComparator());

                             columns[i].decorator(decorator);

                         }

+                        // Set the converter

+                        Object converter = c.getData(Filter.ValueConverter.class);

+                        if (converter != null || columns[i].getData(Filter.ValueConverter.class) != null)

+                            columns[i].setData(Filter.ValueConverter.class, converter);

                         if (c.getFormatter() instanceof DecimalFormat)

                         {

                             DecimalFormat fm = ((DecimalFormat) c.getFormatter().clone());

@@ -2004,7 +2435,15 @@
                         {

                             //BytesFormat fm = ((BytesFormat) c.getFormatter().clone());

                             // Force the sign - can't retrieve information from existing formatter

-                            columns[i].formatting(bfm);

+                            if (c.getFormatter().getClass() == BytesFormat.class)

+                            {

+                                columns[i].formatting(bfm);

+                            }

+                            else

+                            {

+                                columns[i].formatting(drbfm);

+                                columns[i].setData(Filter.ValueConverter.class, drbfm.converter);

+                            }

                         }

                         else

                         {

@@ -2023,6 +2462,10 @@
                             columns[i].decorator(decorator);

                         }

                         columns[i].formatting(c.getFormatter());

+                        // Set the converter

+                        Object converter = c.getData(Filter.ValueConverter.class);

+                        if (converter != null || columns[i].getData(Filter.ValueConverter.class) != null)

+                            columns[i].setData(Filter.ValueConverter.class, converter);

                     }

                     i++;

                     if ((mode == Mode.DIFF_RATIO_TO_FIRST || mode == Mode.DIFF_RATIO_TO_PREVIOUS) && j > 0)

@@ -2094,18 +2537,43 @@
                     {

                         String label;

                         final int prev = mode == Mode.DIFF_TO_PREVIOUS || mode == Mode.DIFF_RATIO_TO_PREVIOUS ? j - 1 : 0;

+                        Comparator<?>comparator;

                         if (j == 0 || mode == Mode.ABSOLUTE)

                         {

                             label = MessageUtil.format(Messages.CompareTablesQuery_ColumnAbsolute, c.getLabel(), j + 1);

+                            final Comparator<Object> cmp = (Comparator<Object>) c.getComparator();

+                            if (cmp != null)

+                            {

+                                final int tab = j;

+                                comparator = new Comparator<ComparedRow>(){

+                                    public int compare(ComparedRow o1, ComparedRow o2)

+                                    {

+                                        Object row1 = o1.rows[tab];

+                                        Object row2 = o2.rows[tab];

+                                        // Compare nulls - sort first

+                                        if (row1 == null)

+                                            return row2 == null ? 0 : -1;

+                                        else if (row2 == null)

+                                            return 1;

+                                        else

+                                            return cmp.compare(row1, row2);

+                                    }

+                                };

+                            }

+                            else

+                            {

+                                comparator = null;

+                            }

                         }

                         else

                         {

                             label = MessageUtil.format(Messages.CompareTablesQuery_ColumnDifference,

                                             c.getLabel(), j + 1,

                                             prev + 1);

+                            comparator = null;

                         }

                         result.add(new Column(label, c.getType(), c.getAlign(), c.getSortDirection(), c.getFormatter(),

-                                        null));

+                                        comparator));

                         // Pass through the decorator

                         if (c.getDecorator() != null)

                             result.get(result.size() - 1).decorator(new Decorator(c.getDecorator(), j));

@@ -2140,6 +2608,12 @@
             return ret;

         }

 

+        /**

+         * Add the derived operations from the source tables.

+         * No needed as using the RefinedResult versions of the tables

+         * already has them refined.

+         * @param answer

+         */

         void derivedops(ResultMetaData.Builder answer)

         {

             int found = 0;

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/MultiplePath2GCRootsQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/MultiplePath2GCRootsQuery.java
index b73662b..c370881 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/MultiplePath2GCRootsQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/MultiplePath2GCRootsQuery.java
@@ -208,7 +208,7 @@
 

         public boolean hasChildren(Object element)

         {

-            // to expensive to calculate

+            // too expensive to calculate

             return true;

         }

 

@@ -263,6 +263,39 @@
 

             return result;

         }

+

+        /**

+         * Needed as getElements returns new Nodes each time.

+         */

+        @Override

+        public int hashCode()

+        {

+            final int prime = 31;

+            int result = 1;

+            result = prime * result + level;

+            result = prime * result + objectId;

+            return result;

+        }

+

+        /**

+         * Needed as getElements returns new Nodes each time.

+         */

+        @Override

+        public boolean equals(Object obj)

+        {

+            if (this == obj)

+                return true;

+            if (obj == null)

+                return false;

+            if (getClass() != obj.getClass())

+                return false;

+            Node other = (Node) obj;

+            if (level != other.level)

+                return false;

+            if (objectId != other.objectId)

+                return false;

+            return true;

+        }

     }

 

     private static class ClassNode extends Node

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/annotations.properties b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/annotations.properties
index 7bd16b6..24870fc 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/annotations.properties
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/snapshot/inspections/annotations.properties
@@ -63,8 +63,10 @@
 merged paths from objects to garbage collection roots grouped by class, so all objects of the same class are shown as one
 
 CompareTablesQuery.name = Compare Tables and Trees
-CompareTablesQuery.menu.0.label = Compare tables and trees with all set operations
-CompareTablesQuery.menu.1.label = Compare tables and trees ignoring addresses when matching keys
+CompareTablesQuery.menu.0.label = 1|Compare tables and trees with all set operations
+CompareTablesQuery.menu.1.label = 2|Compare tables and trees ignoring addresses when matching keys
+CompareTablesQuery.menu.2.label = 3|Compare tables and trees ignoring addresses and array sizes
+CompareTablesQuery.menu.3.label = 4|Compare tables and trees ignoring addresses and array sizes with all set operations
 CompareTablesQuery.help = Compares two or more tables or trees. If the tables or trees are from the same snapshot \
 as the current snapshot then the context menu shows objects from those tables or trees. It also can perform \
 the following operations depending on the setop argument:\n\
@@ -73,6 +75,29 @@
 Table 1\u2296Table 2 The symmetric difference of that row from table 1 and table 2, so all the objects from that row in only one table (or an odd number of tables)\n\
 Table 1\u2216Table 2 The difference of that row between table 1 and table 2, so all the objects from that row in table 1 but not table 2\n\
 Table 2\u2216Table 1 The difference of that row between table 2 and table 1, so all the objects from that row in table 2 but not table 1
+CompareTablesQuery.menu.0.help = Compares two or more tables or trees with set operations. If the tables or trees are from the same snapshot \
+as the current snapshot then the context menu shows objects from those tables or trees. It also performs \
+the following operations via the context menu:\n\
+Table 1\u2229Table 2 The intersection of that row from table 1 and table 2, so only the objects from that row in both tables\n\
+Table 1\u222ATable 2 The intersection of that row from table 1 and table 2, so all the objects from that row in either table\n\
+Table 1\u2296Table 2 The symmetric difference of that row from table 1 and table 2, so all the objects from that row in only one table (or an odd number of tables)\n\
+Table 1\u2216Table 2 The difference of that row between table 1 and table 2, so all the objects from that row in table 1 but not table 2\n\
+Table 2\u2216Table 1 The difference of that row between table 2 and table 1, so all the objects from that row in table 2 but not table 1
+CompareTablesQuery.menu.1.help = Compares two or more tables or trees, matching different objects. Matches keys with different object addresses of the form \
+' @ 0x12345678' and array indices of the form '[1234]' in the prefix so is useful for comparisons of objects between different snapshots.
+CompareTablesQuery.menu.2.help = Compares two or more tables or trees, matching different objects and arrays. Matches keys with different object addresses of the form \
+' @ 0x12345678' and array indices of the form '[1234]' in the prefix so is useful for comparisons of objects between different snapshots. \
+Also matches keys with arrays of different sizes.
+CompareTablesQuery.menu.3.help = Compares two or more tables or trees, matching different objects and arrays and with set operations. Matches keys with different object addresses of the form \
+' @ 0x12345678' and array indices of the form '[1234]' in the prefix so is useful for comparisons of objects between different snapshots \
+or different parts of the same snapshot. \
+Also matches keys with arrays of different sizes, and for tables and trees from the current snapshot performs \
+the following operations via the context menu:\n\
+Table 1\u2229Table 2 The intersection of that row from table 1 and table 2, so only the objects from that row in both tables\n\
+Table 1\u222ATable 2 The intersection of that row from table 1 and table 2, so all the objects from that row in either table\n\
+Table 1\u2296Table 2 The symmetric difference of that row from table 1 and table 2, so all the objects from that row in only one table (or an odd number of tables)\n\
+Table 1\u2216Table 2 The difference of that row between table 1 and table 2, so all the objects from that row in table 1 but not table 2\n\
+Table 2\u2216Table 1 The difference of that row between table 2 and table 1, so all the objects from that row in table 2 but not table 1
 CompareTablesQuery.tables.help = The tables or trees to be compared
 CompareTablesQuery.contents.help = The query contexts corresponding to the tables
 CompareTablesQuery.context.help = The query context of the current snapshot
@@ -86,7 +111,7 @@
 using: \\s@ 0x[0-9a-f]+ or an array index using ^\\[[0-9]+\\]$
 CompareTablesQuery.replace.help = Replacement text for mask matches.
 CompareTablesQuery.prefix.help = Whether to include the prefix of the key column in the match \
-- for example the field name in a path.
+- for example the field name or array index in a path.
 CompareTablesQuery.suffix.help = Whether to include the suffix of the key column in the match \
 - for example the GC root type.
 CompareTablesQuery.extraReferences.help = The key can be extended by adding field references to be \
diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/RefinedStructuredResult.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/RefinedStructuredResult.java
index 0b234d1..ef6714d 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/RefinedStructuredResult.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/RefinedStructuredResult.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

@@ -20,6 +20,7 @@
 

 import org.eclipse.mat.SnapshotException;

 import org.eclipse.mat.collect.ArrayInt;

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

 import org.eclipse.mat.query.Column;

 import org.eclipse.mat.query.Column.SortDirection;

 import org.eclipse.mat.query.ContextDerivedData;

@@ -107,11 +108,13 @@
     {

         private RefinedStructuredResult refinedResult;

         private int sortColumn;

+        private Filter.ValueConverter converter;

 

         public NaturalComparator(RefinedStructuredResult refinedResult, int sortColumn)

         {

             this.refinedResult = refinedResult;

             this.sortColumn = sortColumn;

+            this.converter = (Filter.ValueConverter)refinedResult.columns.get(sortColumn).getData(Filter.ValueConverter.class);

         }

 

         @SuppressWarnings("unchecked")

@@ -119,6 +122,17 @@
         {

             Object d1 = refinedResult.getColumnValue(o1, sortColumn);

             Object d2 = refinedResult.getColumnValue(o2, sortColumn);

+            if (converter != null)

+            {

+                if (d1 instanceof Bytes)

+                    d1 = converter.convert(((Bytes)d1).getValue());

+                else if (d1 instanceof Number)

+                    d1 = converter.convert(((Number)d1).doubleValue());

+                if (d2 instanceof Bytes)

+                    d2 = converter.convert(((Bytes)d2).getValue());

+                else if (d2 instanceof Number)

+                    d2 = converter.convert(((Number)d2).doubleValue());

+            }

 

             // Compare nulls - sort first

             if (d1 == null)

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/TotalsCalculator.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/TotalsCalculator.java
index 0e23684..6df4f0c 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/TotalsCalculator.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/query/refined/TotalsCalculator.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

@@ -68,13 +68,22 @@
 

         double[] sums = new double[thisNumericColumns.size()];

 

+        Filter.ValueConverter converters[] = new Filter.ValueConverter[thisNumericColumns.size()];

+        for (int ii = 0; ii < thisNumericColumns.size(); ii++)

+        {

+            int columnIndex = thisNumericColumns.get(ii);

+            if (columnIndex < 0)

+                continue;

+            converters[ii] = (Filter.ValueConverter)columns.get(columnIndex).getData(Filter.ValueConverter.class);

+        }

+

         int counter = 0;

         ForEachRowLoop: for (Object row : elements)

         {

             // check if canceled

             if (++counter % 100 == 0)

                 if (listener.isCanceled())

-                    throw new IProgressListener.OperationCanceledException();

+                    return answer;

 

             for (int ii = 0; ii < thisNumericColumns.size(); ii++)

             {

@@ -119,6 +128,8 @@
                     }

                 }

 

+                if (converters[ii] != null)

+                    v = converters[ii].convert(v);

                 sums[ii] += v;

             }

         }

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 3a235b3..8705415 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
@@ -12,6 +12,8 @@
 package org.eclipse.mat.tests.snapshot;

 

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

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

+import static org.hamcrest.number.IsCloseTo.closeTo;

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

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

 import static org.junit.Assert.assertFalse;

@@ -19,6 +21,10 @@
 import static org.junit.Assert.assertThat;

 import static org.junit.Assert.assertTrue;

 

+import java.io.IOException;

+import java.net.URL;

+import java.text.Format;

+import java.text.ParseException;

 import java.util.ArrayList;

 import java.util.Arrays;

 import java.util.Collection;

@@ -27,6 +33,8 @@
 import java.util.Map;

 

 import org.eclipse.mat.SnapshotException;

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

@@ -36,11 +44,16 @@
 import org.eclipse.mat.query.IStructuredResult;

 import org.eclipse.mat.query.annotations.descriptors.IAnnotatedObjectDescriptor;

 import org.eclipse.mat.query.annotations.descriptors.IArgumentDescriptor;

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

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

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

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

 import org.eclipse.mat.snapshot.IOQLQuery;

 import org.eclipse.mat.snapshot.ISnapshot;

 import org.eclipse.mat.snapshot.OQLParseException;

 import org.eclipse.mat.snapshot.SnapshotFactory;

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

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

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

 import org.eclipse.mat.tests.TestSnapshots;

 import org.eclipse.mat.util.VoidProgressListener;

@@ -120,7 +133,6 @@
         assertTrue(r2 != null);

     }

 

-

     /**

      * Test that subtraction comparisons are done, even for sizes.

      */

@@ -374,7 +386,7 @@
             rc = 0;

         for (int i = 0; i < rc; ++i)

         {

-            processRow(snapshot1, r2, i, null, 3, 100, 0.1);

+            processRow(snapshot1, r2, i, null, 3, 50, 0.15);

         }

     }

 

@@ -403,9 +415,9 @@
         }

         for (ContextProvider cp : r2.getResultMetaData().getContextProviders())

         {

-            if (cp.getLabel().equals("Union of Table 1, Table 2 and Table 3") && row.toString().equals("java.lang.invoke.LambdaForm$MH:[1019, 513, 715]"))

-                System.out.println(cp.getLabel());

-            IContextObject context2 = cp.getContext(row); 

+            //if (cp.getLabel().equals("Union of Table 1, Table 2 and Table 3") && row.toString().equals("java.lang.invoke.LambdaForm$MH:[1019, 513, 715]"))

+            //    System.out.println(cp.getLabel());

+            IContextObject context2 = cp.getContext(row);

             if (context2 instanceof IContextObjectSet)

             {

                 IContextObjectSet ic = (IContextObjectSet)context2;

@@ -456,6 +468,295 @@
         }

     }

 

+    @Test

+    public void testCompareDiffRatioPreviousRetained1() throws SnapshotException, ParseException

+    {

+        testCompareDiffRatioPreviousRetained("histogram .*");

+    }

+

+    @Test

+    public void testCompareDiffRatioPreviousRetained2() throws SnapshotException, ParseException

+    {

+        testCompareDiffRatioPreviousRetained("oql \"select toString(c), c.@objectId, c.@objectAddress, c.@objectId.shortValue(), c.@objectId.byteValue(), c.@objectId.floatValue(), c.@objectId.doubleValue() from java.lang.Integer c\"");

+    }

+

+    public void testCompareDiffRatioPreviousRetained(String query) throws SnapshotException, ParseException

+    {

+        boolean verbose = true;

+        String queryId = "comparetablesquery";

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

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

+        //ISnapshot snapshot1 = TestSnapshots.getSnapshot(TestSnapshots.OPENJDK_JDK11_04_64BIT, false);

+        //ISnapshot snapshot2 = TestSnapshots.getSnapshot(TestSnapshots.ADOPTOPENJDK_HOTSPOT_JDK11_0_4_11_64BIT, false);

+

+        SnapshotQuery query1 = SnapshotQuery.parse(query, snapshot1);

+        RefinedResultBuilder rb1 = query1.refine(new VoidProgressListener());

+        rb1.setInlineRetainedSizeCalculation(true);

+        rb1.addDefaultContextDerivedColumn(RetainedSizeDerivedData.APPROXIMATE);

+        RefinedTable result1 = (RefinedTable)rb1.build();

+        List<Object>elements1 = new ArrayList<Object>();

+        // Calculate some retained sizes exactly

+        for (int i = 0; i < result1.getRowCount() - 3; i += 4)

+        {

+            elements1.add(result1.getRow(i + 1));

+            elements1.add(result1.getRow(i + 3));

+        }

+        result1.calculate(result1.getJobs().get(0).getContextProvider(), RetainedSizeDerivedData.PRECISE, elements1, null, new VoidProgressListener());

+

+        SnapshotQuery query2 = SnapshotQuery.parse(query, snapshot2);

+        RefinedResultBuilder rb2 = query2.refine(new VoidProgressListener());

+        rb2.setInlineRetainedSizeCalculation(true);

+        rb2.addDefaultContextDerivedColumn(RetainedSizeDerivedData.APPROXIMATE);

+        RefinedTable result2 = (RefinedTable)rb2.build();

+        // Calculate some retained sizes exactly

+        List<Object>elements2 = new ArrayList<Object>();

+        for (int i = 0; i < result2.getRowCount() - 3; i += 4)

+        {

+            elements2.add(result2.getRow(i + 2));

+            elements2.add(result2.getRow(i + 3));

+        }

+        result2.calculate(result2.getJobs().get(0).getContextProvider(), RetainedSizeDerivedData.PRECISE, elements2, null, new VoidProgressListener());

+

+        // Another query without retained sizes

+        SnapshotQuery query3 = SnapshotQuery.parse(query, snapshot1);

+        RefinedResultBuilder rb3 = query3.refine(new VoidProgressListener());

+        //rb3.setInlineRetainedSizeCalculation(true);

+        rb3.addDefaultContextDerivedColumn(RetainedSizeDerivedData.APPROXIMATE);

+        RefinedTable result3 = (RefinedTable)rb3.build();

+        // Calculate some retained sizes exactly

+        List<Object>elements3 = new ArrayList<Object>();

+        for (int i = 0; i < result3.getRowCount() - 3; i += 4)

+        {

+            elements3.add(result3.getRow(i + 1));

+            elements3.add(result3.getRow(i + 2));

+        }

+        result3.calculate(result3.getJobs().get(0).getContextProvider(), RetainedSizeDerivedData.PRECISE, elements3, null, new VoidProgressListener());

+

+        SnapshotQuery queryc = SnapshotQuery.parse(queryId+" -mode DIFF_RATIO_TO_PREVIOUS", snapshot1);

+

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

+        r.add((IResultTable) result1);

+        r.add((IResultTable) result2);

+        r.add((IResultTable) result3);

+        queryc.setArgument("tables", r);

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

+        snapshots.add(snapshot1);

+        snapshots.add(snapshot2);

+        snapshots.add(snapshot1);

+        queryc.setArgument("snapshots", snapshots);

+        RefinedResultBuilder rbc = queryc.refine(new VoidProgressListener());

+        rbc.setSortOrder(4, null);

+        IResultTable r2 = (IResultTable)rbc.build();

+        assertTrue(r2 != null);

+        //System.out.println(Arrays.toString(r2.getColumns()));

+        int count1 = 0;

+        int count2 = 0;

+        for (int i = 0; i < r2.getRowCount(); ++i)

+        {

+            Object v0 = r2.getColumnValue(r2.getRow(i), 0);

+            for (int j = 1; j < r2.getColumns().length - 2; j += 5)

+            {

+                /*

+                 * 3 tables, 5 results.

+                 * Table 1

+                 * Table 2 - Table 1

+                 * Table 2 / Table 1

+                 * Table 3 - Table 2

+                 * Table 3 / Table 2

+                 */

+                Object v1 = r2.getColumnValue(r2.getRow(i), j);

+                Object v2 = r2.getColumnValue(r2.getRow(i), j + 1);

+                Object v3 = r2.getColumnValue(r2.getRow(i), j + 2);

+                Object v4 = r2.getColumnValue(r2.getRow(i), j + 3);

+                Object v5 = r2.getColumnValue(r2.getRow(i), j + 4);

+                Column cols[] = r2.getColumns();

+                if (verbose) System.out.println("Row "+i+" "+cols[0].getLabel()+" "+cols[j].getLabel()+" "+cols[j+1].getLabel()+" "+cols[j+2].getLabel()+" "+cols[j+3].getLabel()+" "+cols[j+4].getLabel());

+                if (verbose) System.out.println("Row "+i+" "+v0+" "+v1+" "+v2+" "+v3+" "+v4+" "+v5);

+                // With a difference, if there is a value from table 1 then the difference has a value

+                if (v1 != null)

+                {

+                    if (v2 != null)

+                        ++count1;

+                    if (v3 != null)

+                        ++count2;

+                }

+                /*

+                 * Check formatting and parsing.

+                 */

+                if (v1 != null)

+                {

+                    Format formatter1 = r2.getColumns()[j].getFormatter();

+                    if (formatter1 != null)

+                    {

+                        String fv1 = formatter1.format(v1);

+                        if (verbose) System.out.println(fv1);

+                        assertNotNull(fv1);

+                        Object vo1 = formatter1.parseObject(fv1);

+                        if (v1 instanceof Double)

+                        {

+                            if (vo1 instanceof Long)

+                                assertThat(fv1, ((Long)vo1).doubleValue(), closeTo((Double)v1, 0.01));

+                            else

+                                assertThat(fv1, (Double)vo1, closeTo((Double)v1, 0.01));

+                        }

+                        else if (v1 instanceof Number)

+                        {

+                            // A Bytes formatter can receive long, but return bytes

+                            if (vo1 instanceof Bytes)

+                                assertThat(fv1, ((Bytes)vo1).getValue(), equalTo(((Number)v1).longValue()));

+                            else

+                                assertThat(fv1, ((Number)vo1).doubleValue(), equalTo(((Number)v1).doubleValue()));

+                        }

+                        else

+                            assertThat(fv1, vo1, equalTo(v1));

+                        checkFormat(formatter1,r2.getColumns()[j],j);

+                    }

+                }

+                if (v2 != null)

+                {

+                    Format formatter2 = r2.getColumns()[j + 1].getFormatter();

+                    String fv2 = formatter2.format(v2);

+                    if (verbose) System.out.println(fv2);

+                    assertNotNull(fv2);

+                    Object vo2 = formatter2.parseObject(fv2);

+                    if (v2 instanceof Double)

+                    {

+                        if (vo2 instanceof Long)

+                            assertThat(fv2, ((Long)vo2).doubleValue(), closeTo((Double)v2, 0.01));

+                        else

+                        {

+                            // E.g. percent can be returned as com.ibm.icu.math.BigDecimal

+                            assertThat(fv2, vo2, instanceOf(Double.class));

+                            assertThat(fv2, (Double)vo2, closeTo((Double)v2, 0.01));

+                        }

+                    }

+                    else if (v2 instanceof Number)

+                    {

+                        // A Bytes formatter can receive long, but return bytes

+                        if (vo2 instanceof Bytes)

+                            assertThat(fv2, ((Bytes)vo2).getValue(), equalTo(((Number)v2).longValue()));

+                        else

+                            assertThat(fv2, ((Number)vo2).doubleValue(), equalTo(((Number)v2).doubleValue()));

+                    }

+                    else

+                        assertThat(fv2, vo2, equalTo(v2));

+                    checkFormat(formatter2, r2.getColumns()[j+1],j+1);

+                }

+                if (v3 != null)

+                {

+                    Format formatter3 = r2.getColumns()[j + 2].getFormatter();

+                    String fv3 = formatter3.format(v3);

+                    if (verbose) System.out.println(fv3);

+                    assertNotNull(fv3);

+                    Object vo3 = formatter3.parseObject(fv3);

+                    if (v3 instanceof Double)

+                    {

+                        // E.g. percent can be returned as com.ibm.icu.math.BigDecimal

+                        assertThat(fv3, vo3, instanceOf(Number.class));

+                        // Hamcrest closeTo matcher problem with infinity

+                        if (Double.isInfinite((Double)v3))

+                            assertThat(fv3, vo3, equalTo(v3));

+                        else

+                            assertThat(fv3, ((Number)vo3).doubleValue(), closeTo((Double)v3, 0.01));

+                    }

+                    else

+                        assertThat(fv3, vo3, equalTo(v3));

+                    checkFormat(formatter3, r2.getColumns()[j+2],j+2);

+                }

+                if (v4 != null)

+                {

+                    Format formatter4 = r2.getColumns()[j + 3].getFormatter();

+                    String fv4 = formatter4.format(v4);

+                    if (verbose) System.out.println(fv4);

+                    assertNotNull(fv4);

+                    Object vo4 = formatter4.parseObject(fv4);

+                    if (v4 instanceof Double)

+                    {

+                        if (vo4 instanceof Long)

+                            assertThat(fv4, ((Long)vo4).doubleValue(), closeTo((Double)v4, 0.01));

+                        else

+                        {

+                            // E.g. percent can be returned as com.ibm.icu.math.BigDecimal

+                            assertThat(fv4, vo4, instanceOf(Double.class));

+                            assertThat(fv4, (Double)vo4, closeTo((Double)v4, 0.01));

+                        }

+                    }

+                    else if (v4 instanceof Number)

+                    {

+                        // A Bytes formatter can receive long, but return bytes

+                        if (vo4 instanceof Bytes)

+                            assertThat(fv4, ((Bytes)vo4).getValue(), equalTo(((Number)v4).longValue()));

+                        else

+                            assertThat(fv4, ((Number)vo4).doubleValue(), equalTo(((Number)v4).doubleValue()));

+                    }

+                    else

+                        assertThat(fv4, vo4, equalTo(v4));

+                    checkFormat(formatter4, r2.getColumns()[j+3],j+3);

+                }

+                if (v5 != null)

+                {

+                    Format formatter5 = r2.getColumns()[j + 4].getFormatter();

+                    String fv5 = formatter5.format(v5);

+                    if (verbose) System.out.println(fv5);

+                    assertNotNull(fv5);

+                    Object vo5 = formatter5.parseObject(fv5);

+                    if (v5 instanceof Double)

+                    {

+                        // E.g. percent can be returned as com.ibm.icu.math.BigDecimal

+                        assertThat(fv5, vo5, instanceOf(Number.class));

+                        // Hamcrest closeTo matcher problem with infinity

+                        if (Double.isInfinite((Double)v5))

+                            assertThat(fv5, vo5, equalTo(v5));

+                        else

+                            assertThat(fv5, ((Number)vo5).doubleValue(), closeTo((Double)v5, 0.01));

+                    }

+                    else

+                        assertThat(fv5, vo5, equalTo(v5));

+                    checkFormat(formatter5, r2.getColumns()[j+4],j+4);

+                }

+            }

+        }

+        assertThat(count1, greaterThan(0));

+        assertThat(count2, greaterThan(0));

+    }

+

+    void checkFormat(Format f, Column c, int colIdx) throws ParseException

+    {

+        final long special = 1000000000000000L;

+        long values[]= {Long.MIN_VALUE, -(1L<<53), -(1L<<52), -(1L<<51), -(1L<<50), -special*3, -special*2, -special, 0, special, special * 2, special * 3, 1L>>50, 1L>>51, 1L>>52, 1L>>53, Long.MAX_VALUE};

+        long dl[] = {-special, -special + 1, -1, 0, 1, special - 1, special};

+        Filter.ValueConverter vc = (Filter.ValueConverter)c.getData(Filter.ValueConverter.class);

+        for (long v1 : values)

+        {

+            for (long v2 : dl)

+            {

+                long v = v1 + v2;

+                String s = f.format(v);

+                Object vx = f.parseObject(s);

+                //System.out.println("formatted "+v+" as "+s);

+                if (vx instanceof Bytes)

+                    assertThat(f.toString() + " " + s, ((Bytes)vx).getValue(), equalTo(((Number)v).longValue()));

+                else

+                    assertThat(f.toString() + " " + s, ((Number)vx).longValue(), equalTo(v));

+                if (vc != null)

+                {

+                    // After conversion should be an ordinary number

+                    double d2 = vc.convert(v);

+                    String s2 = f.format(d2);

+                    // but some still might not convert

+                    long limit = 4000000000000000L;

+                    /*

+                     * approximate values outside limit range can't be

+                     * converted to a plain number which can be printed

+                     * without conversion.

+                     */

+                    if (v < limit && v >= -limit || !s.contains("\u2248"))

+                        assertTrue(colIdx+":"+c.getLabel()+" "+f.toString() +" " + vc + "\n" + v + "\n" + d2 + "\n" + s + "\n" + s2, s2.matches("[+-]?[0-9,]+(\\.[0-9]+)?(\\s?%)?"));

+                }

+            }

+        }

+    }

+

     /**

      * Test that set operations intersection is done.

      */

@@ -604,6 +905,7 @@
 

     /**

      * Test that set operations are done.

+     * @throws IOException

      */

     public void testCompareSetOperations(ISnapshot snapshot1, String setOp, int o1[], int o2[], int o3[], int ei1, int e1[], int ei2, int e2[]) throws SnapshotException

     {

@@ -646,6 +948,20 @@
         for (ContextProvider cp : r2.getResultMetaData().getContextProviders())

         {

             //System.out.println(cp+ " " + cp.getLabel());

+            cp.getLabel();

+            URL u = cp.getIcon();

+            if (u != null)

+            {

+                try

+                {

+                    Object o = u.getContent();

+                }

+                catch (IOException e)

+                {

+                    throw new SnapshotException(e);

+                }

+                assertNotNull(u);

+            }

         }

 

         ContextProvider cp1 = r2.getResultMetaData().getContextProviders().get(ei1);

diff --git a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/compare/CompareBasketView.java b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/compare/CompareBasketView.java
index ed0a1e4..70a1598 100644
--- a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/compare/CompareBasketView.java
+++ b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/compare/CompareBasketView.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2010, 2019 SAP AG and IBM Corporation

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

@@ -36,6 +36,7 @@
 import org.eclipse.mat.query.IResultTable;

 import org.eclipse.mat.query.IResultTree;

 import org.eclipse.mat.query.IStructuredResult;

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

 import org.eclipse.mat.query.registry.ArgumentDescriptor;

 import org.eclipse.mat.query.registry.ArgumentSet;

 import org.eclipse.mat.query.registry.QueryDescriptor;

@@ -162,24 +163,32 @@
 		ComparedResult entry = null;

 		if (pane != null)

 		{

-            QueryResult qr = pane.getAdapter(QueryResult.class);

-            if (qr != null && qr.getSubject() instanceof IResultTree)

-                entry = new ComparedResult(state, editor, (IResultTree)qr.getSubject());

-            if (qr != null && qr.getSubject() instanceof IResultTable)

-                entry = new ComparedResult(state, editor, (IResultTable)qr.getSubject());

+		    RefinedStructuredResult rsr = pane.getAdapter(RefinedStructuredResult.class);

+		    if (rsr != null && rsr instanceof IResultTree)

+		        entry = new ComparedResult(state, editor, (IResultTree)rsr);

+		    else if (rsr != null && rsr instanceof IResultTable)

+		        entry = new ComparedResult(state, editor, (IResultTable)rsr);

+		    else

+		    {

+		        QueryResult qr = pane.getAdapter(QueryResult.class);

+		        if (qr != null && qr.getSubject() instanceof IResultTree)

+		            entry = new ComparedResult(state, editor, (IResultTree)qr.getSubject());

+		        else if (qr != null && qr.getSubject() instanceof IResultTable)

+		            entry = new ComparedResult(state, editor, (IResultTable)qr.getSubject());

+		        else if (pane instanceof HistogramPane)

+		        {

+		            entry = new ComparedResult(state, editor, ((HistogramPane) pane).getHistogram());

+		        }

+		        else if (pane instanceof TableResultPane)

+		        {

+		            entry = new ComparedResult(state, editor, (IResultTable) ((TableResultPane) pane).getSrcQueryResult().getSubject());

+		        }

+		        else if (pane instanceof QueryResultPane)

+		        {

+		            entry = new ComparedResult(state, editor, (IResultTree) ((QueryResultPane) pane).getSrcQueryResult().getSubject());

+		        }

+		    }

 		}

-		if (pane instanceof HistogramPane)

-		{

-			entry = new ComparedResult(state, editor, ((HistogramPane) pane).getHistogram());

-		}

-		else if (pane instanceof TableResultPane)

-		{

-			entry = new ComparedResult(state, editor, (IResultTable) ((TableResultPane) pane).getSrcQueryResult().getSubject());

-		}

-	    else if (pane instanceof QueryResultPane)

-        {

-            entry = new ComparedResult(state, editor, (IResultTree) ((QueryResultPane) pane).getSrcQueryResult().getSubject());

-	    }

 

 		if (entry != null)

 		{

@@ -214,7 +223,7 @@
 	            return true;

 	    }

 		return (pane instanceof HistogramPane) || (pane instanceof TableResultPane) ||

-		                pane instanceof QueryResultPane && 

+		                pane instanceof QueryResultPane &&

 		                ((QueryResultPane) pane).getSrcQueryResult().getSubject() instanceof IResultTree;

 	}

 

@@ -601,7 +610,7 @@
         private final IQueryContext currentContext;

         private final List<ISnapshot> snapshots;

         private final ISnapshot currentSnapshot;

-    

+

         private ComparePolicy(List<IStructuredResult> tables, List<IQueryContext> contexts, IQueryContext currentContext)

         {

             this.tables = tables;

@@ -614,7 +623,7 @@
             }

             currentSnapshot = (ISnapshot)currentContext.get(ISnapshot.class, null);

         }

-    

+

         /**

          * Only operate on queries with multiple tables and query contexts

          */

@@ -632,8 +641,19 @@
                         {

                             if (!argument.getType().isAssignableFrom(res.getClass()))

                             {

-                                // Can't convert table/tree

-                                return false;

+                                if (res instanceof RefinedStructuredResult)

+                                {

+                                    // Perhaps the query needs a specific sort of result, not the refined version

+                                    IStructuredResult isr = ((RefinedStructuredResult)res).unwrap();

+                                    if (!argument.getType().isAssignableFrom(isr.getClass()))

+                                        // Can't convert unwrapped table/tree

+                                        return false;

+                                }

+                                else

+                                {

+                                    // Can't convert table/tree

+                                    return false;

+                                }

                             }

                             foundTables = true;

                         }

@@ -682,19 +702,34 @@
             }

             return foundTables && (foundContexts || foundSnapshots);

         }

-    

+

         public void fillInObjectArguments(ISnapshot snapshot, QueryDescriptor query, ArgumentSet set)

         {

             for (ArgumentDescriptor argument : query.getArguments()) {

+

                 if (IStructuredResult.class.isAssignableFrom(argument.getType()))

                 {

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

+                    // Do we need to unwrap some results?

+                    for (IStructuredResult res : tables)

+                    {

+                        // Perhaps the query needs a specific sort of result, not the refined version

+                        if (!argument.getType().isAssignableFrom(res.getClass()) && res instanceof RefinedStructuredResult)

+                        {

+                            // Already tested in accept() that this works

+                            IStructuredResult isr = ((RefinedStructuredResult)res).unwrap();

+                            tables1.add(isr);

+                        }

+                        else

+                            tables1.add(res);

+                    }

                     if (argument.isMultiple())

                     {

-                        set.setArgumentValue(argument, tables);

+                        set.setArgumentValue(argument, tables1);

                     }

                     else

                     {

-                        set.setArgumentValue(argument, tables.get(0));

+                        set.setArgumentValue(argument, tables1.get(0));

                     }

                 }

                 else if (IQueryContext.class.isAssignableFrom(argument.getType()))

diff --git a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/panes/QueryResultPane.java b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/panes/QueryResultPane.java
index 7df9ce3..7303635 100644
--- a/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/panes/QueryResultPane.java
+++ b/plugins/org.eclipse.mat.ui/src/org/eclipse/mat/ui/internal/panes/QueryResultPane.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2019 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

@@ -34,6 +34,7 @@
 import org.eclipse.mat.query.IResultTree;

 import org.eclipse.mat.query.IStructuredResult;

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

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

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

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

 import org.eclipse.mat.query.registry.ArgumentSet;

@@ -386,6 +387,10 @@
         {

             return (adapter.cast(srcQueryResult));

         }

+        if (adapter.isAssignableFrom(RefinedStructuredResult.class))

+        {

+            return (adapter.cast(viewer.getResult()));

+        }

         if (adapter.isAssignableFrom(srcQueryResult.getSubject().getClass()))

         {

             return (adapter.cast(srcQueryResult.getSubject()));