/* * 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.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Stack; /** * {@link CallStackReconstructor} helps in reconstructing per thread call stacks from a sequence of * trace events (method entry/exit events). */ public class CallStackReconstructor { /** Method id corresponding to the top level call under which all calls are nested. */ private final long mTopLevelCallId; /** List of calls currently assumed to be at stack depth 0 (called from the top level) */ private final List<Call.Builder> mTopLevelCalls = new ArrayList<Call.Builder>(); /** Current call stack based on the sequence of received trace events. */ private final Stack<Call.Builder> mCallStack = new Stack<Call.Builder>(); /** The single top level call under which the entire reconstructed call stack nests. */ private Call mTopLevelCall; /** * Constructs a call stack reconstructor with the method id under which * the entire call stack should nest. * */ public CallStackReconstructor(long topLevelCallId) { mTopLevelCallId = topLevelCallId; } public void addTraceAction(long methodId, TraceAction action, int threadTime, int globalTime) { if (action == TraceAction.METHOD_ENTER) { enterMethod(methodId, threadTime, globalTime); } else { exitMethod(methodId, threadTime, globalTime); } } private void enterMethod(long methodId, int threadTime, int globalTime) { Call.Builder cb = new Call.Builder(methodId); cb.setMethodEntryTime(threadTime, globalTime); if (mCallStack.isEmpty()) { mTopLevelCalls.add(cb); } else { Call.Builder caller = mCallStack.peek(); caller.addCallee(cb); } mCallStack.push(cb); } private void exitMethod(long methodId, int threadTime, int globalTime) { if (!mCallStack.isEmpty()) { Call.Builder c = mCallStack.pop(); if (c.getMethodId() != methodId) { String msg = String .format("Error during call stack reconstruction. Attempt to exit from method 0x%1$x while in method 0x%2$x", c.getMethodId(), methodId); throw new RuntimeException(msg); } c.setMethodExitTime(threadTime, globalTime); } else { // We are exiting out of a method that was entered into before tracing was started. // In such a case, create this method Call.Builder c = new Call.Builder(methodId); // All the previous calls at the top level are now assumed to have been called from // this method. So mark this method as having called all of those methods, and reset // the top level to only include this method for (Call.Builder cb : mTopLevelCalls) { c.addCallee(cb); } mTopLevelCalls.clear(); mTopLevelCalls.add(c); c.setMethodExitTime(threadTime, globalTime); // We don't know this method's entry times, so we try to guess: // If it has atleast 1 callee, then we know it must've been atleast before that callee's // start time. If there are no callees, then we just assume that it was just before its // exit times. int entryThreadTime = threadTime - 1; int entryGlobalTime = globalTime - 1; if (c.getCallees() != null && !c.getCallees().isEmpty()) { Call.Builder callee = c.getCallees().get(0); entryThreadTime = Math.max(callee.getMethodEntryThreadTime() - 1, 0); entryGlobalTime = Math.max(callee.getMethodEntryGlobalTime() - 1, 0); } c.setMethodEntryTime(entryThreadTime, entryGlobalTime); } } /** * Generates a trace action equivalent to exiting from the given method * @param methoId id of the method from which we are exiting * @param entryThreadTime method's thread entry time * @param entryGlobalTime method's global entry time * @param callees from the method that we are exiting */ private void exitMethod(long methoId, int entryThreadTime, int entryGlobalTime, @Nullable List<Call.Builder> callees) { int lastExitThreadTime; int lastExitGlobalTime; if (callees == null || callees.isEmpty()) { // if the call doesn't have any callees, we assume that it just ran for 1 unit of time lastExitThreadTime = entryThreadTime + 1; lastExitGlobalTime = entryGlobalTime + 1; } else { // if it did call other methods, we assume that this call exited 1 unit of time after // its last callee exited Call.Builder last = callees.get(callees.size() - 1); lastExitThreadTime = last.getMethodExitThreadTime() + 1; lastExitGlobalTime = last.getMethodExitGlobalTime() + 1; } exitMethod(methoId, lastExitThreadTime, lastExitGlobalTime); } private void fixupCallStacks() { if (mTopLevelCall != null) { return; } // If there are any methods still on the call stack, then the trace doesn't have // exit trace action for them, so clean those up while (!mCallStack.isEmpty()) { Call.Builder cb = mCallStack.peek(); exitMethod(cb.getMethodId(), cb.getMethodEntryThreadTime(), cb.getMethodEntryGlobalTime(), cb.getCallees()); } // Now that we have parsed the entire call stack, let us move all of it under a single // top level call. exitMethod(mTopLevelCallId, 0, 0, mTopLevelCalls); // TODO: use global / thread times to infer context switches // Build calls from their respective builders // Now that we've added the top level call, there should be only 1 top level call assert mTopLevelCalls.size() == 1; mTopLevelCall = mTopLevelCalls.get(0).build(new Stack<Long>()); } public Call getTopLevel() { fixupCallStacks(); return mTopLevelCall; } }