/* * 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; } }