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