VmTraceParser.java
/*
* Copyright 2023 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 androidx.benchmark.vmtrace;
import androidx.annotation.NonNull;
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.nio.charset.Charset;
class VmTraceParser {
static final Charset CHARSET_US_ASCII = Charset.forName("US-ASCII");
static final Charset CHARSET_UTF_8 = Charset.forName("UTF-8");
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 final File mTraceFile;
final VmTraceHandler mTraceDataHandler;
int mVersion;
private VmClockType mVmClockType;
VmTraceParser(@NonNull File traceFile, @NonNull VmTraceHandler traceHandler) {
if (!traceFile.exists()) {
throw new IllegalArgumentException(
"Trace file " + traceFile.getAbsolutePath() + " does not exist.");
}
mTraceFile = traceFile;
mTraceDataHandler = traceHandler;
}
public void parse() throws IOException {
ByteBuffer buffer;
if (isStreamingTrace(mTraceFile)) {
throw new UnsupportedOperationException("Streaming traces are not supported.");
} else {
long headerLength = parseHeader(mTraceFile);
buffer = ByteBufferUtil.mapFile(mTraceFile, headerLength, ByteOrder.LITTLE_ENDIAN);
}
parseData(buffer);
}
private static boolean isStreamingTrace(File file) throws IOException {
BufferedReader in =
new BufferedReader(
new InputStreamReader(new FileInputStream(file), CHARSET_US_ASCII));
try {
String firstLine = in.readLine();
if (firstLine != null && firstLine.startsWith(HEADER_SECTION_VERSION)) {
// Trace file not obtained by using streaming mode
return false;
}
} finally {
in.close();
}
return true;
}
// The values of PARSE_METHODS, PARSE_THREADS and PARSE_SUMMARY match the corresponding value
// in trace files,
// which are written by Android Runtime (ART) code. Please do not change their values without
// a matching change to ART.
private static final int PARSE_VERSION = 0;
private static final int PARSE_METHODS = 1;
private static final int PARSE_THREADS = 2;
private static final int PARSE_OPTIONS = 4;
/** Parses the trace file header and returns the offset in the file where the header ends. */
long parseHeader(File f) throws IOException {
long offset = 0;
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(f), CHARSET_UTF_8));
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().
// We can't use line.length() as unicode characters can be represented by more
// than 1 byte.
offset += line.getBytes(CHARSET_UTF_8).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:
mVersion = Integer.decode(line);
mTraceDataHandler.setVersion(mVersion);
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 {
in.close();
} catch (IOException e) {
// cannot happen
}
}
}
return offset;
}
/** Parses trace option formatted as a key value pair. */
void parseOption(String line) {
String[] tokens = line.split("=");
if (tokens.length == 2) {
String key = tokens[0];
String value = tokens[1];
mTraceDataHandler.setProperty(key, value);
if (key.equals(KEY_CLOCK)) {
if (value.equals("thread-cpu")) {
mVmClockType = VmClockType.THREAD_CPU;
} else if (value.equals("wall")) {
mVmClockType = VmClockType.WALL;
} else if (value.equals("dual")) {
mVmClockType = VmClockType.DUAL;
}
}
}
}
/** 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();
mTraceDataHandler.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];
if (tokens.length >= 5) {
pathname = tokens[4];
}
} else {
pathname = tokens[2];
lineNumber = Integer.decode(tokens[3]);
}
}
mTraceDataHandler.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 = mVersion;
while (buffer.hasRemaining()) {
int threadTime;
int globalTime;
int positionStart = buffer.position();
threadId = version == 1 ? buffer.get() : buffer.getShort();
methodId = buffer.getInt();
switch (mVmClockType) {
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 &= ~0x03;
mTraceDataHandler.addMethodAction(
threadId, unsignedIntToLong(methodId), methodAction, threadTime, globalTime);
}
}
private static long unsignedIntToLong(int value) {
return value & 0xffffffffL;
}
/**
* 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) {
validateMagic(buffer.getInt());
// read version
int version = buffer.getShort();
if (version != mVersion) {
String msg =
String.format(
"Error: version number mismatch; got %d in data header but %d in "
+ "options\n",
version, mVersion);
throw new RuntimeException(msg);
}
validateTraceVersion(version);
// read offset
int offsetToData = buffer.getShort() - 16;
// read startWhen
mTraceDataHandler.setStartTimeUs(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;
}
static void validateTraceVersion(int version) {
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);
}
}
static void validateMagic(int magic) {
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);
}
}
}