SamplingProfiler.java revision cfa84a2aac159bb8a1763298882df7aa98f7fc6f
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package dalvik.system.profiler;
18
19import java.util.Arrays;
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.Map;
23import java.util.Set;
24import java.util.Timer;
25import java.util.TimerTask;
26
27/**
28 * A sampling profiler. It currently is implemented without any
29 * virtual machine support, relying solely on {@code
30 * Thread.getStackTrace} to collect samples. As such, the overhead is
31 * higher than a native approach and it does not provide insight into
32 * where time is spent within native code, but it can still provide
33 * useful insight into where a program is spending time.
34 *
35 * <h3>Usage Example</h3>
36 *
37 * The following example shows how to use the {@code
38 * SamplingProfiler}. It samples the current thread's stack to a depth
39 * of 12 stack frame elements over two different measurement periods
40 * with samples taken every 100 milliseconds. In then prints the
41 * results in hprof format to the standard output.
42 *
43 * <pre> {@code
44 * ThreadSet threadSet = SamplingProfiler.newArrayThreadSet(Thread.currentThread());
45 * SamplingProfiler profiler = new SamplingProfiler(12, threadSet);
46 * profiler.start(100);
47 * // period of measurement
48 * profiler.stop();
49 * // period of non-measurement
50 * profiler.start(100);
51 * // another period of measurement
52 * profiler.stop();
53 * profiler.shutdown();
54 * HprofWriter writer = new AsciiHprofWriter(profiler.getHprofData(), System.out);
55 * writer.write();
56 * }</pre>
57 */
58public final class SamplingProfiler {
59
60    /**
61     * Map of stack traces to a mutable sample count.
62     */
63    private final Map<HprofData.StackTrace, int[]> stackTraces
64            = new HashMap<HprofData.StackTrace, int[]>();
65
66    /**
67     * Data collected by the sampling profiler
68     */
69    private final HprofData hprofData = new HprofData(stackTraces);
70
71    /**
72     * Timer that is used for the lifetime of the profiler
73     */
74    private final Timer timer = new Timer("SamplingProfiler", true);
75
76    /**
77     * A sampler is created every time profiling starts and cleared
78     * everytime profiling stops because once a {@code TimerTask} is
79     * canceled it cannot be reused.
80     */
81    private TimerTask sampler;
82
83    /**
84     * The maximum number of {@code StackTraceElements} to retain in
85     * each stack.
86     */
87    private final int depth;
88
89    /**
90     * The {@code ThreadSet} that identifies which threads to sample.
91     */
92    private final ThreadSet threadSet;
93
94    /*
95     *  Real hprof output examples don't start the thread and trace
96     *  identifiers at one but seem to start at these arbitrary
97     *  constants. It certainly seems useful to have relatively unique
98     *  identifers when manual searching hprof output.
99     */
100    private int nextThreadId = 200001;
101    private int nextStackTraceId = 300001;
102    private int nextObjectId = 1;
103
104    /**
105     * The threads currently known to the profiler for detecting
106     * thread start and end events.
107     */
108    private Thread[] currentThreads = new Thread[0];
109
110    /**
111     * Map of currently active threads to their identifiers. When
112     * threads disappear they are removed and only referenced by their
113     * identifiers to prevent retaining garbage threads.
114     */
115    private final Map<Thread, Integer> threadIds = new HashMap<Thread, Integer>();
116
117    /**
118     * Mutable StackTrace that is used for probing stackTraces Map
119     * without allocating a StackTrace. If addStackTrace needs to
120     * be thread safe, this would need to be reconsidered.
121     */
122    private final HprofData.StackTrace mutableStackTrace = new HprofData.StackTrace();
123
124    /**
125     * Create a sampling profiler that collects stacks with the
126     * specified depth from the threads specified by the specified
127     * thread collector.
128     *
129     * @param depth The maximum stack depth to retain for each sample
130     * similar to the hprof option of the same name. Any stack deeper
131     * than this will be truncated to this depth. A good starting
132     * value is 4 although it is not uncommon to need to raise this to
133     * get enough context to understand program behavior. While
134     * programs with extensive recursion may require a high value for
135     * depth, simply passing in a value for Integer.MAX_VALUE is not
136     * advised because of the significant memory need to retain such
137     * stacks and runtime overhead to compare stacks.
138     */
139    public SamplingProfiler(int depth, ThreadSet threadSet) {
140        this.depth = depth;
141        this.threadSet = threadSet;
142        hprofData.setFlags(BinaryHprof.ControlSettings.CPU_SAMPLING.bitmask);
143        hprofData.setDepth(depth);
144    }
145
146    /**
147     * A ThreadSet specifies the set of threads to sample.
148     */
149    public static interface ThreadSet {
150        /**
151         * Returns an array containing the threads to be sampled. The
152         * array may be longer than the number of threads to be
153         * sampled, in which case the extra elements must be null.
154         */
155        public Thread[] threads();
156    }
157
158    /**
159     * Returns a ThreadSet for a fixed set of threads that will not
160     * vary at runtime. This has less overhead than a dynamically
161     * calculated set, such as {@link #newThreadGroupTheadSet}, which has
162     * to enumerate the threads each time profiler wants to collect
163     * samples.
164     */
165    public static ThreadSet newArrayThreadSet(Thread... threads) {
166        return new ArrayThreadSet(threads);
167    }
168
169    /**
170     * An ArrayThreadSet samples a fixed set of threads that does not
171     * vary over the life of the profiler.
172     */
173    private static class ArrayThreadSet implements ThreadSet {
174        private final Thread[] threads;
175        public ArrayThreadSet(Thread... threads) {
176            if (threads == null) {
177                throw new NullPointerException("threads == null");
178            }
179            this.threads = threads;
180        }
181        public Thread[] threads() {
182            return threads;
183        }
184    }
185
186    /**
187     * Returns a ThreadSet that is dynamically computed based on the
188     * threads found in the specified ThreadGroup and that
189     * ThreadGroup's children.
190     */
191    public static ThreadSet newThreadGroupTheadSet(ThreadGroup threadGroup) {
192        return new ThreadGroupThreadSet(threadGroup);
193    }
194
195    /**
196     * An ThreadGroupThreadSet sample the threads from the specified
197     * ThreadGroup and the ThreadGroup's children
198     */
199    private static class ThreadGroupThreadSet implements ThreadSet {
200        private final ThreadGroup threadGroup;
201        private Thread[] threads;
202        private int lastThread;
203
204        public ThreadGroupThreadSet(ThreadGroup threadGroup) {
205            if (threadGroup == null) {
206                throw new NullPointerException("threadGroup == null");
207            }
208            this.threadGroup = threadGroup;
209            resize();
210        }
211
212        private void resize() {
213            int count = threadGroup.activeCount();
214            // we can only tell if we had enough room for all active
215            // threads if we actually are larger than the the number of
216            // active threads. making it larger also leaves us room to
217            // tolerate additional threads without resizing.
218            threads = new Thread[count*2];
219            lastThread = 0;
220        }
221
222        public Thread[] threads() {
223            int threadCount;
224            while (true) {
225                threadCount = threadGroup.enumerate(threads);
226                if (threadCount == threads.length) {
227                    resize();
228                } else {
229                    break;
230                }
231            }
232            if (threadCount < lastThread) {
233                // avoid retaining pointers to threads that have ended
234                Arrays.fill(threads, threadCount, lastThread, null);
235            }
236            lastThread = threadCount;
237            return threads;
238        }
239    }
240
241    /**
242     * Starts profiler sampling at the specified rate.
243     *
244     * @param interval The number of milliseconds between samples
245     */
246    public void start(int interval) {
247        if (interval < 1) {
248            throw new IllegalArgumentException("interval < 1");
249        }
250        if (sampler != null) {
251            throw new IllegalStateException("profiling already started");
252        }
253        sampler = new Sampler();
254        hprofData.setStartMillis(System.currentTimeMillis());
255        timer.scheduleAtFixedRate(sampler, 0, interval);
256    }
257
258    /**
259     * Stops profiler sampling. It can be restarted with {@link
260     * #start(int)} to continue sampling.
261     */
262    public void stop() {
263        if (sampler == null) {
264            return;
265        }
266        sampler.cancel();
267        sampler = null;
268    }
269
270    /**
271     * Shuts down profiling after which it can not be restarted. It is
272     * important to shut down profiling when done to free resources
273     * used by the profiler. Shutting down the profiler also stops the
274     * profiling if that has not already been done.
275     */
276    public void shutdown() {
277        stop();
278        timer.cancel();
279    }
280
281    /**
282     * Returns the hprof data accumulated by the profiler since it was
283     * created. The profiler needs to be stopped, but not necessarily
284     * shut down, in order to access the data. If the profiler is
285     * restarted, there is no thread safe way to access the data.
286     */
287    public HprofData getHprofData() {
288        if (sampler != null) {
289            throw new IllegalStateException("cannot access hprof data while sampling");
290        }
291        return hprofData;
292    }
293
294    /**
295     * The Sampler does the real work of the profiler.
296     *
297     * At every sample time, it asks the thread set for the set
298     * of threads to sample. It maintains a history of thread creation
299     * and death events based on changes observed to the threads
300     * returned by the {@code ThreadSet}.
301     *
302     * For each thread to be sampled, a stack is collected and used to
303     * update the set of collected samples. Stacks are truncated to a
304     * maximum depth. There is no way to tell if a stack has been truncated.
305     */
306    private class Sampler extends TimerTask {
307
308        private Thread timerThread;
309
310        public void run() {
311            if (timerThread == null) {
312                timerThread = Thread.currentThread();
313            }
314
315            // process thread creation and death first so that we
316            // assign thread ids to any new threads before allocating
317            // new stacks for them
318            Thread[] newThreads = threadSet.threads();
319            if (!Arrays.equals(currentThreads, newThreads)) {
320                updateThreadHistory(currentThreads, newThreads);
321                currentThreads = newThreads.clone();
322            }
323
324            for (Thread thread : currentThreads) {
325                if (thread == null) {
326                    break;
327                }
328                if (thread == timerThread) {
329                    continue;
330                }
331
332                // TODO replace with a VMStack.getThreadStackTrace
333                // variant to avoid allocating unneeded elements
334                StackTraceElement[] stackFrames = thread.getStackTrace();
335                if (stackFrames.length == 0) {
336                    continue;
337                }
338                if (stackFrames.length > depth) {
339                    stackFrames = Arrays.copyOfRange(stackFrames, 0, depth);
340                }
341                recordStackTrace(thread, stackFrames);
342            }
343        }
344
345        /**
346         * Record a new stack trace. The thread should have been
347         * previously registered with addStartThread.
348         */
349        private void recordStackTrace(Thread thread, StackTraceElement[] stackFrames) {
350            Integer threadId = threadIds.get(thread);
351            if (threadId == null) {
352                throw new IllegalArgumentException("Unknown thread " + thread);
353            }
354            mutableStackTrace.threadId = threadId;
355            mutableStackTrace.stackFrames = stackFrames;
356
357            int[] countCell = stackTraces.get(mutableStackTrace);
358            if (countCell == null) {
359                countCell = new int[1];
360                HprofData.StackTrace stackTrace
361                        = new HprofData.StackTrace(nextStackTraceId++, threadId, stackFrames);
362                hprofData.addStackTrace(stackTrace, countCell);
363            }
364            countCell[0]++;
365        }
366
367        private void updateThreadHistory(Thread[] oldThreads, Thread[] newThreads) {
368            // thread start/stop shouldn't happen too often and
369            // these aren't too big, so hopefully this approach
370            // won't be too slow...
371            Set<Thread> n = new HashSet<Thread>(Arrays.asList(newThreads));
372            Set<Thread> o = new HashSet<Thread>(Arrays.asList(oldThreads));
373
374            // added = new-old
375            Set<Thread> added = new HashSet<Thread>(n);
376            added.removeAll(o);
377
378            // removed = old-new
379            Set<Thread> removed = new HashSet<Thread>(o);
380            removed.removeAll(n);
381
382            for (Thread thread : added) {
383                if (thread == null) {
384                    continue;
385                }
386                if (thread == timerThread) {
387                    continue;
388                }
389                addStartThread(thread);
390            }
391            for (Thread thread : removed) {
392                if (thread == null) {
393                    continue;
394                }
395                if (thread == timerThread) {
396                    continue;
397                }
398                addEndThread(thread);
399            }
400        }
401
402        /**
403         * Record that a newly noticed thread.
404         */
405        private void addStartThread(Thread thread) {
406            if (thread == null) {
407                throw new NullPointerException("thread == null");
408            }
409            int threadId = nextThreadId++;
410            Integer old = threadIds.put(thread, threadId);
411            if (old != null) {
412                throw new IllegalArgumentException("Thread already registered as " + old);
413            }
414
415            String threadName = thread.getName();
416            // group will become null when thread is terminated
417            ThreadGroup group = thread.getThreadGroup();
418            String groupName = group == null ? null : group.getName();
419            ThreadGroup parentGroup = group == null ? null : group.getParent();
420            String parentGroupName = parentGroup == null ? null : parentGroup.getName();
421
422            HprofData.ThreadEvent event
423                    = HprofData.ThreadEvent.start(nextObjectId++, threadId,
424                                                  threadName, groupName, parentGroupName);
425            hprofData.addThreadEvent(event);
426        }
427
428        /**
429         * Record that a thread has disappeared.
430         */
431        private void addEndThread(Thread thread) {
432            if (thread == null) {
433                throw new NullPointerException("thread == null");
434            }
435            Integer threadId = threadIds.remove(thread);
436            if (threadId == null) {
437                throw new IllegalArgumentException("Unknown thread " + thread);
438            }
439            HprofData.ThreadEvent event = HprofData.ThreadEvent.end(threadId);
440            hprofData.addThreadEvent(event);
441        }
442    }
443}
444