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 /**
29  * Provides a static method to diff two heap dumps.
30  */
31 public class Diff {
Diff()32   private Diff() {
33   }
34 
35   /**
36    * Performs a diff between two heap lists.
37    *
38    * Heaps are diffed based on heap name. PlaceHolder heaps will be added to
39    * the given lists as necessary so that every heap in A has a corresponding
40    * heap in B and vice-versa.
41    */
heaps(List<AhatHeap> a, List<AhatHeap> b)42   private static void heaps(List<AhatHeap> a, List<AhatHeap> b) {
43     int asize = a.size();
44     int bsize = b.size();
45     for (int i = 0; i < bsize; i++) {
46       // Set the B heap's baseline as null to mark that we have not yet
47       // matched it with an A heap.
48       b.get(i).setBaseline(null);
49     }
50 
51     for (int i = 0; i < asize; i++) {
52       AhatHeap aheap = a.get(i);
53       aheap.setBaseline(null);
54       for (int j = 0; j < bsize; j++) {
55         AhatHeap bheap = b.get(j);
56         if (bheap.getBaseline() == null && aheap.getName().equals(bheap.getName())) {
57           // We found a match between aheap and bheap.
58           aheap.setBaseline(bheap);
59           bheap.setBaseline(aheap);
60           break;
61         }
62       }
63 
64       if (aheap.getBaseline() == null) {
65         // We did not find any match for aheap in snapshot B.
66         // Create a placeholder heap in snapshot B to use as the baseline.
67         b.add(AhatHeap.newPlaceHolderHeap(aheap.getName(), aheap));
68       }
69     }
70 
71     // Make placeholder heaps in snapshot A for any unmatched heaps in
72     // snapshot B.
73     for (int i = 0; i < bsize; i++) {
74       AhatHeap bheap = b.get(i);
75       if (bheap.getBaseline() == null) {
76         a.add(AhatHeap.newPlaceHolderHeap(bheap.getName(), bheap));
77       }
78     }
79   }
80 
81   /**
82    * Key represents an equivalence class of AhatInstances that are allowed to
83    * be considered for correspondence between two different snapshots.
84    */
85   private static class Key {
86     // Corresponding objects must belong to classes of the same name.
87     private final String mClass;
88 
89     // Corresponding objects must belong to heaps of the same name.
90     private final String mHeapName;
91 
92     // Corresponding string objects must have the same value.
93     // mStringValue is set to the empty string for non-string objects.
94     private final String mStringValue;
95 
96     // Corresponding class objects must have the same class name.
97     // mClassName is set to the empty string for non-class objects.
98     private final String mClassName;
99 
100     // Corresponding array objects must have the same length.
101     // mArrayLength is set to 0 for non-array objects.
102     private final int mArrayLength;
103 
104 
Key(AhatInstance inst)105     private Key(AhatInstance inst) {
106       mClass = inst.getClassName();
107       mHeapName = inst.getHeap().getName();
108       mClassName = inst.isClassObj() ? inst.asClassObj().getName() : "";
109       String string = inst.asString();
110       mStringValue = string == null ? "" : string;
111       AhatArrayInstance array = inst.asArrayInstance();
112       mArrayLength = array == null ? 0 : array.getLength();
113     }
114 
115     /**
116      * Return the key for the given instance.
117      */
keyFor(AhatInstance inst)118     public static Key keyFor(AhatInstance inst) {
119       return new Key(inst);
120     }
121 
122     @Override
equals(Object other)123     public boolean equals(Object other) {
124       if (!(other instanceof Key)) {
125         return false;
126       }
127       Key o = (Key)other;
128       return mClass.equals(o.mClass)
129           && mHeapName.equals(o.mHeapName)
130           && mStringValue.equals(o.mStringValue)
131           && mClassName.equals(o.mClassName)
132           && mArrayLength == o.mArrayLength;
133     }
134 
135     @Override
hashCode()136     public int hashCode() {
137       return Objects.hash(mClass, mHeapName, mStringValue, mClassName, mArrayLength);
138     }
139   }
140 
141   private static class InstanceListPair {
142     public final List<AhatInstance> a;
143     public final List<AhatInstance> b;
144 
InstanceListPair()145     public InstanceListPair() {
146       this.a = new ArrayList<AhatInstance>();
147       this.b = new ArrayList<AhatInstance>();
148     }
149 
InstanceListPair(List<AhatInstance> a, List<AhatInstance> b)150     public InstanceListPair(List<AhatInstance> a, List<AhatInstance> b) {
151       this.a = a;
152       this.b = b;
153     }
154   }
155 
156   /**
157    * Recursively create placeholder instances for the given instance and
158    * every instance dominated by that instance.
159    * Returns the placeholder instance created for the given instance.
160    * Adds all allocated placeholders to the given placeholders list.
161    */
createPlaceHolders(AhatInstance inst, List<AhatInstance> placeholders)162   private static AhatInstance createPlaceHolders(AhatInstance inst,
163       List<AhatInstance> placeholders) {
164     // Don't actually use recursion, because we could easily smash the stack.
165     // Instead we iterate.
166     AhatInstance result = inst.newPlaceHolderInstance();
167     placeholders.add(result);
168     Deque<AhatInstance> deque = new ArrayDeque<AhatInstance>();
169     deque.push(inst);
170     while (!deque.isEmpty()) {
171       inst = deque.pop();
172 
173       for (AhatInstance child : inst.getDominated()) {
174         placeholders.add(child.newPlaceHolderInstance());
175         deque.push(child);
176       }
177     }
178     return result;
179   }
180 
181   /**
182    * Recursively diff two dominator trees of instances.
183    * PlaceHolder objects are appended to the lists as needed to ensure every
184    * object has a corresponding baseline in the other list. All PlaceHolder
185    * objects are also appended to the given placeholders list, so their Site
186    * info can be updated later on.
187    */
instances(List<AhatInstance> a, List<AhatInstance> b, List<AhatInstance> placeholders)188   private static void instances(List<AhatInstance> a, List<AhatInstance> b,
189       List<AhatInstance> placeholders) {
190     // Don't actually use recursion, because we could easily smash the stack.
191     // Instead we iterate.
192     Deque<InstanceListPair> deque = new ArrayDeque<InstanceListPair>();
193     deque.push(new InstanceListPair(a, b));
194     while (!deque.isEmpty()) {
195       InstanceListPair p = deque.pop();
196 
197       // Group instances of the same equivalence class together.
198       Map<Key, InstanceListPair> byKey = new HashMap<Key, InstanceListPair>();
199       for (AhatInstance inst : p.a) {
200         Key key = Key.keyFor(inst);
201         InstanceListPair pair = byKey.get(key);
202         if (pair == null) {
203           pair = new InstanceListPair();
204           byKey.put(key, pair);
205         }
206         pair.a.add(inst);
207       }
208       for (AhatInstance inst : p.b) {
209         Key key = Key.keyFor(inst);
210         InstanceListPair pair = byKey.get(key);
211         if (pair == null) {
212           pair = new InstanceListPair();
213           byKey.put(key, pair);
214         }
215         pair.b.add(inst);
216       }
217 
218       // diff objects from the same key class.
219       for (InstanceListPair pair : byKey.values()) {
220         // Sort by retained size and assume the elements at the top of the lists
221         // correspond to each other in that order. This could probably be
222         // improved if desired, but it gives good enough results for now.
223         Collections.sort(pair.a, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE);
224         Collections.sort(pair.b, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE);
225 
226         int common = Math.min(pair.a.size(), pair.b.size());
227         for (int i = 0; i < common; i++) {
228           AhatInstance ainst = pair.a.get(i);
229           AhatInstance binst = pair.b.get(i);
230           ainst.setBaseline(binst);
231           binst.setBaseline(ainst);
232           deque.push(new InstanceListPair(ainst.getDominated(), binst.getDominated()));
233         }
234 
235         // Add placeholder objects for anything leftover.
236         for (int i = common; i < pair.a.size(); i++) {
237           p.b.add(createPlaceHolders(pair.a.get(i), placeholders));
238         }
239 
240         for (int i = common; i < pair.b.size(); i++) {
241           p.a.add(createPlaceHolders(pair.b.get(i), placeholders));
242         }
243       }
244     }
245   }
246 
247   /**
248    * Sets the baseline for root and all its descendants to baseline.
249    */
setSitesBaseline(Site root, Site baseline)250   private static void setSitesBaseline(Site root, Site baseline) {
251     root.setBaseline(baseline);
252     for (Site child : root.getChildren()) {
253       setSitesBaseline(child, baseline);
254     }
255   }
256 
257   /**
258    * Recursively diff the two sites, setting them and their descendants as
259    * baselines for each other as appropriate.
260    *
261    * This requires that instances have already been diffed. In particular, we
262    * require all AhatClassObjs in one snapshot have corresponding (possibly
263    * placeholder) AhatClassObjs in the other snapshot.
264    */
sites(Site a, Site b)265   private static void sites(Site a, Site b) {
266     // Set the sites as baselines of each other.
267     a.setBaseline(b);
268     b.setBaseline(a);
269 
270     // Set the site's ObjectsInfos as baselines of each other. This implicitly
271     // adds new empty ObjectsInfo as needed.
272     for (Site.ObjectsInfo ainfo : a.getObjectsInfos()) {
273       AhatClassObj baseClassObj = null;
274       if (ainfo.classObj != null) {
275         baseClassObj = (AhatClassObj) ainfo.classObj.getBaseline();
276       }
277       ainfo.setBaseline(b.getObjectsInfo(ainfo.heap.getBaseline(), baseClassObj));
278     }
279     for (Site.ObjectsInfo binfo : b.getObjectsInfos()) {
280       AhatClassObj baseClassObj = null;
281       if (binfo.classObj != null) {
282         baseClassObj = (AhatClassObj) binfo.classObj.getBaseline();
283       }
284       binfo.setBaseline(a.getObjectsInfo(binfo.heap.getBaseline(), baseClassObj));
285     }
286 
287     // Set B children's baselines as null to mark that we have not yet matched
288     // them with A children.
289     for (Site bchild : b.getChildren()) {
290       bchild.setBaseline(null);
291     }
292 
293     for (Site achild : a.getChildren()) {
294       achild.setBaseline(null);
295       for (Site bchild : b.getChildren()) {
296         if (achild.getLineNumber() == bchild.getLineNumber()
297             && achild.getMethodName().equals(bchild.getMethodName())
298             && achild.getSignature().equals(bchild.getSignature())
299             && achild.getFilename().equals(bchild.getFilename())) {
300           // We found a match between achild and bchild.
301           sites(achild, bchild);
302           break;
303         }
304       }
305 
306       if (achild.getBaseline() == null) {
307         // We did not find any match for achild in site B.
308         // Use B for the baseline of achild and its descendants.
309         setSitesBaseline(achild, b);
310       }
311     }
312 
313     for (Site bchild : b.getChildren()) {
314       if (bchild.getBaseline() == null) {
315         setSitesBaseline(bchild, a);
316       }
317     }
318   }
319 
320   /**
321    * Performs a diff of two snapshots.
322    * Each snapshot will be set as the baseline for the other snapshot.
323    * <p>
324    * The diff algorithm attempts to match instances in snapshot <code>a</code>
325    * to corresponding instances in snapshot <code>b</code>. The snapshots need
326    * not come from the same running process, application version, or platform
327    * version.
328    *
329    * @param a one of the snapshots to diff
330    * @param b the other of the snapshots to diff
331    */
snapshots(AhatSnapshot a, AhatSnapshot b)332   public static void snapshots(AhatSnapshot a, AhatSnapshot b) {
333     a.setBaseline(b);
334     b.setBaseline(a);
335 
336     // Diff the heaps of each snapshot.
337     heaps(a.getHeaps(), b.getHeaps());
338 
339     // Diff the instances of each snapshot.
340     List<AhatInstance> placeholders = new ArrayList<AhatInstance>();
341     instances(a.getRooted(), b.getRooted(), placeholders);
342 
343     // Diff the sites of each snapshot.
344     // This requires the instances have already been diffed.
345     sites(a.getRootSite(), b.getRootSite());
346 
347     // Add placeholders to their corresponding sites.
348     // This requires the sites have already been diffed.
349     for (AhatInstance placeholder : placeholders) {
350       placeholder.getBaseline().getSite().getBaseline().addInstance(placeholder);
351     }
352   }
353 }
354