diff options
Diffstat (limited to 'tools/ahat/src/heapdump/Diff.java')
-rw-r--r-- | tools/ahat/src/heapdump/Diff.java | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/tools/ahat/src/heapdump/Diff.java b/tools/ahat/src/heapdump/Diff.java new file mode 100644 index 0000000000..943e6e63f5 --- /dev/null +++ b/tools/ahat/src/heapdump/Diff.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class Diff { + /** + * Perform a diff between two heap lists. + * + * Heaps are diffed based on heap name. PlaceHolder heaps will be added to + * the given lists as necessary so that every heap in A has a corresponding + * heap in B and vice-versa. + */ + private static void heaps(List<AhatHeap> a, List<AhatHeap> b) { + int asize = a.size(); + int bsize = b.size(); + for (int i = 0; i < bsize; i++) { + // Set the B heap's baseline as null to mark that we have not yet + // matched it with an A heap. + b.get(i).setBaseline(null); + } + + for (int i = 0; i < asize; i++) { + AhatHeap aheap = a.get(i); + aheap.setBaseline(null); + for (int j = 0; j < bsize; j++) { + AhatHeap bheap = b.get(j); + if (bheap.getBaseline() == null && aheap.getName().equals(bheap.getName())) { + // We found a match between aheap and bheap. + aheap.setBaseline(bheap); + bheap.setBaseline(aheap); + break; + } + } + + if (aheap.getBaseline() == null) { + // We did not find any match for aheap in snapshot B. + // Create a placeholder heap in snapshot B to use as the baseline. + b.add(AhatHeap.newPlaceHolderHeap(aheap.getName(), aheap)); + } + } + + // Make placeholder heaps in snapshot A for any unmatched heaps in + // snapshot B. + for (int i = 0; i < bsize; i++) { + AhatHeap bheap = b.get(i); + if (bheap.getBaseline() == null) { + a.add(AhatHeap.newPlaceHolderHeap(bheap.getName(), bheap)); + } + } + } + + /** + * Key represents an equivalence class of AhatInstances that are allowed to + * be considered for correspondence between two different snapshots. + */ + private static class Key { + // Corresponding objects must belong to classes of the same name. + private final String mClass; + + // Corresponding objects must belong to heaps of the same name. + private final String mHeapName; + + // Corresponding string objects must have the same value. + // mStringValue is set to the empty string for non-string objects. + private final String mStringValue; + + // Corresponding class objects must have the same class name. + // mClassName is set to the empty string for non-class objects. + private final String mClassName; + + // Corresponding array objects must have the same length. + // mArrayLength is set to 0 for non-array objects. + private final int mArrayLength; + + + private Key(AhatInstance inst) { + mClass = inst.getClassName(); + mHeapName = inst.getHeap().getName(); + mClassName = inst.isClassObj() ? inst.asClassObj().getName() : ""; + String string = inst.asString(); + mStringValue = string == null ? "" : string; + AhatArrayInstance array = inst.asArrayInstance(); + mArrayLength = array == null ? 0 : array.getLength(); + } + + /** + * Return the key for the given instance. + */ + public static Key keyFor(AhatInstance inst) { + return new Key(inst); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Key)) { + return false; + } + Key o = (Key)other; + return mClass.equals(o.mClass) + && mHeapName.equals(o.mHeapName) + && mStringValue.equals(o.mStringValue) + && mClassName.equals(o.mClassName) + && mArrayLength == o.mArrayLength; + } + + @Override + public int hashCode() { + return Objects.hash(mClass, mHeapName, mStringValue, mClassName, mArrayLength); + } + } + + private static class InstanceListPair { + public final List<AhatInstance> a; + public final List<AhatInstance> b; + + public InstanceListPair() { + this.a = new ArrayList<AhatInstance>(); + this.b = new ArrayList<AhatInstance>(); + } + + public InstanceListPair(List<AhatInstance> a, List<AhatInstance> b) { + this.a = a; + this.b = b; + } + } + + /** + * Recursively create place holder instances for the given instance and + * every instance dominated by that instance. + * Returns the place holder instance created for the given instance. + * Adds all allocated placeholders to the given placeholders list. + */ + private static AhatInstance createPlaceHolders(AhatInstance inst, + List<AhatInstance> placeholders) { + // Don't actually use recursion, because we could easily smash the stack. + // Instead we iterate. + AhatInstance result = inst.newPlaceHolderInstance(); + placeholders.add(result); + Deque<AhatInstance> deque = new ArrayDeque<AhatInstance>(); + deque.push(inst); + while (!deque.isEmpty()) { + inst = deque.pop(); + + for (AhatInstance child : inst.getDominated()) { + placeholders.add(child.newPlaceHolderInstance()); + deque.push(child); + } + } + return result; + } + + /** + * Recursively diff two dominator trees of instances. + * PlaceHolder objects are appended to the lists as needed to ensure every + * object has a corresponding baseline in the other list. All PlaceHolder + * objects are also appended to the given placeholders list, so their Site + * info can be updated later on. + */ + private static void instances(List<AhatInstance> a, List<AhatInstance> b, + List<AhatInstance> placeholders) { + // Don't actually use recursion, because we could easily smash the stack. + // Instead we iterate. + Deque<InstanceListPair> deque = new ArrayDeque<InstanceListPair>(); + deque.push(new InstanceListPair(a, b)); + while (!deque.isEmpty()) { + InstanceListPair p = deque.pop(); + + // Group instances of the same equivalence class together. + Map<Key, InstanceListPair> byKey = new HashMap<Key, InstanceListPair>(); + for (AhatInstance inst : p.a) { + Key key = Key.keyFor(inst); + InstanceListPair pair = byKey.get(key); + if (pair == null) { + pair = new InstanceListPair(); + byKey.put(key, pair); + } + pair.a.add(inst); + } + for (AhatInstance inst : p.b) { + Key key = Key.keyFor(inst); + InstanceListPair pair = byKey.get(key); + if (pair == null) { + pair = new InstanceListPair(); + byKey.put(key, pair); + } + pair.b.add(inst); + } + + // diff objects from the same key class. + for (InstanceListPair pair : byKey.values()) { + // Sort by retained size and assume the elements at the top of the lists + // correspond to each other in that order. This could probably be + // improved if desired, but it gives good enough results for now. + Collections.sort(pair.a, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE); + Collections.sort(pair.b, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE); + + int common = Math.min(pair.a.size(), pair.b.size()); + for (int i = 0; i < common; i++) { + AhatInstance ainst = pair.a.get(i); + AhatInstance binst = pair.b.get(i); + ainst.setBaseline(binst); + binst.setBaseline(ainst); + deque.push(new InstanceListPair(ainst.getDominated(), binst.getDominated())); + } + + // Add placeholder objects for anything leftover. + for (int i = common; i < pair.a.size(); i++) { + p.b.add(createPlaceHolders(pair.a.get(i), placeholders)); + } + + for (int i = common; i < pair.b.size(); i++) { + p.a.add(createPlaceHolders(pair.b.get(i), placeholders)); + } + } + } + } + + /** + * Sets the baseline for root and all its descendants to baseline. + */ + private static void setSitesBaseline(Site root, Site baseline) { + root.setBaseline(baseline); + for (Site child : root.getChildren()) { + setSitesBaseline(child, baseline); + } + } + + /** + * Recursively diff the two sites, setting them and their descendants as + * baselines for each other as appropriate. + * + * This requires that instances have already been diffed. In particular, we + * require all AhatClassObjs in one snapshot have corresponding (possibly + * place-holder) AhatClassObjs in the other snapshot. + */ + private static void sites(Site a, Site b) { + // Set the sites as baselines of each other. + a.setBaseline(b); + b.setBaseline(a); + + // Set the site's ObjectsInfos as baselines of each other. This implicitly + // adds new empty ObjectsInfo as needed. + for (Site.ObjectsInfo ainfo : a.getObjectsInfos()) { + AhatClassObj baseClassObj = null; + if (ainfo.classObj != null) { + baseClassObj = (AhatClassObj) ainfo.classObj.getBaseline(); + } + ainfo.setBaseline(b.getObjectsInfo(ainfo.heap.getBaseline(), baseClassObj)); + } + for (Site.ObjectsInfo binfo : b.getObjectsInfos()) { + AhatClassObj baseClassObj = null; + if (binfo.classObj != null) { + baseClassObj = (AhatClassObj) binfo.classObj.getBaseline(); + } + binfo.setBaseline(a.getObjectsInfo(binfo.heap.getBaseline(), baseClassObj)); + } + + // Set B children's baselines as null to mark that we have not yet matched + // them with A children. + for (Site bchild : b.getChildren()) { + bchild.setBaseline(null); + } + + for (Site achild : a.getChildren()) { + achild.setBaseline(null); + for (Site bchild : b.getChildren()) { + if (achild.getLineNumber() == bchild.getLineNumber() + && achild.getMethodName().equals(bchild.getMethodName()) + && achild.getSignature().equals(bchild.getSignature()) + && achild.getFilename().equals(bchild.getFilename())) { + // We found a match between achild and bchild. + sites(achild, bchild); + break; + } + } + + if (achild.getBaseline() == null) { + // We did not find any match for achild in site B. + // Use B for the baseline of achild and its descendants. + setSitesBaseline(achild, b); + } + } + + for (Site bchild : b.getChildren()) { + if (bchild.getBaseline() == null) { + setSitesBaseline(bchild, a); + } + } + } + + /** + * Perform a diff of the two snapshots, setting each as the baseline for the + * other. + */ + public static void snapshots(AhatSnapshot a, AhatSnapshot b) { + a.setBaseline(b); + b.setBaseline(a); + + // Diff the heaps of each snapshot. + heaps(a.getHeaps(), b.getHeaps()); + + // Diff the instances of each snapshot. + List<AhatInstance> placeholders = new ArrayList<AhatInstance>(); + instances(a.getRooted(), b.getRooted(), placeholders); + + // Diff the sites of each snapshot. + // This requires the instances have already been diffed. + sites(a.getRootSite(), b.getRootSite()); + + // Add placeholders to their corresponding sites. + // This requires the sites have already been diffed. + for (AhatInstance placeholder : placeholders) { + placeholder.getBaseline().getSite().getBaseline().addPlaceHolderInstance(placeholder); + } + } + + /** + * Diff two lists of field values. + * PlaceHolder objects are added to the given lists as needed to ensure + * every FieldValue in A ends up with a corresponding FieldValue in B. + */ + public static void fields(List<FieldValue> a, List<FieldValue> b) { + // Fields with the same name and type are considered matching fields. + // For simplicity, we assume the matching fields are in the same order in + // both A and B, though some fields may be added or removed in either + // list. If our assumption is wrong, in the worst case the quality of the + // field diff is poor. + + for (int i = 0; i < a.size(); i++) { + FieldValue afield = a.get(i); + afield.setBaseline(null); + + // Find the matching field in B, if any. + for (int j = i; j < b.size(); j++) { + FieldValue bfield = b.get(j); + if (afield.getName().equals(bfield.getName()) + && afield.getType().equals(bfield.getType())) { + // We found the matching field in B. + // Assume fields i, ..., j-1 in B have no match in A. + for ( ; i < j; i++) { + a.add(i, FieldValue.newPlaceHolderFieldValue(b.get(i))); + } + + afield.setBaseline(bfield); + bfield.setBaseline(afield); + break; + } + } + + if (afield.getBaseline() == null) { + b.add(i, FieldValue.newPlaceHolderFieldValue(afield)); + } + } + + // All remaining fields in B are unmatched by any in A. + for (int i = a.size(); i < b.size(); i++) { + a.add(i, FieldValue.newPlaceHolderFieldValue(b.get(i))); + } + } +} |