1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ahat.heapdump;
18 
19 import java.util.ArrayDeque;
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.Deque;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Objects;
27 
28 public class Diff {
29   /**
30    * Perform a diff between two heap lists.
31    *
32    * Heaps are diffed based on heap name. PlaceHolder heaps will be added to
33    * the given lists as necessary so that every heap in A has a corresponding
34    * heap in B and vice-versa.
35    */
heaps(List<AhatHeap> a, List<AhatHeap> b)36   private static void heaps(List<AhatHeap> a, List<AhatHeap> b) {
37     int asize = a.size();
38     int bsize = b.size();
39     for (int i = 0; i < bsize; i++) {
40       // Set the B heap's baseline as null to mark that we have not yet
41       // matched it with an A heap.
42       b.get(i).setBaseline(null);
43     }
44 
45     for (int i = 0; i < asize; i++) {
46       AhatHeap aheap = a.get(i);
47       aheap.setBaseline(null);
48       for (int j = 0; j < bsize; j++) {
49         AhatHeap bheap = b.get(j);
50         if (bheap.getBaseline() == null && aheap.getName().equals(bheap.getName())) {
51           // We found a match between aheap and bheap.
52           aheap.setBaseline(bheap);
53           bheap.setBaseline(aheap);
54           break;
55         }
56       }
57 
58       if (aheap.getBaseline() == null) {
59         // We did not find any match for aheap in snapshot B.
60         // Create a placeholder heap in snapshot B to use as the baseline.
61         b.add(AhatHeap.newPlaceHolderHeap(aheap.getName(), aheap));
62       }
63     }
64 
65     // Make placeholder heaps in snapshot A for any unmatched heaps in
66     // snapshot B.
67     for (int i = 0; i < bsize; i++) {
68       AhatHeap bheap = b.get(i);
69       if (bheap.getBaseline() == null) {
70         a.add(AhatHeap.newPlaceHolderHeap(bheap.getName(), bheap));
71       }
72     }
73   }
74 
75   /**
76    * Key represents an equivalence class of AhatInstances that are allowed to
77    * be considered for correspondence between two different snapshots.
78    */
79   private static class Key {
80     // Corresponding objects must belong to classes of the same name.
81     private final String mClass;
82 
83     // Corresponding objects must belong to heaps of the same name.
84     private final String mHeapName;
85 
86     // Corresponding string objects must have the same value.
87     // mStringValue is set to the empty string for non-string objects.
88     private final String mStringValue;
89 
90     // Corresponding class objects must have the same class name.
91     // mClassName is set to the empty string for non-class objects.
92     private final String mClassName;
93 
94     // Corresponding array objects must have the same length.
95     // mArrayLength is set to 0 for non-array objects.
96     private final int mArrayLength;
97 
98 
Key(AhatInstance inst)99     private Key(AhatInstance inst) {
100       mClass = inst.getClassName();
101       mHeapName = inst.getHeap().getName();
102       mClassName = inst.isClassObj() ? inst.asClassObj().getName() : "";
103       String string = inst.asString();
104       mStringValue = string == null ? "" : string;
105       AhatArrayInstance array = inst.asArrayInstance();
106       mArrayLength = array == null ? 0 : array.getLength();
107     }
108 
109     /**
110      * Return the key for the given instance.
111      */
keyFor(AhatInstance inst)112     public static Key keyFor(AhatInstance inst) {
113       return new Key(inst);
114     }
115 
116     @Override
equals(Object other)117     public boolean equals(Object other) {
118       if (!(other instanceof Key)) {
119         return false;
120       }
121       Key o = (Key)other;
122       return mClass.equals(o.mClass)
123           && mHeapName.equals(o.mHeapName)
124           && mStringValue.equals(o.mStringValue)
125           && mClassName.equals(o.mClassName)
126           && mArrayLength == o.mArrayLength;
127     }
128 
129     @Override
hashCode()130     public int hashCode() {
131       return Objects.hash(mClass, mHeapName, mStringValue, mClassName, mArrayLength);
132     }
133   }
134 
135   private static class InstanceListPair {
136     public final List<AhatInstance> a;
137     public final List<AhatInstance> b;
138 
InstanceListPair()139     public InstanceListPair() {
140       this.a = new ArrayList<AhatInstance>();
141       this.b = new ArrayList<AhatInstance>();
142     }
143 
InstanceListPair(List<AhatInstance> a, List<AhatInstance> b)144     public InstanceListPair(List<AhatInstance> a, List<AhatInstance> b) {
145       this.a = a;
146       this.b = b;
147     }
148   }
149 
150   /**
151    * Recursively create place holder instances for the given instance and
152    * every instance dominated by that instance.
153    * Returns the place holder instance created for the given instance.
154    * Adds all allocated placeholders to the given placeholders list.
155    */
createPlaceHolders(AhatInstance inst, List<AhatInstance> placeholders)156   private static AhatInstance createPlaceHolders(AhatInstance inst,
157       List<AhatInstance> placeholders) {
158     // Don't actually use recursion, because we could easily smash the stack.
159     // Instead we iterate.
160     AhatInstance result = inst.newPlaceHolderInstance();
161     placeholders.add(result);
162     Deque<AhatInstance> deque = new ArrayDeque<AhatInstance>();
163     deque.push(inst);
164     while (!deque.isEmpty()) {
165       inst = deque.pop();
166 
167       for (AhatInstance child : inst.getDominated()) {
168         placeholders.add(child.newPlaceHolderInstance());
169         deque.push(child);
170       }
171     }
172     return result;
173   }
174 
175   /**
176    * Recursively diff two dominator trees of instances.
177    * PlaceHolder objects are appended to the lists as needed to ensure every
178    * object has a corresponding baseline in the other list. All PlaceHolder
179    * objects are also appended to the given placeholders list, so their Site
180    * info can be updated later on.
181    */
instances(List<AhatInstance> a, List<AhatInstance> b, List<AhatInstance> placeholders)182   private static void instances(List<AhatInstance> a, List<AhatInstance> b,
183       List<AhatInstance> placeholders) {
184     // Don't actually use recursion, because we could easily smash the stack.
185     // Instead we iterate.
186     Deque<InstanceListPair> deque = new ArrayDeque<InstanceListPair>();
187     deque.push(new InstanceListPair(a, b));
188     while (!deque.isEmpty()) {
189       InstanceListPair p = deque.pop();
190 
191       // Group instances of the same equivalence class together.
192       Map<Key, InstanceListPair> byKey = new HashMap<Key, InstanceListPair>();
193       for (AhatInstance inst : p.a) {
194         Key key = Key.keyFor(inst);
195         InstanceListPair pair = byKey.get(key);
196         if (pair == null) {
197           pair = new InstanceListPair();
198           byKey.put(key, pair);
199         }
200         pair.a.add(inst);
201       }
202       for (AhatInstance inst : p.b) {
203         Key key = Key.keyFor(inst);
204         InstanceListPair pair = byKey.get(key);
205         if (pair == null) {
206           pair = new InstanceListPair();
207           byKey.put(key, pair);
208         }
209         pair.b.add(inst);
210       }
211 
212       // diff objects from the same key class.
213       for (InstanceListPair pair : byKey.values()) {
214         // Sort by retained size and assume the elements at the top of the lists
215         // correspond to each other in that order. This could probably be
216         // improved if desired, but it gives good enough results for now.
217         Collections.sort(pair.a, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE);
218         Collections.sort(pair.b, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE);
219 
220         int common = Math.min(pair.a.size(), pair.b.size());
221         for (int i = 0; i < common; i++) {
222           AhatInstance ainst = pair.a.get(i);
223           AhatInstance binst = pair.b.get(i);
224           ainst.setBaseline(binst);
225           binst.setBaseline(ainst);
226           deque.push(new InstanceListPair(ainst.getDominated(), binst.getDominated()));
227         }
228 
229         // Add placeholder objects for anything leftover.
230         for (int i = common; i < pair.a.size(); i++) {
231           p.b.add(createPlaceHolders(pair.a.get(i), placeholders));
232         }
233 
234         for (int i = common; i < pair.b.size(); i++) {
235           p.a.add(createPlaceHolders(pair.b.get(i), placeholders));
236         }
237       }
238     }
239   }
240 
241   /**
242    * Sets the baseline for root and all its descendants to baseline.
243    */
setSitesBaseline(Site root, Site baseline)244   private static void setSitesBaseline(Site root, Site baseline) {
245     root.setBaseline(baseline);
246     for (Site child : root.getChildren()) {
247       setSitesBaseline(child, baseline);
248     }
249   }
250 
251   /**
252    * Recursively diff the two sites, setting them and their descendants as
253    * baselines for each other as appropriate.
254    *
255    * This requires that instances have already been diffed. In particular, we
256    * require all AhatClassObjs in one snapshot have corresponding (possibly
257    * place-holder) AhatClassObjs in the other snapshot.
258    */
sites(Site a, Site b)259   private static void sites(Site a, Site b) {
260     // Set the sites as baselines of each other.
261     a.setBaseline(b);
262     b.setBaseline(a);
263 
264     // Set the site's ObjectsInfos as baselines of each other. This implicitly
265     // adds new empty ObjectsInfo as needed.
266     for (Site.ObjectsInfo ainfo : a.getObjectsInfos()) {
267       AhatClassObj baseClassObj = null;
268       if (ainfo.classObj != null) {
269         baseClassObj = (AhatClassObj) ainfo.classObj.getBaseline();
270       }
271       ainfo.setBaseline(b.getObjectsInfo(ainfo.heap.getBaseline(), baseClassObj));
272     }
273     for (Site.ObjectsInfo binfo : b.getObjectsInfos()) {
274       AhatClassObj baseClassObj = null;
275       if (binfo.classObj != null) {
276         baseClassObj = (AhatClassObj) binfo.classObj.getBaseline();
277       }
278       binfo.setBaseline(a.getObjectsInfo(binfo.heap.getBaseline(), baseClassObj));
279     }
280 
281     // Set B children's baselines as null to mark that we have not yet matched
282     // them with A children.
283     for (Site bchild : b.getChildren()) {
284       bchild.setBaseline(null);
285     }
286 
287     for (Site achild : a.getChildren()) {
288       achild.setBaseline(null);
289       for (Site bchild : b.getChildren()) {
290         if (achild.getLineNumber() == bchild.getLineNumber()
291             && achild.getMethodName().equals(bchild.getMethodName())
292             && achild.getSignature().equals(bchild.getSignature())
293             && achild.getFilename().equals(bchild.getFilename())) {
294           // We found a match between achild and bchild.
295           sites(achild, bchild);
296           break;
297         }
298       }
299 
300       if (achild.getBaseline() == null) {
301         // We did not find any match for achild in site B.
302         // Use B for the baseline of achild and its descendants.
303         setSitesBaseline(achild, b);
304       }
305     }
306 
307     for (Site bchild : b.getChildren()) {
308       if (bchild.getBaseline() == null) {
309         setSitesBaseline(bchild, a);
310       }
311     }
312   }
313 
314   /**
315    * Perform a diff of the two snapshots, setting each as the baseline for the
316    * other.
317    */
snapshots(AhatSnapshot a, AhatSnapshot b)318   public static void snapshots(AhatSnapshot a, AhatSnapshot b) {
319     a.setBaseline(b);
320     b.setBaseline(a);
321 
322     // Diff the heaps of each snapshot.
323     heaps(a.getHeaps(), b.getHeaps());
324 
325     // Diff the instances of each snapshot.
326     List<AhatInstance> placeholders = new ArrayList<AhatInstance>();
327     instances(a.getRooted(), b.getRooted(), placeholders);
328 
329     // Diff the sites of each snapshot.
330     // This requires the instances have already been diffed.
331     sites(a.getRootSite(), b.getRootSite());
332 
333     // Add placeholders to their corresponding sites.
334     // This requires the sites have already been diffed.
335     for (AhatInstance placeholder : placeholders) {
336       placeholder.getBaseline().getSite().getBaseline().addPlaceHolderInstance(placeholder);
337     }
338   }
339 
340   /**
341    * Diff two lists of field values.
342    * PlaceHolder objects are added to the given lists as needed to ensure
343    * every FieldValue in A ends up with a corresponding FieldValue in B.
344    */
fields(List<FieldValue> a, List<FieldValue> b)345   public static void fields(List<FieldValue> a, List<FieldValue> b) {
346     // Fields with the same name and type are considered matching fields.
347     // For simplicity, we assume the matching fields are in the same order in
348     // both A and B, though some fields may be added or removed in either
349     // list. If our assumption is wrong, in the worst case the quality of the
350     // field diff is poor.
351 
352     for (int i = 0; i < a.size(); i++) {
353       FieldValue afield = a.get(i);
354       afield.setBaseline(null);
355 
356       // Find the matching field in B, if any.
357       for (int j = i; j < b.size(); j++) {
358         FieldValue bfield = b.get(j);
359         if (afield.getName().equals(bfield.getName())
360             && afield.getType().equals(bfield.getType())) {
361           // We found the matching field in B.
362           // Assume fields i, ..., j-1 in B have no match in A.
363           for ( ; i < j; i++) {
364             a.add(i, FieldValue.newPlaceHolderFieldValue(b.get(i)));
365           }
366 
367           afield.setBaseline(bfield);
368           bfield.setBaseline(afield);
369           break;
370         }
371       }
372 
373       if (afield.getBaseline() == null) {
374         b.add(i, FieldValue.newPlaceHolderFieldValue(afield));
375       }
376     }
377 
378     // All remaining fields in B are unmatched by any in A.
379     for (int i = a.size(); i < b.size(); i++) {
380       a.add(i, FieldValue.newPlaceHolderFieldValue(b.get(i)));
381     }
382   }
383 }
384