/*
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.util;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.lang.instrument.Instrumentation;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

A simple CPU profiling tool similar to java -Xrunhprof. It can be used in-process (to profile the current application) or as a standalone program (to profile a different process, or files containing full thread dumps).
/** * A simple CPU profiling tool similar to java -Xrunhprof. It can be used * in-process (to profile the current application) or as a standalone program * (to profile a different process, or files containing full thread dumps). */
public class Profiler implements Runnable { private static Instrumentation instrumentation; private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n"); private static final int MAX_ELEMENTS = 1000; public int interval = 2; public int depth = 48; public boolean paused; public boolean sumClasses; public boolean sumMethods; private int pid; private final String[] ignoreLines = ( "java," + "sun," + "com.sun.," + "com.google.common.," + "com.mongodb.," + "org.bson.," ).split(","); private final String[] ignorePackages = ( "java," + "sun," + "com.sun.," + "com.google.common.," + "com.mongodb.," + "org.bson" ).split(","); private final String[] ignoreThreads = ( "java.lang.Object.wait," + "java.lang.Thread.dumpThreads," + "java.lang.Thread.getThreads," + "java.lang.Thread.sleep," + "java.lang.UNIXProcess.waitForProcessExit," + "java.net.PlainDatagramSocketImpl.receive0," + "java.net.PlainSocketImpl.accept," + "java.net.PlainSocketImpl.socketAccept," + "java.net.SocketInputStream.socketRead," + "java.net.SocketOutputStream.socketWrite," + "org.eclipse.jetty.io.nio.SelectorManager$SelectSet.doSelect," + "sun.awt.windows.WToolkit.eventLoop," + "sun.misc.Unsafe.park," + "sun.nio.ch.EPollArrayWrapper.epollWait," + "sun.nio.ch.KQueueArrayWrapper.kevent0," + "sun.nio.ch.ServerSocketChannelImpl.accept," + "dalvik.system.VMStack.getThreadStackTrace," + "dalvik.system.NativeStart.run" ).split(","); private volatile boolean stop; private final HashMap<String, Integer> counts = new HashMap<>();
The summary (usually one entry per package, unless sumClasses is enabled, in which case it's one entry per class).
/** * The summary (usually one entry per package, unless sumClasses is enabled, * in which case it's one entry per class). */
private final HashMap<String, Integer> summary = new HashMap<>(); private int minCount = 1; private int total; private Thread thread; private long start; private long time; private int threadDumps;
This method is called when the agent is installed.
Params:
  • agentArgs – the agent arguments
  • inst – the instrumentation object
/** * This method is called when the agent is installed. * * @param agentArgs the agent arguments * @param inst the instrumentation object */
public static void premain(@SuppressWarnings("unused") String agentArgs, Instrumentation inst) { instrumentation = inst; }
Get the instrumentation object if started as an agent.
Returns:the instrumentation, or null
/** * Get the instrumentation object if started as an agent. * * @return the instrumentation, or null */
public static Instrumentation getInstrumentation() { return instrumentation; }
Run the command line version of the profiler. The JDK (jps and jstack) need to be in the path.
Params:
  • args – the process id of the process - if not set the java processes are listed
/** * Run the command line version of the profiler. The JDK (jps and jstack) * need to be in the path. * * @param args the process id of the process - if not set the java processes * are listed */
public static void main(String... args) { new Profiler().run(args); } private void run(String... args) { if (args.length == 0) { System.out.println("Show profiling data"); System.out.println("Usage: java " + getClass().getName() + " <pid> | <stackTraceFileNames>"); System.out.println("Processes:"); String processes = exec("jps", "-l"); System.out.println(processes); return; } start = System.nanoTime(); if (args[0].matches("\\d+")) { pid = Integer.parseInt(args[0]); long last = 0; while (true) { tick(); long t = System.nanoTime(); if (t - last > TimeUnit.SECONDS.toNanos(5)) { time = System.nanoTime() - start; System.out.println(getTopTraces(3)); last = t; } } } try { for (String arg : args) { if (arg.startsWith("-")) { if ("-classes".equals(arg)) { sumClasses = true; } else if ("-methods".equals(arg)) { sumMethods = true; } else if ("-packages".equals(arg)) { sumClasses = false; sumMethods = false; } else { throw new IllegalArgumentException(arg); } continue; } try (Reader reader = new InputStreamReader(new FileInputStream(arg))) { LineNumberReader r = new LineNumberReader(reader); while (true) { String line = r.readLine(); if (line == null) { break; } else if (line.startsWith("Full thread dump")) { threadDumps++; } } } try (Reader reader = new InputStreamReader(new FileInputStream(arg))) { LineNumberReader r = new LineNumberReader(reader); processList(readStackTrace(r)); } } System.out.println(getTopTraces(5)); } catch (IOException e) { throw new RuntimeException(e); } } private static List<Object[]> getRunnableStackTraces() { ArrayList<Object[]> list = new ArrayList<>(); Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces(); for (Map.Entry<Thread, StackTraceElement[]> entry : map.entrySet()) { Thread t = entry.getKey(); if (t.getState() != Thread.State.RUNNABLE) { continue; } StackTraceElement[] dump = entry.getValue(); if (dump == null || dump.length == 0) { continue; } list.add(dump); } return list; } private static List<Object[]> readRunnableStackTraces(int pid) { try { String jstack = exec("jstack", Integer.toString(pid)); LineNumberReader r = new LineNumberReader( new StringReader(jstack)); return readStackTrace(r); } catch (IOException e) { throw new RuntimeException(e); } } private static List<Object[]> readStackTrace(LineNumberReader r) throws IOException { ArrayList<Object[]> list = new ArrayList<>(); while (true) { String line = r.readLine(); if (line == null) { break; } if (!line.startsWith("\"")) { // not a thread continue; } line = r.readLine(); if (line == null) { break; } line = line.trim(); if (!line.startsWith("java.lang.Thread.State: RUNNABLE")) { continue; } ArrayList<String> stack = new ArrayList<>(); while (true) { line = r.readLine(); if (line == null) { break; } line = line.trim(); if (line.startsWith("- ")) { continue; } if (!line.startsWith("at ")) { break; } line = StringUtils.trimSubstring(line, 3); stack.add(line); } if (!stack.isEmpty()) { String[] s = stack.toArray(new String[0]); list.add(s); } } return list; } private static String exec(String... args) { ByteArrayOutputStream err = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { Process p = Runtime.getRuntime().exec(args); copyInThread(p.getInputStream(), out); copyInThread(p.getErrorStream(), err); p.waitFor(); String e = new String(err.toByteArray(), StandardCharsets.UTF_8); if (e.length() > 0) { throw new RuntimeException(e); } return new String(out.toByteArray(), StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException(e); } } private static void copyInThread(final InputStream in, final OutputStream out) { new Thread("Profiler stream copy") { @Override public void run() { byte[] buffer = new byte[4096]; try { while (true) { int len = in.read(buffer, 0, buffer.length); if (len < 0) { break; } out.write(buffer, 0, len); } } catch (Exception e) { throw new RuntimeException(e); } } }.start(); }
Start collecting profiling data.
Returns:this
/** * Start collecting profiling data. * * @return this */
public Profiler startCollecting() { thread = new Thread(this, "Profiler"); thread.setDaemon(true); thread.start(); return this; }
Stop collecting.
Returns:this
/** * Stop collecting. * * @return this */
public Profiler stopCollecting() { stop = true; if (thread != null) { try { thread.join(); } catch (InterruptedException e) { // ignore } thread = null; } return this; } @Override public void run() { start = System.nanoTime(); while (!stop) { try { tick(); } catch (Throwable t) { break; } } time = System.nanoTime() - start; } private void tick() { if (interval > 0) { if (paused) { return; } try { Thread.sleep(interval, 0); } catch (Exception e) { // ignore } } List<Object[]> list; if (pid != 0) { list = readRunnableStackTraces(pid); } else { list = getRunnableStackTraces(); } threadDumps++; processList(list); } private void processList(List<Object[]> list) { for (Object[] dump : list) { if (startsWithAny(dump[0].toString(), ignoreThreads)) { continue; } StringBuilder buff = new StringBuilder(); // simple recursive calls are ignored String last = null; boolean packageCounts = false; for (int j = 0, i = 0; i < dump.length && j < depth; i++) { String el = dump[i].toString(); if (!el.equals(last) && !startsWithAny(el, ignoreLines)) { last = el; buff.append("at ").append(el).append(LINE_SEPARATOR); if (!packageCounts && !startsWithAny(el, ignorePackages)) { packageCounts = true; int index = 0; for (; index < el.length(); index++) { char c = el.charAt(index); if (c == '(' || Character.isUpperCase(c)) { break; } } if (index > 0 && el.charAt(index - 1) == '.') { index--; } if (sumClasses) { int m = el.indexOf('.', index + 1); index = m >= 0 ? m : index; } if (sumMethods) { int m = el.indexOf('(', index + 1); index = m >= 0 ? m : index; } String groupName = el.substring(0, index); increment(summary, groupName, 0); } j++; } } if (buff.length() > 0) { minCount = increment(counts, buff.toString().trim(), minCount); total++; } } } private static boolean startsWithAny(String s, String[] prefixes) { for (String p : prefixes) { if (p.length() > 0 && s.startsWith(p)) { return true; } } return false; } private static int increment(HashMap<String, Integer> map, String trace, int minCount) { Integer oldCount = map.get(trace); if (oldCount == null) { map.put(trace, 1); } else { map.put(trace, oldCount + 1); } while (map.size() > MAX_ELEMENTS) { for (Iterator<Map.Entry<String, Integer>> ei = map.entrySet().iterator(); ei.hasNext();) { Map.Entry<String, Integer> e = ei.next(); if (e.getValue() <= minCount) { ei.remove(); } } if (map.size() > MAX_ELEMENTS) { minCount++; } } return minCount; }
Get the top stack traces.
Params:
  • count – the maximum number of stack traces
Returns:the stack traces.
/** * Get the top stack traces. * * @param count the maximum number of stack traces * @return the stack traces. */
public String getTop(int count) { stopCollecting(); return getTopTraces(count); } private String getTopTraces(int count) { StringBuilder buff = new StringBuilder(); buff.append("Profiler: top ").append(count).append(" stack trace(s) of "); if (time > 0) { buff.append(" of ").append(TimeUnit.NANOSECONDS.toMillis(time)).append(" ms"); } if (threadDumps > 0) { buff.append(" of ").append(threadDumps).append(" thread dumps"); } buff.append(":").append(LINE_SEPARATOR); if (counts.size() == 0) { buff.append("(none)").append(LINE_SEPARATOR); } HashMap<String, Integer> copy = new HashMap<>(counts); appendTop(buff, copy, count, total, false); buff.append("summary:").append(LINE_SEPARATOR); copy = new HashMap<>(summary); appendTop(buff, copy, count, total, true); buff.append('.'); return buff.toString(); } private static void appendTop(StringBuilder buff, HashMap<String, Integer> map, int count, int total, boolean table) { for (int x = 0, min = 0;;) { int highest = 0; Map.Entry<String, Integer> best = null; for (Map.Entry<String, Integer> el : map.entrySet()) { if (el.getValue() > highest) { best = el; highest = el.getValue(); } } if (best == null) { break; } map.remove(best.getKey()); if (++x >= count) { if (best.getValue() < min) { break; } min = best.getValue(); } int c = best.getValue(); int percent = 100 * c / Math.max(total, 1); if (table) { if (percent > 1) { buff.append(percent). append("%: ").append(best.getKey()). append(LINE_SEPARATOR); } } else { buff.append(c).append('/').append(total).append(" ("). append(percent). append("%):").append(LINE_SEPARATOR). append(best.getKey()). append(LINE_SEPARATOR); } } } }