1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.util; |
7 | |
8 | import java.io.ByteArrayOutputStream; |
9 | import java.io.FileInputStream; |
10 | import java.io.IOException; |
11 | import java.io.InputStream; |
12 | import java.io.InputStreamReader; |
13 | import java.io.LineNumberReader; |
14 | import java.io.OutputStream; |
15 | import java.io.Reader; |
16 | import java.io.StringReader; |
17 | import java.lang.instrument.Instrumentation; |
18 | import java.util.ArrayList; |
19 | import java.util.HashMap; |
20 | import java.util.Iterator; |
21 | import java.util.List; |
22 | import java.util.Map; |
23 | |
24 | /** |
25 | * A simple CPU profiling tool similar to java -Xrunhprof. It can be used |
26 | * in-process (to profile the current application) or as a standalone program |
27 | * (to profile a different process, or files containing full thread dumps). |
28 | */ |
29 | public class Profiler implements Runnable { |
30 | |
31 | private static Instrumentation instrumentation; |
32 | private static final String LINE_SEPARATOR = |
33 | System.getProperty("line.separator", "\n"); |
34 | private static final int MAX_ELEMENTS = 1000; |
35 | |
36 | public int interval = 2; |
37 | public int depth = 48; |
38 | public boolean paused; |
39 | public boolean sumClasses; |
40 | |
41 | private int pid; |
42 | |
43 | private final String[] ignoreLines = ( |
44 | "java," + |
45 | "sun," + |
46 | "com.sun.," + |
47 | "com.google.common.," + |
48 | "com.mongodb." |
49 | ).split(","); |
50 | private final String[] ignorePackages = ( |
51 | "java," + |
52 | "sun," + |
53 | "com.sun.," + |
54 | "com.google.common.," + |
55 | "com.mongodb." |
56 | ).split(","); |
57 | private final String[] ignoreThreads = ( |
58 | "java.lang.Object.wait," + |
59 | "java.lang.Thread.dumpThreads," + |
60 | "java.lang.Thread.getThreads," + |
61 | "java.lang.Thread.sleep," + |
62 | "java.lang.UNIXProcess.waitForProcessExit," + |
63 | "java.net.PlainDatagramSocketImpl.receive0," + |
64 | "java.net.PlainSocketImpl.accept," + |
65 | "java.net.PlainSocketImpl.socketAccept," + |
66 | "java.net.SocketInputStream.socketRead," + |
67 | "java.net.SocketOutputStream.socketWrite," + |
68 | "sun.awt.windows.WToolkit.eventLoop," + |
69 | "sun.misc.Unsafe.park," + |
70 | "sun.nio.ch.EPollArrayWrapper.epollWait," + |
71 | "sun.nio.ch.KQueueArrayWrapper.kevent0," + |
72 | "sun.nio.ch.ServerSocketChannelImpl.accept," + |
73 | "dalvik.system.VMStack.getThreadStackTrace," + |
74 | "dalvik.system.NativeStart.run" |
75 | ).split(","); |
76 | |
77 | private volatile boolean stop; |
78 | private final HashMap<String, Integer> counts = |
79 | new HashMap<String, Integer>(); |
80 | |
81 | /** |
82 | * The summary (usually one entry per package, unless sumClasses is enabled, |
83 | * in which case it's one entry per class). |
84 | */ |
85 | private final HashMap<String, Integer> summary = |
86 | new HashMap<String, Integer>(); |
87 | private int minCount = 1; |
88 | private int total; |
89 | private Thread thread; |
90 | private long start; |
91 | private long time; |
92 | private int threadDumps; |
93 | |
94 | /** |
95 | * This method is called when the agent is installed. |
96 | * |
97 | * @param agentArgs the agent arguments |
98 | * @param inst the instrumentation object |
99 | */ |
100 | public static void premain(String agentArgs, Instrumentation inst) { |
101 | instrumentation = inst; |
102 | } |
103 | |
104 | /** |
105 | * Get the instrumentation object if started as an agent. |
106 | * |
107 | * @return the instrumentation, or null |
108 | */ |
109 | public static Instrumentation getInstrumentation() { |
110 | return instrumentation; |
111 | } |
112 | |
113 | /** |
114 | * Run the command line version of the profiler. The JDK (jps and jstack) |
115 | * need to be in the path. |
116 | * |
117 | * @param args the process id of the process - if not set the java processes |
118 | * are listed |
119 | */ |
120 | public static void main(String... args) { |
121 | new Profiler().run(args); |
122 | } |
123 | |
124 | private void run(String... args) { |
125 | if (args.length == 0) { |
126 | System.out.println("Show profiling data"); |
127 | System.out.println("Usage: java " + getClass().getName() + |
128 | " <pid> | <stackTraceFileNames>"); |
129 | System.out.println("Processes:"); |
130 | String processes = exec("jps", "-l"); |
131 | System.out.println(processes); |
132 | return; |
133 | } |
134 | start = System.currentTimeMillis(); |
135 | if (args[0].matches("\\d+")) { |
136 | pid = Integer.parseInt(args[0]); |
137 | long last = 0; |
138 | while (true) { |
139 | tick(); |
140 | long t = System.currentTimeMillis(); |
141 | if (t - last > 5000) { |
142 | time = System.currentTimeMillis() - start; |
143 | System.out.println(getTopTraces(3)); |
144 | last = t; |
145 | } |
146 | } |
147 | } |
148 | try { |
149 | for (String file : args) { |
150 | Reader reader; |
151 | LineNumberReader r; |
152 | reader = new InputStreamReader( |
153 | new FileInputStream(file), "CP1252"); |
154 | r = new LineNumberReader(reader); |
155 | while (true) { |
156 | String line = r.readLine(); |
157 | if (line == null) { |
158 | break; |
159 | } else if (line.startsWith("Full thread dump")) { |
160 | threadDumps++; |
161 | } |
162 | } |
163 | reader.close(); |
164 | reader = new InputStreamReader( |
165 | new FileInputStream(file), "CP1252"); |
166 | r = new LineNumberReader(reader); |
167 | processList(readStackTrace(r)); |
168 | reader.close(); |
169 | } |
170 | System.out.println(getTopTraces(3)); |
171 | } catch (IOException e) { |
172 | throw new RuntimeException(e); |
173 | } |
174 | } |
175 | |
176 | private static List<Object[]> getRunnableStackTraces() { |
177 | ArrayList<Object[]> list = new ArrayList<Object[]>(); |
178 | Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces(); |
179 | for (Map.Entry<Thread, StackTraceElement[]> entry : map.entrySet()) { |
180 | Thread t = entry.getKey(); |
181 | if (t.getState() != Thread.State.RUNNABLE) { |
182 | continue; |
183 | } |
184 | StackTraceElement[] dump = entry.getValue(); |
185 | if (dump == null || dump.length == 0) { |
186 | continue; |
187 | } |
188 | list.add(dump); |
189 | } |
190 | return list; |
191 | } |
192 | |
193 | private static List<Object[]> readRunnableStackTraces(int pid) { |
194 | try { |
195 | String jstack = exec("jstack", "" + pid); |
196 | LineNumberReader r = new LineNumberReader( |
197 | new StringReader(jstack)); |
198 | return readStackTrace(r); |
199 | } catch (IOException e) { |
200 | throw new RuntimeException(e); |
201 | } |
202 | } |
203 | |
204 | private static List<Object[]> readStackTrace(LineNumberReader r) |
205 | throws IOException { |
206 | ArrayList<Object[]> list = new ArrayList<Object[]>(); |
207 | while (true) { |
208 | String line = r.readLine(); |
209 | if (line == null) { |
210 | break; |
211 | } |
212 | if (!line.startsWith("\"")) { |
213 | // not a thread |
214 | continue; |
215 | } |
216 | line = r.readLine(); |
217 | if (line == null) { |
218 | break; |
219 | } |
220 | line = line.trim(); |
221 | if (!line.startsWith("java.lang.Thread.State: RUNNABLE")) { |
222 | continue; |
223 | } |
224 | ArrayList<String> stack = new ArrayList<String>(); |
225 | while (true) { |
226 | line = r.readLine(); |
227 | if (line == null) { |
228 | break; |
229 | } |
230 | line = line.trim(); |
231 | if (line.startsWith("- ")) { |
232 | continue; |
233 | } |
234 | if (!line.startsWith("at ")) { |
235 | break; |
236 | } |
237 | line = line.substring(3).trim(); |
238 | stack.add(line); |
239 | } |
240 | if (stack.size() > 0) { |
241 | String[] s = stack.toArray(new String[stack.size()]); |
242 | list.add(s); |
243 | } |
244 | } |
245 | return list; |
246 | } |
247 | |
248 | private static String exec(String... args) { |
249 | ByteArrayOutputStream err = new ByteArrayOutputStream(); |
250 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
251 | try { |
252 | Process p = Runtime.getRuntime().exec(args); |
253 | copyInThread(p.getInputStream(), out); |
254 | copyInThread(p.getErrorStream(), err); |
255 | p.waitFor(); |
256 | String e = new String(err.toByteArray(), "UTF-8"); |
257 | if (e.length() > 0) { |
258 | throw new RuntimeException(e); |
259 | } |
260 | String output = new String(out.toByteArray(), "UTF-8"); |
261 | return output; |
262 | } catch (Exception e) { |
263 | throw new RuntimeException(e); |
264 | } |
265 | } |
266 | |
267 | private static void copyInThread(final InputStream in, |
268 | final OutputStream out) { |
269 | new Thread("Profiler stream copy") { |
270 | @Override |
271 | public void run() { |
272 | byte[] buffer = new byte[4096]; |
273 | try { |
274 | while (true) { |
275 | int len = in.read(buffer, 0, buffer.length); |
276 | if (len < 0) { |
277 | break; |
278 | } |
279 | out.write(buffer, 0, len); |
280 | } |
281 | } catch (Exception e) { |
282 | throw new RuntimeException(e); |
283 | } |
284 | } |
285 | }.start(); |
286 | } |
287 | |
288 | /** |
289 | * Start collecting profiling data. |
290 | * |
291 | * @return this |
292 | */ |
293 | public Profiler startCollecting() { |
294 | thread = new Thread(this, "Profiler"); |
295 | thread.setDaemon(true); |
296 | thread.start(); |
297 | return this; |
298 | } |
299 | |
300 | /** |
301 | * Stop collecting. |
302 | * |
303 | * @return this |
304 | */ |
305 | public Profiler stopCollecting() { |
306 | stop = true; |
307 | if (thread != null) { |
308 | try { |
309 | thread.join(); |
310 | } catch (InterruptedException e) { |
311 | // ignore |
312 | } |
313 | thread = null; |
314 | } |
315 | return this; |
316 | } |
317 | |
318 | @Override |
319 | public void run() { |
320 | start = System.currentTimeMillis(); |
321 | while (!stop) { |
322 | try { |
323 | tick(); |
324 | } catch (Throwable t) { |
325 | break; |
326 | } |
327 | } |
328 | time = System.currentTimeMillis() - start; |
329 | } |
330 | |
331 | private void tick() { |
332 | if (interval > 0) { |
333 | if (paused) { |
334 | return; |
335 | } |
336 | try { |
337 | Thread.sleep(interval); |
338 | } catch (Exception e) { |
339 | // ignore |
340 | } |
341 | } |
342 | |
343 | List<Object[]> list; |
344 | if (pid != 0) { |
345 | list = readRunnableStackTraces(pid); |
346 | } else { |
347 | list = getRunnableStackTraces(); |
348 | } |
349 | threadDumps++; |
350 | processList(list); |
351 | } |
352 | |
353 | private void processList(List<Object[]> list) { |
354 | for (Object[] dump : list) { |
355 | if (startsWithAny(dump[0].toString(), ignoreThreads)) { |
356 | continue; |
357 | } |
358 | StringBuilder buff = new StringBuilder(); |
359 | // simple recursive calls are ignored |
360 | String last = null; |
361 | boolean packageCounts = false; |
362 | for (int j = 0, i = 0; i < dump.length && j < depth; i++) { |
363 | String el = dump[i].toString(); |
364 | if (!el.equals(last) && !startsWithAny(el, ignoreLines)) { |
365 | last = el; |
366 | buff.append("at ").append(el).append(LINE_SEPARATOR); |
367 | if (!packageCounts && !startsWithAny(el, ignorePackages)) { |
368 | packageCounts = true; |
369 | int index = 0; |
370 | for (; index < el.length(); index++) { |
371 | char c = el.charAt(index); |
372 | if (c == '(' || Character.isUpperCase(c)) { |
373 | break; |
374 | } |
375 | } |
376 | if (index > 0 && el.charAt(index - 1) == '.') { |
377 | index--; |
378 | } |
379 | if (sumClasses) { |
380 | int m = el.indexOf('.', index + 1); |
381 | index = m >= 0 ? m : index; |
382 | } |
383 | String groupName = el.substring(0, index); |
384 | increment(summary, groupName, 0); |
385 | } |
386 | j++; |
387 | } |
388 | } |
389 | if (buff.length() > 0) { |
390 | minCount = increment(counts, buff.toString().trim(), minCount); |
391 | total++; |
392 | } |
393 | } |
394 | } |
395 | |
396 | private static boolean startsWithAny(String s, String[] prefixes) { |
397 | for (String p : prefixes) { |
398 | if (p.length() > 0 && s.startsWith(p)) { |
399 | return true; |
400 | } |
401 | } |
402 | return false; |
403 | } |
404 | |
405 | private static int increment(HashMap<String, Integer> map, String trace, |
406 | int minCount) { |
407 | Integer oldCount = map.get(trace); |
408 | if (oldCount == null) { |
409 | map.put(trace, 1); |
410 | } else { |
411 | map.put(trace, oldCount + 1); |
412 | } |
413 | while (map.size() > MAX_ELEMENTS) { |
414 | for (Iterator<Map.Entry<String, Integer>> ei = |
415 | map.entrySet().iterator(); ei.hasNext();) { |
416 | Map.Entry<String, Integer> e = ei.next(); |
417 | if (e.getValue() <= minCount) { |
418 | ei.remove(); |
419 | } |
420 | } |
421 | if (map.size() > MAX_ELEMENTS) { |
422 | minCount++; |
423 | } |
424 | } |
425 | return minCount; |
426 | } |
427 | |
428 | /** |
429 | * Get the top stack traces. |
430 | * |
431 | * @param count the maximum number of stack traces |
432 | * @return the stack traces. |
433 | */ |
434 | public String getTop(int count) { |
435 | stopCollecting(); |
436 | return getTopTraces(count); |
437 | } |
438 | |
439 | private String getTopTraces(int count) { |
440 | StringBuilder buff = new StringBuilder(); |
441 | buff.append("Profiler: top ").append(count).append(" stack trace(s) of "); |
442 | if (time > 0) { |
443 | buff.append(" of ").append(time).append(" ms"); |
444 | } |
445 | if (threadDumps > 0) { |
446 | buff.append(" of ").append(threadDumps).append(" thread dumps"); |
447 | } |
448 | buff.append(":").append(LINE_SEPARATOR); |
449 | if (counts.size() == 0) { |
450 | buff.append("(none)").append(LINE_SEPARATOR); |
451 | } |
452 | HashMap<String, Integer> copy = new HashMap<String, Integer>(counts); |
453 | appendTop(buff, copy, count, total, false); |
454 | buff.append("summary:").append(LINE_SEPARATOR); |
455 | copy = new HashMap<String, Integer>(summary); |
456 | appendTop(buff, copy, count, total, true); |
457 | buff.append('.'); |
458 | return buff.toString(); |
459 | } |
460 | |
461 | private static void appendTop(StringBuilder buff, |
462 | HashMap<String, Integer> map, int count, int total, boolean table) { |
463 | for (int x = 0, min = 0;;) { |
464 | int highest = 0; |
465 | Map.Entry<String, Integer> best = null; |
466 | for (Map.Entry<String, Integer> el : map.entrySet()) { |
467 | if (el.getValue() > highest) { |
468 | best = el; |
469 | highest = el.getValue(); |
470 | } |
471 | } |
472 | if (best == null) { |
473 | break; |
474 | } |
475 | map.remove(best.getKey()); |
476 | if (++x >= count) { |
477 | if (best.getValue() < min) { |
478 | break; |
479 | } |
480 | min = best.getValue(); |
481 | } |
482 | int c = best.getValue(); |
483 | int percent = 100 * c / Math.max(total, 1); |
484 | if (table) { |
485 | if (percent > 1) { |
486 | buff.append(percent). |
487 | append("%: ").append(best.getKey()). |
488 | append(LINE_SEPARATOR); |
489 | } |
490 | } else { |
491 | buff.append(c).append('/').append(total).append(" ("). |
492 | append(percent). |
493 | append("%):").append(LINE_SEPARATOR). |
494 | append(best.getKey()). |
495 | append(LINE_SEPARATOR); |
496 | } |
497 | } |
498 | } |
499 | |
500 | } |