/* * 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.FileReader; import java.io.IOException; 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 int mVersionNumber = 0; private boolean mDebug = false; private static final int TRACE_MAGIC = 0x574f4c53; private boolean mRegression; private ProfileProvider mProfileProvider; private String mTraceFileName; private MethodData mTopLevel; private ArrayList<Call> mCallList; private ArrayList<Call> mSwitchList; private HashMap<Integer, MethodData> mMethodMap; private HashMap<Integer, ThreadData> mThreadMap; private ThreadData[] mSortedThreads; private MethodData[] mSortedMethods; private long mGlobalEndTime; private MethodData mContextSwitch; private int mOffsetToData; private byte[] mBytes = new byte[8]; // 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$ DmTraceReader(String traceFileName, boolean regression) { mTraceFileName = traceFileName; mRegression = regression; mMethodMap = new HashMap<Integer, MethodData>(); mThreadMap = new HashMap<Integer, ThreadData>(); // 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); generateTrees(); // dumpTrees(); } void generateTrees() { try { long offset = parseKeys(); parseData(offset); analyzeData(); } catch (IOException e) { System.err.println(e.getMessage()); System.exit(1); } } @Override public ProfileProvider getProfileProvider() { if (mProfileProvider == null) mProfileProvider = new ProfileProvider(this); return mProfileProvider; } Call readCall(MappedByteBuffer buffer, Call call) { int threadId; int methodId; long time; try { if (mVersionNumber == 1) threadId = buffer.get(); else threadId = buffer.getShort(); methodId = buffer.getInt(); time = buffer.getInt(); } catch (BufferUnderflowException ex) { return null; } 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); } if (call != null) { call.set(threadId, methodData, time, methodAction); } else { call = new Call(threadId, methodData, time, methodAction); } return call; } private MappedByteBuffer mapFile(String filename, long offset) { MappedByteBuffer buffer = null; try { FileInputStream dataFile = new FileInputStream(filename); 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); } catch (FileNotFoundException ex) { System.err.println(ex.getMessage()); System.exit(1); } catch (IOException ex) { System.err.println(ex.getMessage()); System.exit(1); } return buffer; } 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(); // read offset mOffsetToData = buffer.getShort() - 16; // read startWhen buffer.getLong(); // Skip over "mOffsetToData" bytes for (int ii = 0; ii < mOffsetToData; ii++) { buffer.get(); } // Save this position so that we can re-read the data later buffer.mark(); } private void parseData(long offset) { MappedByteBuffer buffer = mapFile(mTraceFileName, offset); readDataFileHeader(buffer); parseDataPass1(buffer); buffer.reset(); parseDataPass2(buffer); } private void parseDataPass1(MappedByteBuffer buffer) { mSwitchList = new ArrayList<Call>(); // Read the first call so that we can set "prevThreadData" Call call = new Call(); call = readCall(buffer, call); if (call == null) return; long callTime = call.mThreadStartTime; long prevCallTime = 0; ThreadData threadData = mThreadMap.get(call.getThreadId()); if (threadData == null) { String name = String.format("[%1$d]", call.getThreadId()); // $NON-NLS-1$ threadData = new ThreadData(call.getThreadId(), name, mTopLevel); mThreadMap.put(call.getThreadId(), threadData); } ThreadData prevThreadData = threadData; while (true) { // If a context switch occurred, then insert a placeholder "call" // record so that we can do something reasonable with the global // timestamps. if (prevThreadData != threadData) { Call switchEnter = new Call(prevThreadData.getId(), mContextSwitch, prevCallTime, 0); prevThreadData.setLastContextSwitch(switchEnter); mSwitchList.add(switchEnter); Call contextSwitch = threadData.getLastContextSwitch(); if (contextSwitch != null) { long prevStartTime = contextSwitch.mThreadStartTime; long elapsed = callTime - prevStartTime; long beforeSwitch = elapsed / 2; long afterSwitch = elapsed - beforeSwitch; long exitTime = callTime - afterSwitch; contextSwitch.mThreadStartTime = prevStartTime + beforeSwitch; Call switchExit = new Call(threadData.getId(), mContextSwitch, exitTime, 1); mSwitchList.add(switchExit); } prevThreadData = threadData; } // Read the next call call = readCall(buffer, call); if (call == null) { break; } prevCallTime = callTime; callTime = call.mThreadStartTime; threadData = mThreadMap.get(call.getThreadId()); if (threadData == null) { String name = String.format("[%d]", call.getThreadId()); threadData = new ThreadData(call.getThreadId(), name, mTopLevel); mThreadMap.put(call.getThreadId(), threadData); } } } void parseDataPass2(MappedByteBuffer buffer) { mCallList = new ArrayList<Call>(); // Read the first call so that we can set "prevThreadData" Call call = readCall(buffer, null); long callTime = call.mThreadStartTime; long prevCallTime = callTime; ThreadData threadData = mThreadMap.get(call.getThreadId()); ThreadData prevThreadData = threadData; threadData.setGlobalStartTime(0); int nthContextSwitch = 0; // Assign a global timestamp to each event. long globalTime = 0; while (true) { long elapsed = callTime - prevCallTime; if (threadData != prevThreadData) { // Get the next context switch. This one is entered // by the previous thread. Call contextSwitch = mSwitchList.get(nthContextSwitch++); mCallList.add(contextSwitch); elapsed = contextSwitch.mThreadStartTime - prevCallTime; globalTime += elapsed; elapsed = 0; contextSwitch.mGlobalStartTime = globalTime; prevThreadData.handleCall(contextSwitch, globalTime); if (!threadData.isEmpty()) { // This context switch is exited by the current thread. contextSwitch = mSwitchList.get(nthContextSwitch++); mCallList.add(contextSwitch); contextSwitch.mGlobalStartTime = globalTime; elapsed = callTime - contextSwitch.mThreadStartTime; threadData.handleCall(contextSwitch, globalTime); } // If the thread's global start time has not been set yet, // then set it. if (threadData.getGlobalStartTime() == -1) threadData.setGlobalStartTime(globalTime); prevThreadData = threadData; } globalTime += elapsed; call.mGlobalStartTime = globalTime; threadData.handleCall(call, globalTime); mCallList.add(call); // Read the next call call = readCall(buffer, null); if (call == null) { break; } prevCallTime = callTime; callTime = call.mThreadStartTime; threadData = mThreadMap.get(call.getThreadId()); } // Allow each thread to do any cleanup of the call stack. // Also add the elapsed time for each thread to the toplevel // method's inclusive time. for (int id : mThreadMap.keySet()) { threadData = mThreadMap.get(id); long endTime = threadData.endTrace(); if (endTime > 0) mTopLevel.addElapsedInclusive(endTime, false, null); } mGlobalEndTime = globalTime; if (mRegression) { 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 { BufferedReader in = null; try { in = new BufferedReader(new FileReader(mTraceFileName)); } catch (FileNotFoundException ex) { System.err.println(ex.getMessage()); } long offset = 0; 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")) { return offset; } } 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: break; } } } 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() { // 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>() { public int compare(ThreadData td1, ThreadData td2) { if (td2.getCpuTime() > td1.getCpuTime()) return 1; if (td2.getCpuTime() < td1.getCpuTime()) return -1; return td2.getName().compareTo(td1.getName()); } }); // Analyze the call tree so that we can label the "worst" children. // Also set all the root pointers in each node in the call tree. long sum = 0; for (ThreadData t : mSortedThreads) { if (t.isEmpty() == false) { Call root = t.getCalltreeRoot(); root.mGlobalStartTime = t.getGlobalStartTime(); } } // 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>() { public int compare(MethodData md1, MethodData md2) { if (md2.getElapsedInclusive() > md1.getElapsedInclusive()) return 1; if (md2.getElapsedInclusive() < md1.getElapsedInclusive()) 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 (md.getElapsedInclusive() == 0) break; nonZero += 1; } // Copy the methods with non-zero time mSortedMethods = new MethodData[nonZero]; int ii = 0; for (MethodData md : methods) { if (md.getElapsedInclusive() == 0) break; md.setRank(ii); mSortedMethods[ii++] = md; } // Let each method analyze its profile data for (MethodData md : mSortedMethods) { md.analyzeData(); } // 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) { Call call = new Call(threadData.getId(), mTopLevel, threadData.getGlobalStartTime(), 0); call.mGlobalStartTime = threadData.getGlobalStartTime(); call.mGlobalEndTime = threadData.getGlobalEndTime(); record = new TimeLineView.Record(threadData, call); timeRecs.add(record); } } for (Call call : mCallList) { if (call.getMethodAction() != 0 || call.getThreadId() == 0) continue; ThreadData threadData = mThreadMap.get(call.getThreadId()); record = new TimeLineView.Record(threadData, call); timeRecs.add(record); } if (mRegression) { dumpTimeRecs(timeRecs); System.exit(0); } return timeRecs; } private void dumpCallTimes() { String action; System.out.format("id thread global start,end method\n"); for (Call call : mCallList) { if (call.getMethodAction() == 0) { action = "+"; } else { action = " "; } long callTime = call.mThreadStartTime; System.out.format("%2d %6d %8d %8d %s %s\n", call.getThreadId(), callTime, call.mGlobalStartTime, call.mGlobalEndTime, action, call.getMethodData().getName()); // if (call.getMethodAction() == 0 && call.getGlobalEndTime() < call.getGlobalStartTime()) { // System.out.printf("endtime %d < startTime %d\n", // call.getGlobalEndTime(), call.getGlobalStartTime()); // } } } private void dumpMethodStats() { System.out.format("\nExclusive Inclusive Calls Method\n"); for (MethodData md : mSortedMethods) { System.out.format("%9d %9d %9s %s\n", md.getElapsedExclusive(), md.getElapsedInclusive(), md.getCalls(), md.getProfileName()); } } private void dumpTimeRecs(ArrayList<TimeLineView.Record> timeRecs) { System.out.format("\nid thread global start,end method\n"); for (TimeLineView.Record record : timeRecs) { Call call = (Call) record.block; long callTime = call.mThreadStartTime; System.out.format("%2d %6d %8d %8d %s\n", call.getThreadId(), callTime, 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 getEndTime() { return mGlobalEndTime; } }