blob: 31ecf65d28f02e346addaeae2f96c1edad36afaf [file] [log] [blame]
/*
* Copyright (C) 2010 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 dalvik.system;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
/**
* A sampling profiler. It currently is implemented without any
* virtual machine support, relying solely on {@code
* Thread.getStackTrace} to collect samples. As such, the overhead is
* higher than a native approach and it does not provide insight into
* where time is spent within native code, but it can still provide
* useful insight into where a program is spending time.
*
* <h3>Usage Example</h3>
*
* The following example shows how to use the {@code
* SamplingProfiler}. It samples the current thread's stack to a depth
* of 12 stack frame elements over two different measurement periods
* at a rate of 10 samples a second. In then prints the results in
* hprof format to the standard output.
*
* <pre> {@code
* ThreadSet threadSet = SamplingProfiler.newArrayThreadSet(Thread.currentThread());
* SamplingProfiler profiler = new SamplingProfiler(12, threadSet);
* profiler.start(10);
* // period of measurement
* profiler.stop();
* // period of non-measurement
* profiler.start(10);
* // another period of measurement
* profiler.stop();
* profiler.shutdown();
* profiler.writeHprofData(System.out);
* }</pre>
*
* @hide
*/
public final class SamplingProfiler {
/*
* Real hprof output examples don't start the thread and trace
* identifiers at one but seem to start at these arbitrary
* constants. It certainly seems useful to have relatively unique
* identifers when manual searching hprof output.
*/
private int nextThreadId = 200001;
private int nextTraceId = 300001;
private int nextObjectId = 1;
/**
* Map of currently active threads to their identifiers. When
* threads disappear they are removed and only referenced by their
* identifiers to prevent retaining garbage threads.
*/
private final Map<Thread, Integer> threadIds = new HashMap<Thread, Integer>();
/**
* List of thread creation and death events.
*/
private final List<ThreadEvent> threadHistory = new ArrayList<ThreadEvent>();
/**
* Map of stack traces to a mutable sample count.
*/
private final Map<Trace, int[]> traces = new HashMap<Trace, int[]>();
/**
* Timer that is used for the lifetime of the profiler
*/
// note that dalvik/vm/Thread.c depends on this name
private final Timer timer = new Timer("SamplingProfiler", true);
/**
* A sampler is created every time profiling starts and cleared
* everytime profiling stops because once a {@code TimerTask} is
* canceled it cannot be reused.
*/
private TimerTask sampler;
/**
* The maximum number of {@code StackTraceElements} to retain in
* each stack.
*/
private final int depth;
/**
* The {@code ThreadSet} that identifies which threads to sample.
*/
private final ThreadSet threadSet;
/**
* Create a sampling profiler that collects stacks with the
* specified depth from the threads specified by the specified
* thread collector.
*
* @param depth The maximum stack depth to retain for each sample
* similar to the hprof option of the same name. Any stack deeper
* than this will be truncated to this depth. A good starting
* value is 4 although it is not uncommon to need to raise this to
* get enough context to understand program behavior. While
* programs with extensive recursion may require a high value for
* depth, simply passing in a value for Integer.MAX_VALUE is not
* advised because of the significant memory need to retain such
* stacks and runtime overhead to compare stacks.
*/
public SamplingProfiler(int depth, ThreadSet threadSet) {
this.depth = depth;
this.threadSet = threadSet;
}
/**
* A ThreadSet specifies the set of threads to sample.
*/
public static interface ThreadSet {
/**
* Returns an array containing the threads to be sampled. The
* array may be longer than the number of threads to be
* sampled, in which case the extra elements must be null.
*/
public Thread[] threads();
}
/**
* Returns a ThreadSet for a fixed set of threads that will not
* vary at runtime. This has less overhead than a dynamically
* calculated set, such as {@link newThreadGroupTheadSet}, which has
* to enumerate the threads each time profiler wants to collect
* samples.
*/
public static ThreadSet newArrayThreadSet(Thread... threads) {
return new ArrayThreadSet(threads);
}
/**
* An ArrayThreadSet samples a fixed set of threads that does not
* vary over the life of the profiler.
*/
private static class ArrayThreadSet implements ThreadSet {
private final Thread[] threads;
public ArrayThreadSet(Thread... threads) {
if (threads == null) {
throw new NullPointerException("threads == null");
}
this.threads = threads;
}
public Thread[] threads() {
return threads;
}
}
/**
* Returns a ThreadSet that is dynamically computed based on the
* threads found in the specified ThreadGroup and that
* ThreadGroup's children.
*/
public static ThreadSet newThreadGroupTheadSet(ThreadGroup threadGroup) {
return new ThreadGroupThreadSet(threadGroup);
}
/**
* An ThreadGroupThreadSet sample the threads from the specified
* ThreadGroup and the ThreadGroup's children
*/
private static class ThreadGroupThreadSet implements ThreadSet {
private final ThreadGroup threadGroup;
private Thread[] threads;
private int lastThread;
public ThreadGroupThreadSet(ThreadGroup threadGroup) {
if (threadGroup == null) {
throw new NullPointerException("threadGroup == null");
}
this.threadGroup = threadGroup;
resize();
}
private void resize() {
int count = threadGroup.activeCount();
// we can only tell if we had enough room for all active
// threads if we actually are larger than the the number of
// active threads. making it larger also leaves us room to
// tolerate additional threads without resizing.
threads = new Thread[count*2];
lastThread = 0;
}
public Thread[] threads() {
int threadCount;
while (true) {
threadCount = threadGroup.enumerate(threads);
if (threadCount == threads.length) {
resize();
} else {
break;
}
}
if (threadCount < lastThread) {
// avoid retaining pointers to threads that have ended
Arrays.fill(threads, threadCount, lastThread, null);
}
lastThread = threadCount;
return threads;
}
}
/**
* Starts profiler sampling at the specified rate.
*
* @param samplesPerSecond The number of samples to take a second
*/
public void start(int samplesPerSecond) {
if (samplesPerSecond < 1) {
throw new IllegalArgumentException("samplesPerSecond < 1");
}
if (sampler != null) {
throw new IllegalStateException("profiling already started");
}
sampler = new Sampler();
timer.scheduleAtFixedRate(sampler, 0, 1000/samplesPerSecond);
}
/**
* Stops profiler sampling. It can be restarted with {@link
* #start(int)} to continue sampling.
*/
public void stop() {
if (sampler == null) {
return;
}
sampler.cancel();
sampler = null;
}
/**
* Shuts down profiling after which it can not be restarted. It is
* important to shut down profiling when done to free resources
* used by the profiler. Shutting down the profiler also stops the
* profiling if that has not already been done.
*/
public void shutdown() {
stop();
timer.cancel();
}
/**
* The Sampler does the real work of the profiler.
*
* At every sample time, it asks the thread set for the set
* of threads to sample. It maintains a history of thread creation
* and death events based on changes observed to the threads
* returned by the {@code ThreadSet}.
*
* For each thread to be sampled, a stack is collected and used to
* update the set of collected samples. Stacks are truncated to a
* maximum depth. There is no way to tell if a stack has been truncated.
*/
private class Sampler extends TimerTask {
private Thread timerThread;
private Thread[] currentThreads = new Thread[0];
private final Trace mutableTrace = new Trace();
public void run() {
if (timerThread == null) {
timerThread = Thread.currentThread();
}
// process thread creation and death first so that we
// assign thread ids to any new threads before allocating
// new stacks for them
Thread[] newThreads = threadSet.threads();
if (!Arrays.equals(currentThreads, newThreads)) {
updateThreadHistory(currentThreads, newThreads);
currentThreads = newThreads.clone();
}
for (Thread thread : currentThreads) {
if (thread == null) {
break;
}
if (thread == timerThread) {
continue;
}
// TODO replace with a VMStack.getThreadStackTrace
// variant to avoid allocating unneeded elements
StackTraceElement[] stack = thread.getStackTrace();
if (stack.length > depth) {
stack = Arrays.copyOfRange(stack, 0, depth);
}
mutableTrace.threadId = threadIds.get(thread);
mutableTrace.stack = stack;
int[] count = traces.get(mutableTrace);
if (count == null) {
Trace trace = new Trace(nextTraceId++, mutableTrace);
count = new int[1];
traces.put(trace, count);
}
count[0]++;
}
}
private void updateThreadHistory(Thread[] oldThreads, Thread[] newThreads) {
// thread start/stop shouldn't happen too often and
// these aren't too big, so hopefully this approach
// won't be too slow...
Set<Thread> n = new HashSet<Thread>(Arrays.asList(newThreads));
Set<Thread> o = new HashSet<Thread>(Arrays.asList(oldThreads));
// added = new-old
Set<Thread> added = new HashSet<Thread>(n);
added.removeAll(o);
// removed = old-new
Set<Thread> removed = new HashSet<Thread>(o);
removed.removeAll(n);
for (Thread thread : added) {
if (thread == null) {
continue;
}
if (thread == timerThread) {
continue;
}
int threadId = nextThreadId++;
threadIds.put(thread, threadId);
ThreadEvent event = ThreadEvent.start(nextObjectId++,threadId, thread);
threadHistory.add(event);
}
for (Thread thread : removed) {
if (thread == null) {
continue;
}
if (thread == timerThread) {
continue;
}
int threadId = threadIds.remove(thread);
ThreadEvent event = ThreadEvent.stop(threadId);
threadHistory.add(event);
}
}
}
private enum ThreadEventType { START, STOP };
/**
* ThreadEvent represents thread creation and death events for
* reporting. It provides a record of the thread and thread group
* names for tying samples back to their source thread.
*/
private static class ThreadEvent {
private final ThreadEventType type;
private final int objectId;
private final int threadId;
private final String threadName;
private final String groupName;
private static ThreadEvent start(int objectId, int threadId, Thread thread) {
return new ThreadEvent(ThreadEventType.START, objectId, threadId, thread);
}
private static ThreadEvent stop(int threadId) {
return new ThreadEvent(ThreadEventType.STOP, threadId);
}
private ThreadEvent(ThreadEventType type, int objectId, int threadId, Thread thread) {
this.type = ThreadEventType.START;
this.objectId = objectId;
this.threadId = threadId;
this.threadName = thread.getName();
this.groupName = thread.getThreadGroup().getName();
}
private ThreadEvent(ThreadEventType type, int threadId) {
this.type = ThreadEventType.STOP;
this.objectId = -1;
this.threadId = threadId;
this.threadName = null;
this.groupName = null;
}
@Override
public String toString() {
switch (type) {
case START:
return String.format(
"THREAD START (obj=%d, id = %d, name=\"%s\", group=\"%s\")",
objectId, threadId, threadName, groupName);
case STOP:
return String.format("THREAD END (id = %d)", threadId);
}
throw new IllegalStateException(type.toString());
}
}
/**
* A Trace represents a unique stack trace for a specific thread.
*/
private static class Trace {
private final int traceId;
private int threadId;
private StackTraceElement[] stack;
private Trace() {
this.traceId = -1;
}
private Trace(int traceId, Trace mutableTrace) {
this.traceId = traceId;
this.threadId = mutableTrace.threadId;
this.stack = mutableTrace.stack;
}
protected int getThreadId() {
return threadId;
}
protected StackTraceElement[] getStack() {
return stack;
}
@Override
public final int hashCode() {
int result = 17;
result = 31 * result + getThreadId();
result = 31 * result + Arrays.hashCode(getStack());
return result;
}
@Override
public final boolean equals(Object o) {
Trace s = (Trace) o;
return getThreadId() == s.getThreadId() && Arrays.equals(getStack(), s.getStack());
}
}
/**
* Prints the profiler's collected data in hprof format to the
* specified {@code File}. See the {@link #writeHprofData(PrintStream)
* PrintStream} variant for more information.
*/
public void writeHprofData(File file) throws IOException {
PrintStream out = null;
try {
out = new PrintStream(new BufferedOutputStream(new FileOutputStream(file)));
writeHprofData(out);
} finally {
if (out != null) {
out.close();
}
}
}
private static final class SampleComparator implements Comparator<Entry<Trace, int[]>> {
private static final SampleComparator INSTANCE = new SampleComparator();
public int compare(Entry<Trace, int[]> e1, Entry<Trace, int[]> e2) {
return e2.getValue()[0] - e1.getValue()[0];
}
}
/**
* Prints the profiler's collected data in hprof format to the
* specified {@code PrintStream}. The profiler needs to be
* stopped, but not necessarily shut down, in order to produce a
* report.
*/
public void writeHprofData(PrintStream out) {
if (sampler != null) {
throw new IllegalStateException("cannot print hprof data while sampling");
}
for (ThreadEvent e : threadHistory) {
out.println(e);
}
List<Entry<Trace, int[]>> samples = new ArrayList<Entry<Trace, int[]>>(traces.entrySet());
Collections.sort(samples, SampleComparator.INSTANCE);
int total = 0;
for (Entry<Trace, int[]> sample : samples) {
Trace trace = sample.getKey();
int count = sample.getValue()[0];
total += count;
out.printf("TRACE %d: (thread=%d)\n", trace.traceId, trace.threadId);
for (StackTraceElement e : trace.stack) {
out.printf("\t%s\n", e);
}
}
Date now = new Date();
// "CPU SAMPLES BEGIN (total = 826) Wed Jul 21 12:03:46 2010"
out.printf("CPU SAMPLES BEGIN (total = %d) %ta %tb %td %tT %tY\n",
total, now, now, now, now, now);
out.printf("rank self accum count trace method\n");
int rank = 0;
double accum = 0;
for (Entry<Trace, int[]> sample : samples) {
rank++;
Trace trace = sample.getKey();
int count = sample.getValue()[0];
double self = (double)count/(double)total;
accum += self;
// " 1 65.62% 65.62% 542 300302 java.lang.Long.parseLong"
out.printf("% 4d% 6.2f%%% 6.2f%% % 7d % 5d %s.%s\n",
rank, self*100, accum*100, count, trace.traceId,
trace.stack[0].getClassName(),
trace.stack[0].getMethodName());
}
out.printf("CPU SAMPLES END\n");
}
}