blob: 026610352e42e9737a5551b374a9376950ea9c27 [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.profiler;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
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
* with samples taken every 100 milliseconds. 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(100);
* // period of measurement
* profiler.stop();
* // period of non-measurement
* profiler.start(100);
* // another period of measurement
* profiler.stop();
* profiler.shutdown();
* AsciiHprofWriter.write(profiler.getHprofData(), System.out);
* }</pre>
*/
public final class SamplingProfiler {
/**
* Map of stack traces to a mutable sample count.
*/
private final Map<HprofData.StackTrace, int[]> stackTraces
= new HashMap<HprofData.StackTrace, int[]>();
/**
* Data collected by the sampling profiler
*/
private final HprofData hprofData = new HprofData(stackTraces);
/**
* Timer that is used for the lifetime of the profiler
*/
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 Sampler 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;
/*
* 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 nextStackTraceId = 300001;
private int nextObjectId = 1;
/**
* The threads currently known to the profiler for detecting
* thread start and end events.
*/
private Thread[] currentThreads = new Thread[0];
/**
* 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>();
/**
* Mutable {@code StackTrace} that is used for probing the {@link
* #stackTraces stackTraces} map without allocating a {@code
* StackTrace}. If {@link #addStackTrace addStackTrace} needs to
* be thread safe, have a single mutable instance would need to be
* reconsidered.
*/
private final HprofData.StackTrace mutableStackTrace = new HprofData.StackTrace();
/**
* The {@code ThreadSampler} is used to produce a {@code
* StackTraceElement} array for a given thread. The array is
* expected to be {@link #depth depth} or less in length.
*/
private final ThreadSampler threadSampler;
/**
* 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.
*
* @param threadSet The thread set specifies which threads to
* sample. In a general purpose program, all threads typically
* should be sample with a ThreadSet such as provied by {@link
* #newThreadGroupTheadSet newThreadGroupTheadSet}. For a
* benchmark a fixed set such as provied by {@link
* #newArrayThreadSet newArrayThreadSet} can reduce the overhead
* of profiling.
*/
public SamplingProfiler(int depth, ThreadSet threadSet) {
this.depth = depth;
this.threadSet = threadSet;
this.threadSampler = findDefaultThreadSampler();
threadSampler.setDepth(depth);
hprofData.setFlags(BinaryHprof.ControlSettings.CPU_SAMPLING.bitmask);
hprofData.setDepth(depth);
}
private static ThreadSampler findDefaultThreadSampler() {
if ("Dalvik Core Library".equals(System.getProperty("java.specification.name"))) {
String className = "dalvik.system.profiler.DalvikThreadSampler";
try {
return (ThreadSampler) Class.forName(className).newInstance();
} catch (Exception e) {
System.out.println("Problem creating " + className + ": " + e);
}
}
return new PortableThreadSampler();
}
/**
* 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 interval The number of milliseconds between samples
*/
public void start(int interval) {
if (interval < 1) {
throw new IllegalArgumentException("interval < 1");
}
if (sampler != null) {
throw new IllegalStateException("profiling already started");
}
sampler = new Sampler();
hprofData.setStartMillis(System.currentTimeMillis());
timer.scheduleAtFixedRate(sampler, 0, interval);
}
/**
* Stops profiler sampling. It can be restarted with {@link
* #start(int)} to continue sampling.
*/
public void stop() {
if (sampler == null) {
return;
}
synchronized(sampler) {
sampler.stop = true;
while (!sampler.stopped) {
try {
sampler.wait();
} catch (InterruptedException ignored) {
}
}
}
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();
}
/**
* Returns the hprof data accumulated by the profiler since it was
* created. The profiler needs to be stopped, but not necessarily
* shut down, in order to access the data. If the profiler is
* restarted, there is no thread safe way to access the data.
*/
public HprofData getHprofData() {
if (sampler != null) {
throw new IllegalStateException("cannot access hprof data while sampling");
}
return hprofData;
}
/**
* 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 boolean stop;
private boolean stopped;
private Thread timerThread;
public void run() {
synchronized(this) {
if (stop) {
cancel();
stopped = true;
notifyAll();
return;
}
}
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;
}
StackTraceElement[] stackFrames = threadSampler.getStackTrace(thread);
if (stackFrames == null) {
continue;
}
recordStackTrace(thread, stackFrames);
}
}
/**
* Record a new stack trace. The thread should have been
* previously registered with addStartThread.
*/
private void recordStackTrace(Thread thread, StackTraceElement[] stackFrames) {
Integer threadId = threadIds.get(thread);
if (threadId == null) {
throw new IllegalArgumentException("Unknown thread " + thread);
}
mutableStackTrace.threadId = threadId;
mutableStackTrace.stackFrames = stackFrames;
int[] countCell = stackTraces.get(mutableStackTrace);
if (countCell == null) {
countCell = new int[1];
// cloned because the ThreadSampler may reuse the array
StackTraceElement[] stackFramesCopy = stackFrames.clone();
HprofData.StackTrace stackTrace
= new HprofData.StackTrace(nextStackTraceId++, threadId, stackFramesCopy);
hprofData.addStackTrace(stackTrace, countCell);
}
countCell[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;
}
addStartThread(thread);
}
for (Thread thread : removed) {
if (thread == null) {
continue;
}
if (thread == timerThread) {
continue;
}
addEndThread(thread);
}
}
/**
* Record that a newly noticed thread.
*/
private void addStartThread(Thread thread) {
if (thread == null) {
throw new NullPointerException("thread == null");
}
int threadId = nextThreadId++;
Integer old = threadIds.put(thread, threadId);
if (old != null) {
throw new IllegalArgumentException("Thread already registered as " + old);
}
String threadName = thread.getName();
// group will become null when thread is terminated
ThreadGroup group = thread.getThreadGroup();
String groupName = group == null ? null : group.getName();
ThreadGroup parentGroup = group == null ? null : group.getParent();
String parentGroupName = parentGroup == null ? null : parentGroup.getName();
HprofData.ThreadEvent event
= HprofData.ThreadEvent.start(nextObjectId++, threadId,
threadName, groupName, parentGroupName);
hprofData.addThreadEvent(event);
}
/**
* Record that a thread has disappeared.
*/
private void addEndThread(Thread thread) {
if (thread == null) {
throw new NullPointerException("thread == null");
}
Integer threadId = threadIds.remove(thread);
if (threadId == null) {
throw new IllegalArgumentException("Unknown thread " + thread);
}
HprofData.ThreadEvent event = HprofData.ThreadEvent.end(threadId);
hprofData.addThreadEvent(event);
}
}
}