/*
* Copyright (C) 2013 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.tools.perflib.vmtrace;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.ByteBufferUtil;
import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import com.google.common.primitives.UnsignedInts;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
import java.util.Set;
public class VmTraceParser {
private static final int TRACE_MAGIC = 0x574f4c53; // 'SLOW'
private static final String HEADER_SECTION_VERSION = "*version";
private static final String HEADER_SECTION_THREADS = "*threads";
private static final String HEADER_SECTION_METHODS = "*methods";
private static final String HEADER_END = "*end";
private static final String KEY_CLOCK = "clock";
private static final String KEY_DATA_OVERFLOW = "data-file-overflow";
private static final String KEY_VM = "vm";
private final File mTraceFile;
private final VmTraceData.Builder mTraceDataBuilder;
private VmTraceData mTraceData;
public VmTraceParser(File traceFile) {
if (!traceFile.exists()) {
throw new IllegalArgumentException(
"Trace file " + traceFile.getAbsolutePath() + " does not exist.");
}
mTraceFile = traceFile;
mTraceDataBuilder = new VmTraceData.Builder();
}
public void parse() throws IOException {
long headerLength = parseHeader(mTraceFile);
ByteBuffer buffer = ByteBufferUtil.mapFile(mTraceFile, headerLength, ByteOrder.LITTLE_ENDIAN);
parseData(buffer);
computeTimingStatistics();
}
public VmTraceData getTraceData() {
if (mTraceData == null) {
mTraceData = mTraceDataBuilder.build();
}
return mTraceData;
}
static final int PARSE_VERSION = 0;
static final int PARSE_THREADS = 1;
static final int PARSE_METHODS = 2;
static final int PARSE_OPTIONS = 4;
/** Parses the trace file header and returns the offset in the file where the header ends. */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
long parseHeader(File f) throws IOException {
long offset = 0;
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(f), Charsets.US_ASCII));
int mode = PARSE_VERSION;
String line;
while (true) {
line = in.readLine();
if (line == null) {
throw new IOException("Key section does not have an *end marker");
}
// Calculate how much we have read from the file so far. The
// extra byte is for the line ending not included by readLine().
offset += line.length() + 1;
if (line.startsWith("*")) {
if (line.equals(HEADER_SECTION_VERSION)) {
mode = PARSE_VERSION;
continue;
}
if (line.equals(HEADER_SECTION_THREADS)) {
mode = PARSE_THREADS;
continue;
}
if (line.equals(HEADER_SECTION_METHODS)) {
mode = PARSE_METHODS;
continue;
}
if (line.equals(HEADER_END)) {
break;
}
}
switch (mode) {
case PARSE_VERSION:
mTraceDataBuilder.setVersion(Integer.decode(line));
mode = PARSE_OPTIONS;
break;
case PARSE_THREADS:
parseThread(line);
break;
case PARSE_METHODS:
parseMethod(line);
break;
case PARSE_OPTIONS:
parseOption(line);
break;
}
}
} finally {
if (in != null) {
try {
Closeables.close(in, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
}
}
return offset;
}
/** Parses trace option formatted as a key value pair. */
private void parseOption(String line) {
String[] tokens = line.split("=");
if (tokens.length == 2) {
String key = tokens[0];
String value = tokens[1];
if (key.equals(KEY_CLOCK)) {
if (value.equals("thread-cpu")) {
mTraceDataBuilder.setVmClockType(VmTraceData.VmClockType.THREAD_CPU);
} else if (value.equals("wall")) {
mTraceDataBuilder.setVmClockType(VmTraceData.VmClockType.WALL);
} else if (value.equals("dual")) {
mTraceDataBuilder.setVmClockType(VmTraceData.VmClockType.DUAL);
}
} else if (key.equals(KEY_DATA_OVERFLOW)) {
mTraceDataBuilder.setDataFileOverflow(Boolean.parseBoolean(value));
} else if (key.equals(KEY_VM)) {
mTraceDataBuilder.setVm(value);
} else {
mTraceDataBuilder.setProperty(key, value);
}
}
}
/** Parses thread information comprising an integer id and the thread name */
private void parseThread(String line) {
int index = line.indexOf('\t');
if (index < 0) {
return;
}
try {
int id = Integer.decode(line.substring(0, index));
String name = line.substring(index).trim();
mTraceDataBuilder.addThread(id, name);
} catch (NumberFormatException ignored) {
}
}
void parseMethod(String line) {
String[] tokens = line.split("\t");
long id;
try {
id = Long.decode(tokens[0]);
} catch (NumberFormatException e) {
return;
}
String className = tokens[1];
String methodName = null;
String signature = null;
String pathname = null;
int lineNumber = -1;
if (tokens.length == 6) {
methodName = tokens[2];
signature = tokens[3];
pathname = tokens[4];
lineNumber = Integer.decode(tokens[5]);
pathname = constructPathname(className, pathname);
} else if (tokens.length > 2) {
if (tokens[3].startsWith("(")) {
methodName = tokens[2];
signature = tokens[3];
} else {
pathname = tokens[2];
lineNumber = Integer.decode(tokens[3]);
}
}
mTraceDataBuilder.addMethod(id, new MethodInfo(id, className, methodName, signature,
pathname, lineNumber));
}
private String constructPathname(String className, String pathname) {
int index = className.lastIndexOf('/');
if (index > 0 && index < className.length() - 1 && pathname.endsWith(".java")) {
pathname = className.substring(0, index + 1) + pathname;
}
return pathname;
}
/**
* Parses the data section of the trace. The data section comprises of a header followed
* by a list of records.
*
* All values are stored in little-endian order.
*/
private void parseData(ByteBuffer buffer) {
int recordSize = readDataFileHeader(buffer);
parseMethodTraceData(buffer, recordSize);
}
/**
* Parses the list of records corresponding to each trace event (method entry, exit, ...)
* Record format v1:
* u1 thread ID
* u4 method ID | method action
* u4 time delta since start, in usec
*
* Record format v2:
* u2 thread ID
* u4 method ID | method action
* u4 time delta since start, in usec
*
* Record format v3:
* u2 thread ID
* u4 method ID | method action
* u4 time delta since start, in usec
* u4 wall time since start, in usec (when clock == "dual" only)
*
* 32 bits of microseconds is 70 minutes.
*/
private void parseMethodTraceData(ByteBuffer buffer, int recordSize) {
int methodId;
int threadId;
int version = mTraceDataBuilder.getVersion();
VmTraceData.VmClockType vmClockType = mTraceDataBuilder.getVmClockType();
while (buffer.hasRemaining()) {
int threadTime;
int globalTime;
int positionStart = buffer.position();
threadId = version == 1 ? buffer.get() : buffer.getShort();
methodId = buffer.getInt();
switch (vmClockType) {
case WALL:
globalTime = buffer.getInt();
threadTime = globalTime;
break;
case DUAL:
threadTime = buffer.getInt();
globalTime = buffer.getInt();
break;
case THREAD_CPU:
default:
threadTime = buffer.getInt();
globalTime = threadTime;
break;
}
int positionEnd = buffer.position();
int bytesRead = positionEnd - positionStart;
if (bytesRead < recordSize) {
buffer.position(positionEnd + (recordSize - bytesRead));
}
int action = methodId & 0x03;
TraceAction methodAction;
switch (action) {
case 0:
methodAction = TraceAction.METHOD_ENTER;
break;
case 1:
methodAction = TraceAction.METHOD_EXIT;
break;
case 2:
methodAction = TraceAction.METHOD_EXIT_UNROLL;
break;
default:
throw new RuntimeException(
"Invalid trace action, expected one of method entry, exit or unroll.");
}
methodId = methodId & ~0x03;
mTraceDataBuilder.addMethodAction(threadId, UnsignedInts.toLong(methodId), methodAction,
threadTime, globalTime);
}
}
/**
* Parses the data header with the following format:
* u4 magic ('SLOW')
* u2 version
* u2 offset to data
* u8 start date/time in usec
* u2 record size in bytes (version >= 2 only)
* ... padding to 32 bytes
* @param buffer byte buffer pointing to the header
* @return record size for each data entry following the header
*/
private int readDataFileHeader(ByteBuffer buffer) {
int magic = buffer.getInt();
if (magic != TRACE_MAGIC) {
String msg = String.format("Error: magic number mismatch; got 0x%x, expected 0x%x\n",
magic, TRACE_MAGIC);
throw new RuntimeException(msg);
}
// read version
int version = buffer.getShort();
if (version != mTraceDataBuilder.getVersion()) {
String msg = String.format(
"Error: version number mismatch; got %d in data header but %d in options\n",
version, mTraceData.getVersion());
throw new RuntimeException(msg);
}
if (version < 1 || version > 3) {
String msg = String.format(
"Error: unsupported trace version number %d. "
+ "Please use a newer version of TraceView to read this file.",
version);
throw new RuntimeException(msg);
}
// read offset
int offsetToData = buffer.getShort() - 16;
// read startWhen
buffer.getLong();
// read record size
int recordSize;
switch (version) {
case 1:
recordSize = 9;
break;
case 2:
recordSize = 10;
break;
default:
recordSize = buffer.getShort();
offsetToData -= 2;
break;
}
// Skip over offsetToData bytes
while (offsetToData-- > 0) {
buffer.get();
}
return recordSize;
}
private void computeTimingStatistics() {
VmTraceData data = getTraceData();
ProfileDataBuilder builder = new ProfileDataBuilder();
for (ThreadInfo thread : data.getThreads()) {
Call c = thread.getTopLevelCall();
if (c == null) {
continue;
}
builder.computeCallStats(c, null, thread);
}
for (Long methodId : builder.getMethodsWithProfileData()) {
MethodInfo method = data.getMethod(methodId);
method.setProfileData(builder.getProfileData(methodId));
}
}
private static class ProfileDataBuilder {
/** Maps method ids to their corresponding method data builders */
private final Map<Long, MethodProfileData.Builder> mBuilderMap = Maps.newHashMap();
public void computeCallStats(Call c, Call parent, ThreadInfo thread) {
long methodId = c.getMethodId();
MethodProfileData.Builder builder = getProfileDataBuilder(methodId);
builder.addCallTime(c, parent, thread);
builder.incrementInvocationCount(c, parent, thread);
if (c.isRecursive()) {
builder.setRecursive();
}
for (Call callee: c.getCallees()) {
computeCallStats(callee, c, thread);
}
}
@NonNull
private MethodProfileData.Builder getProfileDataBuilder(long methodId) {
MethodProfileData.Builder builder = mBuilderMap.get(methodId);
if (builder == null) {
builder = new MethodProfileData.Builder();
mBuilderMap.put(methodId, builder);
}
return builder;
}
public Set<Long> getMethodsWithProfileData() {
return mBuilderMap.keySet();
}
public MethodProfileData getProfileData(Long methodId) {
return mBuilderMap.get(methodId).build();
}
}
}