blob: 9bd688243d6c8998d4137fb846b8746847038f46 [file] [log] [blame]
/*
* Copyright (C) 2006 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.traceview;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.BufferUnderflowException;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DmTraceReader extends TraceReader {
private static final int TRACE_MAGIC = 0x574f4c53;
private static final int METHOD_TRACE_ENTER = 0x00; // method entry
private static final int METHOD_TRACE_EXIT = 0x01; // method exit
private static final int METHOD_TRACE_UNROLL = 0x02; // method exited by exception unrolling
// When in dual clock mode, we report that a context switch has occurred
// when skew between the real time and thread cpu clocks is more than this
// many microseconds.
private static final long MIN_CONTEXT_SWITCH_TIME_USEC = 100;
private enum ClockSource {
THREAD_CPU, WALL, DUAL,
};
private int mVersionNumber;
private boolean mRegression;
private ProfileProvider mProfileProvider;
private String mTraceFileName;
private MethodData mTopLevel;
private ArrayList<Call> mCallList;
private HashMap<String, String> mPropertiesMap;
private HashMap<Integer, MethodData> mMethodMap;
private HashMap<Integer, ThreadData> mThreadMap;
private ThreadData[] mSortedThreads;
private MethodData[] mSortedMethods;
private long mTotalCpuTime;
private long mTotalRealTime;
private MethodData mContextSwitch;
private int mRecordSize;
private ClockSource mClockSource;
// A regex for matching the thread "id name" lines in the .key file
private static final Pattern mIdNamePattern = Pattern.compile("(\\d+)\t(.*)"); //$NON-NLS-1$
public DmTraceReader(String traceFileName, boolean regression) throws IOException {
mTraceFileName = traceFileName;
mRegression = regression;
mPropertiesMap = new HashMap<String, String>();
mMethodMap = new HashMap<Integer, MethodData>();
mThreadMap = new HashMap<Integer, ThreadData>();
mCallList = new ArrayList<Call>();
// Create a single top-level MethodData object to hold the profile data
// for time spent in the unknown caller.
mTopLevel = new MethodData(0, "(toplevel)");
mContextSwitch = new MethodData(-1, "(context switch)");
mMethodMap.put(0, mTopLevel);
mMethodMap.put(-1, mContextSwitch);
generateTrees();
}
void generateTrees() throws IOException {
long offset = parseKeys();
parseData(offset);
analyzeData();
}
@Override
public ProfileProvider getProfileProvider() {
if (mProfileProvider == null)
mProfileProvider = new ProfileProvider(this);
return mProfileProvider;
}
private MappedByteBuffer mapFile(String filename, long offset) throws IOException {
MappedByteBuffer buffer = null;
FileInputStream dataFile = new FileInputStream(filename);
try {
File file = new File(filename);
FileChannel fc = dataFile.getChannel();
buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset);
buffer.order(ByteOrder.LITTLE_ENDIAN);
return buffer;
} finally {
dataFile.close(); // this *also* closes the associated channel, fc
}
}
private void readDataFileHeader(MappedByteBuffer buffer) {
int magic = buffer.getInt();
if (magic != TRACE_MAGIC) {
System.err.printf(
"Error: magic number mismatch; got 0x%x, expected 0x%x\n",
magic, TRACE_MAGIC);
throw new RuntimeException();
}
// read version
int version = buffer.getShort();
if (version != mVersionNumber) {
System.err.printf(
"Error: version number mismatch; got %d in data header but %d in options\n",
version, mVersionNumber);
throw new RuntimeException();
}
if (version < 1 || version > 3) {
System.err.printf(
"Error: unsupported trace version number %d. "
+ "Please use a newer version of TraceView to read this file.", version);
throw new RuntimeException();
}
// read offset
int offsetToData = buffer.getShort() - 16;
// read startWhen
buffer.getLong();
// read record size
if (version == 1) {
mRecordSize = 9;
} else if (version == 2) {
mRecordSize = 10;
} else {
mRecordSize = buffer.getShort();
offsetToData -= 2;
}
// Skip over offsetToData bytes
while (offsetToData-- > 0) {
buffer.get();
}
}
private void parseData(long offset) throws IOException {
MappedByteBuffer buffer = mapFile(mTraceFileName, offset);
readDataFileHeader(buffer);
ArrayList<TraceAction> trace = null;
if (mClockSource == ClockSource.THREAD_CPU) {
trace = new ArrayList<TraceAction>();
}
final boolean haveThreadClock = mClockSource != ClockSource.WALL;
final boolean haveGlobalClock = mClockSource != ClockSource.THREAD_CPU;
// Parse all call records to obtain elapsed time information.
ThreadData prevThreadData = null;
for (;;) {
int threadId;
int methodId;
long threadTime, globalTime;
try {
int recordSize = mRecordSize;
if (mVersionNumber == 1) {
threadId = buffer.get();
recordSize -= 1;
} else {
threadId = buffer.getShort();
recordSize -= 2;
}
methodId = buffer.getInt();
recordSize -= 4;
switch (mClockSource) {
case WALL:
threadTime = 0;
globalTime = buffer.getInt();
recordSize -= 4;
break;
case DUAL:
threadTime = buffer.getInt();
globalTime = buffer.getInt();
recordSize -= 8;
break;
default:
case THREAD_CPU:
threadTime = buffer.getInt();
globalTime = 0;
recordSize -= 4;
break;
}
while (recordSize-- > 0) {
buffer.get();
}
} catch (BufferUnderflowException ex) {
break;
}
int methodAction = methodId & 0x03;
methodId = methodId & ~0x03;
MethodData methodData = mMethodMap.get(methodId);
if (methodData == null) {
String name = String.format("(0x%1$x)", methodId); //$NON-NLS-1$
methodData = new MethodData(methodId, name);
mMethodMap.put(methodId, methodData);
}
ThreadData threadData = mThreadMap.get(threadId);
if (threadData == null) {
String name = String.format("[%1$d]", threadId); //$NON-NLS-1$
threadData = new ThreadData(threadId, name, mTopLevel);
mThreadMap.put(threadId, threadData);
}
long elapsedGlobalTime = 0;
if (haveGlobalClock) {
if (!threadData.mHaveGlobalTime) {
threadData.mGlobalStartTime = globalTime;
threadData.mHaveGlobalTime = true;
} else {
elapsedGlobalTime = globalTime - threadData.mGlobalEndTime;
}
threadData.mGlobalEndTime = globalTime;
}
if (haveThreadClock) {
long elapsedThreadTime = 0;
if (!threadData.mHaveThreadTime) {
threadData.mThreadStartTime = threadTime;
threadData.mThreadCurrentTime = threadTime;
threadData.mHaveThreadTime = true;
} else {
elapsedThreadTime = threadTime - threadData.mThreadEndTime;
}
threadData.mThreadEndTime = threadTime;
if (!haveGlobalClock) {
// Detect context switches whenever execution appears to switch from one
// thread to another. This assumption is only valid on uniprocessor
// systems (which is why we now have a dual clock mode).
// We represent context switches in the trace by pushing a call record
// with MethodData mContextSwitch onto the stack of the previous
// thread. We arbitrarily set the start and end time of the context
// switch such that the context switch occurs in the middle of the thread
// time and itself accounts for zero thread time.
if (prevThreadData != null && prevThreadData != threadData) {
// Begin context switch from previous thread.
Call switchCall = prevThreadData.enter(mContextSwitch, trace);
switchCall.mThreadStartTime = prevThreadData.mThreadEndTime;
mCallList.add(switchCall);
// Return from context switch to current thread.
Call top = threadData.top();
if (top.getMethodData() == mContextSwitch) {
threadData.exit(mContextSwitch, trace);
long beforeSwitch = elapsedThreadTime / 2;
top.mThreadStartTime += beforeSwitch;
top.mThreadEndTime = top.mThreadStartTime;
}
}
prevThreadData = threadData;
} else {
// If we have a global clock, then we can detect context switches (or blocking
// calls or cpu suspensions or clock anomalies) by comparing global time to
// thread time for successive calls that occur on the same thread.
// As above, we represent the context switch using a special method call.
long sleepTime = elapsedGlobalTime - elapsedThreadTime;
if (sleepTime > MIN_CONTEXT_SWITCH_TIME_USEC) {
Call switchCall = threadData.enter(mContextSwitch, trace);
long beforeSwitch = elapsedThreadTime / 2;
long afterSwitch = elapsedThreadTime - beforeSwitch;
switchCall.mGlobalStartTime = globalTime - elapsedGlobalTime + beforeSwitch;
switchCall.mGlobalEndTime = globalTime - afterSwitch;
switchCall.mThreadStartTime = threadTime - afterSwitch;
switchCall.mThreadEndTime = switchCall.mThreadStartTime;
threadData.exit(mContextSwitch, trace);
mCallList.add(switchCall);
}
}
// Add thread CPU time.
Call top = threadData.top();
top.addCpuTime(elapsedThreadTime);
}
switch (methodAction) {
case METHOD_TRACE_ENTER: {
Call call = threadData.enter(methodData, trace);
if (haveGlobalClock) {
call.mGlobalStartTime = globalTime;
}
if (haveThreadClock) {
call.mThreadStartTime = threadTime;
}
mCallList.add(call);
break;
}
case METHOD_TRACE_EXIT:
case METHOD_TRACE_UNROLL: {
Call call = threadData.exit(methodData, trace);
if (call != null) {
if (haveGlobalClock) {
call.mGlobalEndTime = globalTime;
}
if (haveThreadClock) {
call.mThreadEndTime = threadTime;
}
}
break;
}
default:
throw new RuntimeException("Unrecognized method action: " + methodAction);
}
}
// Exit any pending open-ended calls.
for (ThreadData threadData : mThreadMap.values()) {
threadData.endTrace(trace);
}
// Recreate the global timeline from thread times, if needed.
if (!haveGlobalClock) {
long globalTime = 0;
prevThreadData = null;
for (TraceAction traceAction : trace) {
Call call = traceAction.mCall;
ThreadData threadData = call.getThreadData();
if (traceAction.mAction == TraceAction.ACTION_ENTER) {
long threadTime = call.mThreadStartTime;
globalTime += call.mThreadStartTime - threadData.mThreadCurrentTime;
call.mGlobalStartTime = globalTime;
if (!threadData.mHaveGlobalTime) {
threadData.mHaveGlobalTime = true;
threadData.mGlobalStartTime = globalTime;
}
threadData.mThreadCurrentTime = threadTime;
} else if (traceAction.mAction == TraceAction.ACTION_EXIT) {
long threadTime = call.mThreadEndTime;
globalTime += call.mThreadEndTime - threadData.mThreadCurrentTime;
call.mGlobalEndTime = globalTime;
threadData.mGlobalEndTime = globalTime;
threadData.mThreadCurrentTime = threadTime;
} // else, ignore ACTION_INCOMPLETE calls, nothing to do
prevThreadData = threadData;
}
}
// Finish updating all calls and calculate the total time spent.
for (int i = mCallList.size() - 1; i >= 0; i--) {
Call call = mCallList.get(i);
// Calculate exclusive real-time by subtracting inclusive real time
// accumulated by children from the total span.
long realTime = call.mGlobalEndTime - call.mGlobalStartTime;
call.mExclusiveRealTime = Math.max(realTime - call.mInclusiveRealTime, 0);
call.mInclusiveRealTime = realTime;
call.finish();
}
mTotalCpuTime = 0;
mTotalRealTime = 0;
for (ThreadData threadData : mThreadMap.values()) {
Call rootCall = threadData.getRootCall();
threadData.updateRootCallTimeBounds();
rootCall.finish();
mTotalCpuTime += rootCall.mInclusiveCpuTime;
mTotalRealTime += rootCall.mInclusiveRealTime;
}
if (mRegression) {
System.out.format("totalCpuTime %dus\n", mTotalCpuTime);
System.out.format("totalRealTime %dus\n", mTotalRealTime);
dumpThreadTimes();
dumpCallTimes();
}
}
static final int PARSE_VERSION = 0;
static final int PARSE_THREADS = 1;
static final int PARSE_METHODS = 2;
static final int PARSE_OPTIONS = 4;
long parseKeys() throws IOException {
long offset = 0;
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(
new FileInputStream(mTraceFileName), "US-ASCII"));
int mode = PARSE_VERSION;
String line = null;
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("*version")) {
mode = PARSE_VERSION;
continue;
}
if (line.equals("*threads")) {
mode = PARSE_THREADS;
continue;
}
if (line.equals("*methods")) {
mode = PARSE_METHODS;
continue;
}
if (line.equals("*end")) {
break;
}
}
switch (mode) {
case PARSE_VERSION:
mVersionNumber = 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;
}
}
} catch (FileNotFoundException ex) {
System.err.println(ex.getMessage());
} finally {
if (in != null) {
in.close();
}
}
if (mClockSource == null) {
mClockSource = ClockSource.THREAD_CPU;
}
return offset;
}
void parseOption(String line) {
String[] tokens = line.split("=");
if (tokens.length == 2) {
String key = tokens[0];
String value = tokens[1];
mPropertiesMap.put(key, value);
if (key.equals("clock")) {
if (value.equals("thread-cpu")) {
mClockSource = ClockSource.THREAD_CPU;
} else if (value.equals("wall")) {
mClockSource = ClockSource.WALL;
} else if (value.equals("dual")) {
mClockSource = ClockSource.DUAL;
}
}
}
}
void parseThread(String line) {
String idStr = null;
String name = null;
Matcher matcher = mIdNamePattern.matcher(line);
if (matcher.find()) {
idStr = matcher.group(1);
name = matcher.group(2);
}
if (idStr == null) return;
if (name == null) name = "(unknown)";
int id = Integer.decode(idStr);
mThreadMap.put(id, new ThreadData(id, name, mTopLevel));
}
void parseMethod(String line) {
String[] tokens = line.split("\t");
int id = Long.decode(tokens[0]).intValue();
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]);
}
}
mMethodMap.put(id, new MethodData(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;
}
private void analyzeData() {
final TimeBase timeBase = getPreferredTimeBase();
// Sort the threads into decreasing cpu time
Collection<ThreadData> tv = mThreadMap.values();
mSortedThreads = tv.toArray(new ThreadData[tv.size()]);
Arrays.sort(mSortedThreads, new Comparator<ThreadData>() {
@Override
public int compare(ThreadData td1, ThreadData td2) {
if (timeBase.getTime(td2) > timeBase.getTime(td1))
return 1;
if (timeBase.getTime(td2) < timeBase.getTime(td1))
return -1;
return td2.getName().compareTo(td1.getName());
}
});
// Sort the methods into decreasing inclusive time
Collection<MethodData> mv = mMethodMap.values();
MethodData[] methods;
methods = mv.toArray(new MethodData[mv.size()]);
Arrays.sort(methods, new Comparator<MethodData>() {
@Override
public int compare(MethodData md1, MethodData md2) {
if (timeBase.getElapsedInclusiveTime(md2) > timeBase.getElapsedInclusiveTime(md1))
return 1;
if (timeBase.getElapsedInclusiveTime(md2) < timeBase.getElapsedInclusiveTime(md1))
return -1;
return md1.getName().compareTo(md2.getName());
}
});
// Count the number of methods with non-zero inclusive time
int nonZero = 0;
for (MethodData md : methods) {
if (timeBase.getElapsedInclusiveTime(md) == 0)
break;
nonZero += 1;
}
// Copy the methods with non-zero time
mSortedMethods = new MethodData[nonZero];
int ii = 0;
for (MethodData md : methods) {
if (timeBase.getElapsedInclusiveTime(md) == 0)
break;
md.setRank(ii);
mSortedMethods[ii++] = md;
}
// Let each method analyze its profile data
for (MethodData md : mSortedMethods) {
md.analyzeData(timeBase);
}
// Update all the calls to include the method rank in
// their name.
for (Call call : mCallList) {
call.updateName();
}
if (mRegression) {
dumpMethodStats();
}
}
/*
* This method computes a list of records that describe the the execution
* timeline for each thread. Each record is a pair: (row, block) where: row:
* is the ThreadData object block: is the call (containing the start and end
* times)
*/
@Override
public ArrayList<TimeLineView.Record> getThreadTimeRecords() {
TimeLineView.Record record;
ArrayList<TimeLineView.Record> timeRecs;
timeRecs = new ArrayList<TimeLineView.Record>();
// For each thread, push a "toplevel" call that encompasses the
// entire execution of the thread.
for (ThreadData threadData : mSortedThreads) {
if (!threadData.isEmpty() && threadData.getId() != 0) {
record = new TimeLineView.Record(threadData, threadData.getRootCall());
timeRecs.add(record);
}
}
for (Call call : mCallList) {
record = new TimeLineView.Record(call.getThreadData(), call);
timeRecs.add(record);
}
if (mRegression) {
dumpTimeRecs(timeRecs);
System.exit(0);
}
return timeRecs;
}
private void dumpThreadTimes() {
System.out.print("\nThread Times\n");
System.out.print("id t-start t-end g-start g-end name\n");
for (ThreadData threadData : mThreadMap.values()) {
System.out.format("%2d %8d %8d %8d %8d %s\n",
threadData.getId(),
threadData.mThreadStartTime, threadData.mThreadEndTime,
threadData.mGlobalStartTime, threadData.mGlobalEndTime,
threadData.getName());
}
}
private void dumpCallTimes() {
System.out.print("\nCall Times\n");
System.out.print("id t-start t-end g-start g-end excl. incl. method\n");
for (Call call : mCallList) {
System.out.format("%2d %8d %8d %8d %8d %8d %8d %s\n",
call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime,
call.mGlobalStartTime, call.mGlobalEndTime,
call.mExclusiveCpuTime, call.mInclusiveCpuTime,
call.getMethodData().getName());
}
}
private void dumpMethodStats() {
System.out.print("\nMethod Stats\n");
System.out.print("Excl Cpu Incl Cpu Excl Real Incl Real Calls Method\n");
for (MethodData md : mSortedMethods) {
System.out.format("%9d %9d %9d %9d %9s %s\n",
md.getElapsedExclusiveCpuTime(), md.getElapsedInclusiveCpuTime(),
md.getElapsedExclusiveRealTime(), md.getElapsedInclusiveRealTime(),
md.getCalls(), md.getProfileName());
}
}
private void dumpTimeRecs(ArrayList<TimeLineView.Record> timeRecs) {
System.out.print("\nTime Records\n");
System.out.print("id t-start t-end g-start g-end method\n");
for (TimeLineView.Record record : timeRecs) {
Call call = (Call) record.block;
System.out.format("%2d %8d %8d %8d %8d %s\n",
call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime,
call.mGlobalStartTime, call.mGlobalEndTime,
call.getMethodData().getName());
}
}
@Override
public HashMap<Integer, String> getThreadLabels() {
HashMap<Integer, String> labels = new HashMap<Integer, String>();
for (ThreadData t : mThreadMap.values()) {
labels.put(t.getId(), t.getName());
}
return labels;
}
@Override
public MethodData[] getMethods() {
return mSortedMethods;
}
@Override
public ThreadData[] getThreads() {
return mSortedThreads;
}
@Override
public long getTotalCpuTime() {
return mTotalCpuTime;
}
@Override
public long getTotalRealTime() {
return mTotalRealTime;
}
@Override
public boolean haveCpuTime() {
return mClockSource != ClockSource.WALL;
}
@Override
public boolean haveRealTime() {
return mClockSource != ClockSource.THREAD_CPU;
}
@Override
public HashMap<String, String> getProperties() {
return mPropertiesMap;
}
@Override
public TimeBase getPreferredTimeBase() {
if (mClockSource == ClockSource.WALL) {
return TimeBase.REAL_TIME;
}
return TimeBase.CPU_TIME;
}
@Override
public String getClockSource() {
switch (mClockSource) {
case THREAD_CPU:
return "cpu time";
case WALL:
return "real time";
case DUAL:
return "real time, dual clock";
}
return null;
}
}