1 /*
2  * Copyright (C) 2015 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;
18 
19 import com.android.ahat.heapdump.AhatClassObj;
20 import com.android.ahat.heapdump.AhatInstance;
21 import com.android.ahat.heapdump.AhatSnapshot;
22 import com.android.ahat.heapdump.Diff;
23 import com.android.ahat.heapdump.FieldValue;
24 import com.android.ahat.heapdump.HprofFormatException;
25 import com.android.ahat.heapdump.Parser;
26 import com.android.ahat.heapdump.Reachability;
27 import com.android.ahat.heapdump.Site;
28 import com.android.ahat.heapdump.Value;
29 import com.android.ahat.proguard.ProguardMap;
30 import java.io.ByteArrayOutputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.InputStreamReader;
34 import java.nio.ByteBuffer;
35 import java.text.ParseException;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.List;
39 import java.util.Objects;
40 
41 /**
42  * The TestDump class is used to get the current and baseline AhatSnapshots
43  * for heap dumps generated by the test-dump program that are stored as
44  * resources in this jar file.
45  */
46 public class TestDump {
47   // It can take on the order of a second to parse and process test dumps.
48   // To avoid repeating this overhead for each test case, we provide a way to
49   // cache loaded instance of TestDump and reuse it when possible. In theory
50   // the test cases should not be able to modify the cached snapshot in a way
51   // that is visible to other test cases.
52   private static List<TestDump> mCachedTestDumps = new ArrayList<TestDump>();
53 
54   // The name of the resources this test dump is loaded from.
55   private String mHprofResource;
56   private String mHprofBaseResource;
57   private String mMapResource;
58   private Reachability mRetained;
59 
60   // If the test dump fails to load the first time, it will likely fail every
61   // other test we try. Rather than having to wait a potentially very long
62   // time for test dump loading to fail over and over again, record when it
63   // fails and don't try to load it again.
64   private boolean mTestDumpFailed = true;
65 
66   // The loaded heap dumps.
67   private AhatSnapshot mSnapshot;
68   private AhatSnapshot mBaseline;
69 
70   // Cached reference to the 'Main' class object in the snapshot and baseline
71   // heap dumps.
72   private AhatClassObj mMain;
73   private AhatClassObj mBaselineMain;
74 
75   /**
76    * Read the named resource into a ByteBuffer.
77    */
dataBufferFromResource(String name)78   private static ByteBuffer dataBufferFromResource(String name) throws IOException {
79     ClassLoader loader = TestDump.class.getClassLoader();
80     InputStream is = loader.getResourceAsStream(name);
81     ByteArrayOutputStream baos = new ByteArrayOutputStream();
82     byte[] buf = new byte[4096];
83     int read;
84     while ((read = is.read(buf)) != -1) {
85       baos.write(buf, 0, read);
86     }
87     return ByteBuffer.wrap(baos.toByteArray());
88   }
89 
90   /**
91    * Create a TestDump instance.
92    * The load() method should be called to load and process the heap dumps.
93    * The files are specified as names of resources compiled into the jar file.
94    * The baseline resouce may be null to indicate that no diffing should be
95    * performed.
96    * The map resource may be null to indicate no proguard map will be used.
97    *
98    */
TestDump(String hprofResource, String hprofBaseResource, String mapResource, Reachability retained)99   private TestDump(String hprofResource,
100                    String hprofBaseResource,
101                    String mapResource,
102                    Reachability retained) {
103     mHprofResource = hprofResource;
104     mHprofBaseResource = hprofBaseResource;
105     mMapResource = mapResource;
106     mRetained = retained;
107   }
108 
109   /**
110    * Load the heap dumps for this TestDump.
111    * An IOException is thrown if there is a failure reading the hprof files or
112    * the proguard map.
113    */
load()114   private void load() throws IOException {
115     ProguardMap map = new ProguardMap();
116     if (mMapResource != null) {
117       try {
118         ClassLoader loader = TestDump.class.getClassLoader();
119         InputStream is = loader.getResourceAsStream(mMapResource);
120         map.readFromReader(new InputStreamReader(is));
121       } catch (ParseException e) {
122         throw new IOException("Unable to load proguard map", e);
123       }
124     }
125 
126     try {
127       ByteBuffer hprof = dataBufferFromResource(mHprofResource);
128       mSnapshot = new Parser(hprof).map(map).retained(mRetained).parse();
129       mMain = findClass(mSnapshot, "Main");
130       assert(mMain != null);
131     } catch (HprofFormatException e) {
132       throw new IOException("Unable to parse heap dump", e);
133     }
134 
135     if (mHprofBaseResource != null) {
136       try {
137         ByteBuffer hprofBase = dataBufferFromResource(mHprofBaseResource);
138         mBaseline = new Parser(hprofBase).map(map).retained(mRetained).parse();
139         mBaselineMain = findClass(mBaseline, "Main");
140         assert(mBaselineMain != null);
141       } catch (HprofFormatException e) {
142         throw new IOException("Unable to parse base heap dump", e);
143       }
144       Diff.snapshots(mSnapshot, mBaseline);
145     }
146 
147     mTestDumpFailed = false;
148   }
149 
150   /**
151    * Get the AhatSnapshot for the test dump program.
152    */
getAhatSnapshot()153   public AhatSnapshot getAhatSnapshot() {
154     return mSnapshot;
155   }
156 
157   /**
158    * Get the baseline AhatSnapshot for the test dump program.
159    */
getBaselineAhatSnapshot()160   public AhatSnapshot getBaselineAhatSnapshot() {
161     return mBaseline;
162   }
163 
164   /**
165    * Returns the value of a field in the DumpedStuff instance in the
166    * snapshot for the test-dump program.
167    */
getDumpedValue(String name)168   public Value getDumpedValue(String name) {
169     return getDumpedValue(name, mMain);
170   }
171 
172   /**
173    * Returns the value of a field in the DumpedStuff instance in the
174    * baseline snapshot for the test-dump program.
175    */
getBaselineDumpedValue(String name)176   public Value getBaselineDumpedValue(String name) {
177     return getDumpedValue(name, mBaselineMain);
178   }
179 
180   /**
181    * Returns the value of a field in the DumpedStuff instance given the Main
182    * class object for the snapshot.
183    */
getDumpedValue(String name, AhatClassObj main)184   private static Value getDumpedValue(String name, AhatClassObj main) {
185     AhatInstance stuff = null;
186     for (FieldValue field : main.getStaticFieldValues()) {
187       if ("stuff".equals(field.name)) {
188         stuff = field.value.asAhatInstance();
189       }
190     }
191     return stuff.getField(name);
192   }
193 
194   /**
195    * Returns a class object in the given heap dump whose name matches the
196    * given name, or null if no such class object could be found.
197    */
findClass(AhatSnapshot snapshot, String name)198   private static AhatClassObj findClass(AhatSnapshot snapshot, String name) {
199     Site root = snapshot.getRootSite();
200     Collection<AhatInstance> classes = new ArrayList<AhatInstance>();
201     root.getObjects(null, "java.lang.Class", classes);
202     for (AhatInstance inst : classes) {
203       if (inst.isClassObj()) {
204         AhatClassObj cls = inst.asClassObj();
205         if (name.equals(cls.getName())) {
206           return cls;
207         }
208       }
209     }
210     return null;
211   }
212 
213   /**
214    * Returns a class object in the heap dump whose name matches the given
215    * name, or null if no such class object could be found.
216    */
findClass(String name)217   public AhatClassObj findClass(String name) {
218     return findClass(mSnapshot, name);
219   }
220 
221   /**
222    * Returns the value of a non-primitive field in the DumpedStuff instance in
223    * the snapshot for the test-dump program.
224    */
getDumpedAhatInstance(String name)225   public AhatInstance getDumpedAhatInstance(String name) {
226     Value value = getDumpedValue(name);
227     return value == null ? null : value.asAhatInstance();
228   }
229 
230   /**
231    * Returns the value of a non-primitive field in the DumpedStuff instance in
232    * the baseline snapshot for the test-dump program.
233    */
getBaselineDumpedAhatInstance(String name)234   public AhatInstance getBaselineDumpedAhatInstance(String name) {
235     Value value = getBaselineDumpedValue(name);
236     return value == null ? null : value.asAhatInstance();
237   }
238 
239   /**
240    * Get the default (cached) test dump.
241    * An IOException is thrown if there is an error reading the test dump hprof
242    * file.
243    * To improve performance, this returns a cached instance of the TestDump
244    * when possible.
245    */
getTestDump()246   public static synchronized TestDump getTestDump() throws IOException {
247     return getTestDump("test-dump.hprof",
248                        "test-dump-base.hprof",
249                        "test-dump.map",
250                        Reachability.STRONG);
251   }
252 
253   /**
254    * Get a (cached) test dump.
255    * @param hprof - The string resouce name of the hprof file.
256    * @param base - The string resouce name of the baseline hprof, may be null.
257    * @param map - The string resouce name of the proguard map, may be null.
258    * @param retained the weakest reachability of instances to treat as retained.
259    * An IOException is thrown if there is an error reading the test dump hprof
260    * file.
261    * To improve performance, this returns a cached instance of the TestDump
262    * when possible.
263    */
getTestDump(String hprof, String base, String map, Reachability retained)264   public static synchronized TestDump getTestDump(String hprof,
265                                                   String base,
266                                                   String map,
267                                                   Reachability retained)
268     throws IOException {
269     for (TestDump loaded : mCachedTestDumps) {
270       if (Objects.equals(loaded.mHprofResource, hprof)
271           && Objects.equals(loaded.mHprofBaseResource, base)
272           && Objects.equals(loaded.mMapResource, map)
273           && Objects.equals(loaded.mRetained, retained)) {
274         if (loaded.mTestDumpFailed) {
275           throw new IOException("Test dump failed before, assuming it will again");
276         }
277         return loaded;
278       }
279     }
280 
281     TestDump dump = new TestDump(hprof, base, map, retained);
282     mCachedTestDumps.add(dump);
283     dump.load();
284     return dump;
285   }
286 }
287