1/*
2 * Copyright (C) 2010 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 dalvik.system.profiler;
18
19import java.io.BufferedInputStream;
20import java.io.DataInputStream;
21import java.io.EOFException;
22import java.io.IOException;
23import java.io.InputStream;
24import java.util.Date;
25import java.util.HashMap;
26import java.util.Map;
27
28/**
29 * <pre>   {@code
30 * BinaryHprofReader reader = new BinaryHprofReader(new BufferedInputStream(inputStream));
31 * reader.setStrict(false); // for RI compatability
32 * reader.read();
33 * inputStream.close();
34 * reader.getVersion();
35 * reader.getHprofData();
36 * }</pre>
37 */
38public final class BinaryHprofReader {
39
40    private static final boolean TRACE = false;
41
42    private final DataInputStream in;
43
44    /**
45     * By default we try to strictly validate rules followed by
46     * our HprofWriter. For example, every end thread is preceded
47     * by a matching start thread.
48     */
49    private boolean strict = true;
50
51    /**
52     * version string from header after read has been performed,
53     * otherwise null. nullness used to detect if callers try to
54     * access data before read is called.
55     */
56    private String version;
57
58    private final Map<HprofData.StackTrace, int[]> stackTraces
59            = new HashMap<HprofData.StackTrace, int[]>();
60
61    private final HprofData hprofData = new HprofData(stackTraces);
62
63    private final Map<Integer, String> idToString = new HashMap<Integer, String>();
64    private final Map<Integer, String> idToClassName = new HashMap<Integer, String>();
65    private final Map<Integer, StackTraceElement> idToStackFrame
66            = new HashMap<Integer, StackTraceElement>();
67    private final Map<Integer, HprofData.StackTrace> idToStackTrace
68            = new HashMap<Integer, HprofData.StackTrace>();
69
70    /**
71     * Creates a BinaryHprofReader around the specified {@code
72     * inputStream}
73     */
74    public BinaryHprofReader(InputStream inputStream) throws IOException {
75        this.in = new DataInputStream(inputStream);
76    }
77
78    public boolean getStrict () {
79        return strict;
80    }
81
82    public void setStrict (boolean strict) {
83        if (version != null) {
84            throw new IllegalStateException("cannot set strict after read()");
85        }
86        this.strict = strict;
87    }
88
89    /**
90     * throws IllegalStateException if read() has not been called.
91     */
92    private void checkRead() {
93        if (version == null) {
94            throw new IllegalStateException("data access before read()");
95        }
96    }
97
98    public String getVersion() {
99        checkRead();
100        return version;
101    }
102
103    public HprofData getHprofData() {
104        checkRead();
105        return hprofData;
106    }
107
108    /**
109     * Read the hprof header and records from the input
110     */
111    public void read() throws IOException {
112        parseHeader();
113        parseRecords();
114    }
115
116    private void parseHeader() throws IOException {
117        if (TRACE) {
118            System.out.println("hprofTag=HEADER");
119        }
120        parseVersion();
121        parseIdSize();
122        parseTime();
123    }
124
125    private void parseVersion() throws IOException {
126        String version = BinaryHprof.readMagic(in);
127        if (version == null) {
128            throw new MalformedHprofException("Could not find HPROF version");
129        }
130        if (TRACE) {
131            System.out.println("\tversion=" + version);
132        }
133        this.version = version;
134    }
135
136    private void parseIdSize() throws IOException {
137        int idSize = in.readInt();
138        if (TRACE) {
139            System.out.println("\tidSize=" + idSize);
140        }
141        if (idSize != BinaryHprof.ID_SIZE) {
142            throw new MalformedHprofException("Unsupported identifier size: " + idSize);
143        }
144    }
145
146    private void parseTime() throws IOException {
147        long time = in.readLong();
148        if (TRACE) {
149            System.out.println("\ttime=" + Long.toHexString(time) + " " + new Date(time));
150        }
151        hprofData.setStartMillis(time);
152    }
153
154    private void parseRecords() throws IOException {
155        while (parseRecord()) {
156            ;
157        }
158    }
159
160    /**
161     * Read and process the next record. Returns true if a
162     * record was handled, false on EOF.
163     */
164    private boolean parseRecord() throws IOException {
165        int tagOrEOF = in.read();
166        if (tagOrEOF == -1) {
167            return false;
168        }
169        byte tag = (byte) tagOrEOF;
170        int timeDeltaInMicroseconds = in.readInt();
171        int recordLength = in.readInt();
172        BinaryHprof.Tag hprofTag = BinaryHprof.Tag.get(tag);
173        if (TRACE) {
174            System.out.println("hprofTag=" + hprofTag);
175        }
176        if (hprofTag == null) {
177            skipRecord(hprofTag, recordLength);
178            return true;
179        }
180        String error = hprofTag.checkSize(recordLength);
181        if (error != null) {
182            throw new MalformedHprofException(error);
183        }
184        switch (hprofTag) {
185            case CONTROL_SETTINGS:
186                parseControlSettings();
187                return true;
188
189            case STRING_IN_UTF8:
190                parseStringInUtf8(recordLength);
191                return true;
192
193            case START_THREAD:
194                parseStartThread();
195                return true;
196            case END_THREAD:
197                parseEndThread();
198                return true;
199
200            case LOAD_CLASS:
201                parseLoadClass();
202                return true;
203            case STACK_FRAME:
204                parseStackFrame();
205                return true;
206            case STACK_TRACE:
207                parseStackTrace(recordLength);
208                return true;
209
210            case CPU_SAMPLES:
211                parseCpuSamples(recordLength);
212                return true;
213
214            case UNLOAD_CLASS:
215            case ALLOC_SITES:
216            case HEAP_SUMMARY:
217            case HEAP_DUMP:
218            case HEAP_DUMP_SEGMENT:
219            case HEAP_DUMP_END:
220            default:
221                skipRecord(hprofTag, recordLength);
222                return true;
223        }
224    }
225
226    private void skipRecord(BinaryHprof.Tag hprofTag, long recordLength) throws IOException {
227        if (TRACE) {
228            System.out.println("\tskipping recordLength=" + recordLength);
229        }
230        long skipped = in.skip(recordLength);
231        if (skipped != recordLength) {
232            throw new EOFException("Expected to skip " + recordLength
233                                   + " bytes but only skipped " + skipped + " bytes");
234        }
235    }
236
237    private void parseControlSettings() throws IOException {
238        int flags = in.readInt();
239        short depth = in.readShort();
240        if (TRACE) {
241            System.out.println("\tflags=" + Integer.toHexString(flags));
242            System.out.println("\tdepth=" + depth);
243        }
244        hprofData.setFlags(flags);
245        hprofData.setDepth(depth);
246    }
247
248    private void parseStringInUtf8(int recordLength) throws IOException {
249        int stringId = in.readInt();
250        byte[] bytes = new byte[recordLength - BinaryHprof.ID_SIZE];
251        readFully(in, bytes);
252        String string = new String(bytes, "UTF-8");
253        if (TRACE) {
254            System.out.println("\tstring=" + string);
255        }
256        String old = idToString.put(stringId, string);
257        if (old != null) {
258            throw new MalformedHprofException("Duplicate string id: " + stringId);
259        }
260    }
261
262    private static void readFully(InputStream in, byte[] dst) throws IOException {
263        int offset = 0;
264        int byteCount = dst.length;
265        while (byteCount > 0) {
266            int bytesRead = in.read(dst, offset, byteCount);
267            if (bytesRead < 0) {
268                throw new EOFException();
269            }
270            offset += bytesRead;
271            byteCount -= bytesRead;
272        }
273    }
274
275    private void parseLoadClass() throws IOException {
276        int classId = in.readInt();
277        int classObjectId = readId();
278        // serial number apparently not a stack trace id. (int vs ID)
279        // we don't use this field.
280        int stackTraceSerialNumber = in.readInt();
281        String className = readString();
282        if (TRACE) {
283            System.out.println("\tclassId=" + classId);
284            System.out.println("\tclassObjectId=" + classObjectId);
285            System.out.println("\tstackTraceSerialNumber=" + stackTraceSerialNumber);
286            System.out.println("\tclassName=" + className);
287        }
288        String old = idToClassName.put(classId, className);
289        if (old != null) {
290            throw new MalformedHprofException("Duplicate class id: " + classId);
291        }
292    }
293
294    private int readId() throws IOException {
295        return in.readInt();
296    }
297
298    private String readString() throws IOException {
299        int id = readId();
300        if (id == 0) {
301            return null;
302        }
303        String string = idToString.get(id);
304        if (string == null) {
305            throw new MalformedHprofException("Unknown string id " + id);
306        }
307        return string;
308    }
309
310    private String readClass() throws IOException {
311        int id = readId();
312        String string = idToClassName.get(id);
313        if (string == null) {
314            throw new MalformedHprofException("Unknown class id " + id);
315        }
316        return string;
317    }
318
319    private void parseStartThread() throws IOException {
320        int threadId = in.readInt();
321        int objectId = readId();
322        // stack trace where thread was created.
323        // serial number apparently not a stack trace id. (int vs ID)
324        // we don't use this field.
325        int stackTraceSerialNumber = in.readInt();
326        String threadName = readString();
327        String groupName = readString();
328        String parentGroupName = readString();
329        if (TRACE) {
330            System.out.println("\tthreadId=" + threadId);
331            System.out.println("\tobjectId=" + objectId);
332            System.out.println("\tstackTraceSerialNumber=" + stackTraceSerialNumber);
333            System.out.println("\tthreadName=" + threadName);
334            System.out.println("\tgroupName=" + groupName);
335            System.out.println("\tparentGroupName=" + parentGroupName);
336        }
337        HprofData.ThreadEvent event
338                = HprofData.ThreadEvent.start(objectId, threadId,
339                                              threadName, groupName, parentGroupName);
340        hprofData.addThreadEvent(event);
341    }
342
343    private void parseEndThread() throws IOException {
344        int threadId = in.readInt();
345        if (TRACE) {
346            System.out.println("\tthreadId=" + threadId);
347        }
348        HprofData.ThreadEvent event = HprofData.ThreadEvent.end(threadId);
349        hprofData.addThreadEvent(event);
350    }
351
352    private void parseStackFrame() throws IOException {
353        int stackFrameId = readId();
354        String methodName = readString();
355        String methodSignature = readString();
356        String file = readString();
357        String className = readClass();
358        int line = in.readInt();
359        if (TRACE) {
360            System.out.println("\tstackFrameId=" + stackFrameId);
361            System.out.println("\tclassName=" + className);
362            System.out.println("\tmethodName=" + methodName);
363            System.out.println("\tmethodSignature=" + methodSignature);
364            System.out.println("\tfile=" + file);
365            System.out.println("\tline=" + line);
366        }
367        StackTraceElement stackFrame = new StackTraceElement(className, methodName, file, line);
368        StackTraceElement old = idToStackFrame.put(stackFrameId, stackFrame);
369        if (old != null) {
370            throw new MalformedHprofException("Duplicate stack frame id: " + stackFrameId);
371        }
372    }
373
374    private void parseStackTrace(int recordLength) throws IOException {
375        int stackTraceId = in.readInt();
376        int threadId = in.readInt();
377        int frames = in.readInt();
378        if (TRACE) {
379            System.out.println("\tstackTraceId=" + stackTraceId);
380            System.out.println("\tthreadId=" + threadId);
381            System.out.println("\tframes=" + frames);
382        }
383        int expectedLength = 4 + 4 + 4 + (frames * BinaryHprof.ID_SIZE);
384        if (recordLength != expectedLength) {
385            throw new MalformedHprofException("Expected stack trace record of size "
386                                              + expectedLength
387                                              + " based on number of frames but header "
388                                              + "specified a length of  " + recordLength);
389        }
390        StackTraceElement[] stackFrames = new StackTraceElement[frames];
391        for (int i = 0; i < frames; i++) {
392            int stackFrameId = readId();
393            StackTraceElement stackFrame = idToStackFrame.get(stackFrameId);
394            if (TRACE) {
395                System.out.println("\tstackFrameId=" + stackFrameId);
396                System.out.println("\tstackFrame=" + stackFrame);
397            }
398            if (stackFrame == null) {
399                throw new MalformedHprofException("Unknown stack frame id " + stackFrameId);
400            }
401            stackFrames[i] = stackFrame;
402        }
403
404        HprofData.StackTrace stackTrace
405                = new HprofData.StackTrace(stackTraceId, threadId, stackFrames);
406        if (strict) {
407            hprofData.addStackTrace(stackTrace, new int[1]);
408        } else {
409            // The RI can have duplicate stacks, presumably they
410            // have a minor race if two samples with the same
411            // stack are taken around the same time. if we have a
412            // duplicate, just skip adding it to hprofData, but
413            // register it locally in idToStackFrame. if it seen
414            // in CPU_SAMPLES, we will find a StackTrace is equal
415            // to the first, so they will share a countCell.
416            int[] countCell = stackTraces.get(stackTrace);
417            if (countCell == null) {
418                hprofData.addStackTrace(stackTrace, new int[1]);
419            }
420        }
421
422        HprofData.StackTrace old = idToStackTrace.put(stackTraceId, stackTrace);
423        if (old != null) {
424            throw new MalformedHprofException("Duplicate stack trace id: " + stackTraceId);
425        }
426
427    }
428
429    private void parseCpuSamples(int recordLength) throws IOException {
430        int totalSamples = in.readInt();
431        int samplesCount = in.readInt();
432        if (TRACE) {
433            System.out.println("\ttotalSamples=" + totalSamples);
434            System.out.println("\tsamplesCount=" + samplesCount);
435        }
436        int expectedLength = 4 + 4 + (samplesCount * (4 + 4));
437        if (recordLength != expectedLength) {
438            throw new MalformedHprofException("Expected CPU samples record of size "
439                                              + expectedLength
440                                              + " based on number of samples but header "
441                                              + "specified a length of  " + recordLength);
442        }
443        int total = 0;
444        for (int i = 0; i < samplesCount; i++) {
445            int count = in.readInt();
446            int stackTraceId = in.readInt();
447            if (TRACE) {
448                System.out.println("\tcount=" + count);
449                System.out.println("\tstackTraceId=" + stackTraceId);
450            }
451            HprofData.StackTrace stackTrace = idToStackTrace.get(stackTraceId);
452            if (stackTrace == null) {
453                throw new MalformedHprofException("Unknown stack trace id " + stackTraceId);
454            }
455            if (count == 0) {
456                throw new MalformedHprofException("Zero sample count for stack trace "
457                                                  + stackTrace);
458            }
459            int[] countCell = stackTraces.get(stackTrace);
460            if (strict) {
461                if (countCell[0] != 0) {
462                    throw new MalformedHprofException("Setting sample count of stack trace "
463                                                      + stackTrace + " to " + count
464                                                      + " found it was already initialized to "
465                                                      + countCell[0]);
466                }
467            } else {
468                // Coalesce counts from duplicate stack traces.
469                // For more on this, see comments in parseStackTrace.
470                count += countCell[0];
471            }
472            countCell[0] = count;
473            total += count;
474        }
475        if (strict && totalSamples != total) {
476            throw new MalformedHprofException("Expected a total of " + totalSamples
477                                              + " samples but saw " + total);
478        }
479    }
480}
481