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