/**
* Copyright 2014 Alexey Ragozin
*
* 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 org.gridkit.jvmtool.cmd;
import static org.gridkit.jvmtool.stacktrace.analytics.ThreadDumpAggregatorFactory.COMMON;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.Thread.State;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Pattern;
import org.gridkit.jvmtool.CategorizerParser;
import org.gridkit.jvmtool.StackHisto;
import org.gridkit.jvmtool.ThreadDumpSource;
import org.gridkit.jvmtool.cli.CommandLauncher;
import org.gridkit.jvmtool.cli.CommandLauncher.CmdRef;
import org.gridkit.jvmtool.codec.stacktrace.ThreadSnapshotEvent;
import org.gridkit.jvmtool.event.EventReader;
import org.gridkit.jvmtool.stacktrace.StackFrame;
import org.gridkit.jvmtool.stacktrace.StackFrameList;
import org.gridkit.jvmtool.stacktrace.analytics.CachingFilterFactory;
import org.gridkit.jvmtool.stacktrace.analytics.ParserException;
import org.gridkit.jvmtool.stacktrace.analytics.SimpleCategorizer;
import org.gridkit.jvmtool.stacktrace.analytics.ThreadDumpAggregatorFactory;
import org.gridkit.jvmtool.stacktrace.analytics.ThreadSnapshotCategorizer;
import org.gridkit.jvmtool.stacktrace.analytics.ThreadSnapshotFilter;
import org.gridkit.jvmtool.stacktrace.analytics.ThreadSplitAggregator;
import org.gridkit.jvmtool.stacktrace.analytics.TraceFilterPredicateParser;
import org.gridkit.jvmtool.stacktrace.analytics.flame.FlameGraphGenerator;
import org.gridkit.jvmtool.stacktrace.analytics.flame.RainbowColorPicker;
import org.gridkit.util.formating.Formats;
import org.gridkit.util.formating.TextTable;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;
public class StackSampleAnalyzerCmd implements CmdRef {
@Override
public String getCommandName() {
return "ssa";
}
@Override
public Runnable newCommand(CommandLauncher host) {
return new SSA(host);
}
@Parameters(commandDescription = "[Stack Sample Analyzer] Analyzing stack trace dumps")
public static class SSA implements Runnable {
@ParametersDelegate
private final CommandLauncher host;
@ParametersDelegate
private final ThreadDumpSource dumpSource;
@Parameter(names={"-cf", "--categorizer-file"}, required = false, description="Path to file with stack trace categorization definition")
private String categorizerFile = null;
@Parameter(names={"-nc", "--named-class"}, required = false, variableArity = true, description="May be used with some commands to define name stack trace classes\nUse <name>=<filter expression> notation")
private List<String> namedClasses = new ArrayList<String>();
@Parameter(names={"-tz", "--time-zone"}, required = false, description="Time zone used for timestamps")
private String timeZone = "UTC";
@Parameter(names={"-co", "--csv-output"}, required = false, description="Output data in CSV format")
private boolean csvOutput = false;
private List<SsaCmd> allCommands = new ArrayList<SsaCmd>();
@ParametersDelegate
private SsaCmd print = new PrintCmd();
@ParametersDelegate
private SsaCmd histo = new HistoCmd();
@ParametersDelegate
private SsaCmd flame = new FlameCmd();
@ParametersDelegate
private SsaCmd csummary = new CategorizeCmd();
@ParametersDelegate
private SsaCmd threadInfo = new ThreadInfoCmd();
@ParametersDelegate
private SsaCmd help = new HelpCmd();
ThreadSnapshotCategorizer categorizer;
public SSA(CommandLauncher host) {
this.host = host;
this.dumpSource = new ThreadDumpSource(host);
}
@Override
public void run() {
try {
List<Runnable> action = new ArrayList<Runnable>();
for(SsaCmd cmd: allCommands) {
if (cmd.isSelected()) {
action.add(cmd);
}
}
if (action.isEmpty() || action.size() > 1) {
host.failAndPrintUsage("You should choose one of " + allCommands);
}
dumpSource.setTimeZone(timeZone());
if (categorizerFile != null) {
if (!namedClasses.isEmpty()) {
host.failAndPrintUsage("You eigther should specify categorizer (-cf) or named classed (-nc)");
}
try {
FileReader csource = new FileReader(categorizerFile);
SimpleCategorizer sc = new SimpleCategorizer();
CachingFilterFactory cff = new CachingFilterFactory();
CategorizerParser.loadCategories(csource, sc, false, cff);
categorizer = sc;
} catch (ParserException e) {
throw host.fail("Failed to parse filter expression at [" + e.getOffset() + "] : " + e.getMessage(), e.getParseText());
}
}
action.get(0).run();
} catch (Exception e) {
host.fail(e.toString(), e);
}
}
TimeZone timeZone() {
return TimeZone.getTimeZone(timeZone);
}
Map<String, ThreadSnapshotFilter> getNamedClasses() {
if (namedClasses.isEmpty()) {
return new HashMap<String, ThreadSnapshotFilter>();
}
else {
CachingFilterFactory factory = new CachingFilterFactory();
Map<String, ThreadSnapshotFilter> classes = new LinkedHashMap<String, ThreadSnapshotFilter>();
for(String nc: namedClasses) {
int n = nc.indexOf('=');
if (n < 0) {
throw host.fail("Cannot parse named class", "[" + nc + "]", "Required format NAME=FILTER_EXPRESSION");
}
String name = nc.substring(0, n);
String filter = nc.substring(n + 1);
if (classes.containsKey(name)) {
throw host.fail("Duplicated class name [" + name + "]");
}
try {
ThreadSnapshotFilter tf = TraceFilterPredicateParser.parseFilter(filter, factory);
classes.put(name, tf);
} catch (ParserException e) {
throw host.fail("Cannot parse named class", "[" + nc + "]", e.getMessage());
}
}
return classes;
}
}
ThreadSnapshotFilter parseFilter(String filter) {
CachingFilterFactory factory = new CachingFilterFactory();
ThreadSnapshotFilter tf = TraceFilterPredicateParser.parseFilter(filter, factory);
return tf;
}
EventReader<ThreadSnapshotEvent> getFilteredReader() {
return dumpSource.getFilteredReader();
}
EventReader<ThreadSnapshotEvent> getUnclassifiedReader() {
return dumpSource.getUnclassifiedReader();
}
abstract class SsaCmd implements Runnable {
public SsaCmd() {
allCommands.add(this);
}
public abstract boolean isSelected();
}
class PrintCmd extends SsaCmd {
@Parameter(names={"--print"}, description="Print traces from file")
boolean run;
@Override
public boolean isSelected() {
return run;
}
@Override
public void run() {
try {
EventReader<ThreadSnapshotEvent> reader = getFilteredReader();
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
fmt.setTimeZone(timeZone());
StringBuilder threadHeader = new StringBuilder();
StringBuilder stackFrameBuffer = new StringBuilder();
for(ThreadSnapshotEvent e: reader) {
String timestamp = fmt.format(e.timestamp());
threadHeader
.append("Thread [")
.append(e.threadId())
.append("] ");
if (e.threadState() != null) {
threadHeader.append(e.threadState()).append(' ');
}
threadHeader.append("at ").append(timestamp);
if (e.threadName() != null) {
threadHeader.append(" - ").append(e.threadName());
}
System.out.println(threadHeader);
StackFrameList trace = e.stackTrace();
for(StackFrame frame: trace) {
frame.toString(stackFrameBuffer);
System.out.println(stackFrameBuffer);
stackFrameBuffer.setLength(0);
}
System.out.println();
threadHeader.setLength(0);
}
} catch (Exception e) {
host.fail(e.toString(), e);
}
}
public String toString() {
return "--print";
}
}
class HistoCmd extends SsaCmd {
@Parameter(names={"--histo"}, description="Print frame histogram")
boolean run;
@Override
public boolean isSelected() {
return run;
}
@Override
public void run() {
try {
StackHisto histo = new StackHisto();
for(Map.Entry<String, ThreadSnapshotFilter> entry: getNamedClasses().entrySet()) {
histo.addCondition(entry.getKey(), entry.getValue());
}
EventReader<ThreadSnapshotEvent> reader = getFilteredReader();
int n = 0;
for(ThreadSnapshotEvent e: reader) {
StackFrameList trace = e.stackTrace();
histo.feed(trace);
++n;
}
if (n > 0) {
if (csvOutput) {
System.out.println(histo.formatHistoToCSV());
}
else {
System.out.println(histo.formatHisto());
}
}
else {
System.out.println("No data");
}
} catch (Exception e) {
host.fail(e.toString(), e);
}
}
public String toString() {
return "--histo";
}
}
class FlameCmd extends SsaCmd {
@Parameter(names={"--flame"}, description="Export flame graph to SVG format")
boolean run;
@Parameter(names={"--title"}, description="Flame graph title")
String title = "Flame Graph";
@Parameter(names={"--width"}, description="Flame graph width in pixels")
int width = 1200;
@Parameter(names={"-rc", "--rainbow"}, variableArity = true, description="List of filters for rainbow coloring")
List<String> rainbow;
@Override
public boolean isSelected() {
return run;
}
@Override
public void run() {
try {
FlameGraphGenerator fg = new FlameGraphGenerator();
if (rainbow != null && rainbow.size() > 0) {
ThreadSnapshotFilter[] filters = new ThreadSnapshotFilter[rainbow.size()];
CachingFilterFactory factory = new CachingFilterFactory();
for (int i = 0; i != rainbow.size(); ++i) {
filters[i] = TraceFilterPredicateParser.parseFilter(rainbow.get(i), factory);
}
fg.setColorPicker(new RainbowColorPicker(filters));
}
EventReader<ThreadSnapshotEvent> reader = getFilteredReader();
int n = 0;
for(ThreadSnapshotEvent e: reader) {
StackFrameList trace = e.stackTrace();
fg.feed(trace);
++n;
}
if (n > 0) {
Writer w = new OutputStreamWriter(System.out);
fg.renderSVG(title, width, w);
w.flush();
}
else {
System.out.println("No data");
}
} catch (Exception e) {
host.fail(e.toString(), e);
}
}
public String toString() {
return "--histo";
}
}
class CategorizeCmd extends SsaCmd {
@Parameter(names={"--categorize"}, description="Print summary for provided categorization")
boolean run;
@Override
public boolean isSelected() {
return run;
}
@Override
public void run() {
try {
ThreadSnapshotCategorizer cat = categorizer;
if (categorizer == null) {
if (!namedClasses.isEmpty()) {
SimpleCategorizer sc = new SimpleCategorizer();
Map<String, ThreadSnapshotFilter> nf = getNamedClasses();
for(String fn: nf.keySet()) {
sc.addCategory(fn, nf.get(fn));
}
cat = sc;
}
}
if (cat == null) {
throw host.fail("Neigther -cf nor -nc. Eigther of them is required.");
}
List<String> bucketNames = new ArrayList<String>(cat.getCategories());
long[] counters = new long[bucketNames.size()];
long total = 0;
EventReader<ThreadSnapshotEvent> reader = getUnclassifiedReader();
for(ThreadSnapshotEvent e: reader) {
String cl = cat.categorize(e);
if (cl != null) {
++total;
++counters[bucketNames.indexOf(cl)];
}
}
TextTable tt = new TextTable();
String tab = csvOutput ? "" : "\t ";
tt.addRow("Total samples", tab + total, tab + "100.00%");
for(int i = 0; i != counters.length; ++i) {
tt.addRow(bucketNames.get(i), tab + counters[i], tab + (counters[i] == 0 ? "0.00%" : String.format("%.2f%%", (100f * counters[i]) / total)));
}
if (csvOutput) {
System.out.println(tt.formatToCSV());
}
else {
System.out.println(tt.formatTextTableUnbordered(Integer.MAX_VALUE));
}
} catch (Exception e) {
host.fail(e.toString(), e);
}
}
public String toString() {
return "--categorize";
}
}
class ThreadInfoCmd extends SsaCmd {
@Parameter(names={"--thread-info"}, description="Per thread info summary")
boolean run;
@Parameter(names={"-si", "--summary-info"}, variableArity = true, description="List of summaries")
List<String> summaryInfo;
@Override
public boolean isSelected() {
return run;
}
List<String> summaryNames = new ArrayList<String>();
List<ThreadDumpAggregatorFactory> summaries = new ArrayList<ThreadDumpAggregatorFactory>();
List<SummaryFormater> summaryFormaters = new ArrayList<SummaryFormater>();
void add(String name, ThreadDumpAggregatorFactory summary) {
add(name, summary, new DefaultFormater());
}
void add(String name, ThreadDumpAggregatorFactory summary, SummaryFormater formater) {
summaryNames.add(name + " ");
summaries.add(summary);
summaryFormaters.add(formater);
}
@Override
public void run() {
try {
if (summaryInfo == null || summaryInfo.isEmpty()) {
add("Name", COMMON.name());
add("Count", COMMON.count(), new RightFormater());
add("On CPU", COMMON.cpu(), new PercentFormater());
add("Alloc ", COMMON.alloc(), new MemRateFormater());
add("RUNNABLE", COMMON.threadState(State.RUNNABLE), new PercentFormater());
add("Native", COMMON.inNative(), new PercentFormater());
}
else {
for(String si: summaryInfo) {
addSummary(si);
}
}
ThreadSplitAggregator threadAgg = new ThreadSplitAggregator(summaries.toArray(new ThreadDumpAggregatorFactory[0]));
EventReader<ThreadSnapshotEvent> reader = getFilteredReader();
for(ThreadSnapshotEvent e: reader) {
threadAgg.feed(e);
}
TextTable tt = new TextTable();
tt.addRow(summaryNames);
int n = 0;
for(Object[] row: threadAgg.report()) {
++n;
String[] frow = new String[summaries.size()];
for(int i = 0; i != summaries.size(); ++i) {
SummaryFormater sf = summaryFormaters.get(i);
frow[i] = sf.toString(row[i + 2]) + " ";
}
tt.addRow(frow);
}
if (n > 0) {
if (csvOutput) {
System.out.println(tt.formatToCSV());
}
else {
System.out.println(tt.formatTextTableUnbordered(80));
}
}
else {
System.out.println("No data");
}
} catch (Exception e) {
host.fail(e.toString(), e);
}
}
private void addSummary(String si) {
si = si.trim();
if ("NAME".equals(si)) {
add("Name", COMMON.name());
}
if (si.startsWith("NAME") && si.indexOf('=') < 0) {
int n = Integer.valueOf(si.substring(4));
add("Name", COMMON.name(n));
}
else if ("COUNT".equals(si)) {
add("Count", COMMON.count(), new RightFormater());
}
else if ("TSMIN".equals(si)) {
add("First time", COMMON.minTimestamp(), new DateFormater(timeZone()));
}
else if ("TSMAX".equals(si)) {
add("Last time", COMMON.maxTimestamp(), new DateFormater(timeZone()));
}
else if ("CPU".equals(si)) {
add("On CPU", COMMON.cpu(), new PercentFormater());
}
else if ("ALLOC".equals(si)) {
add("Alloc ", COMMON.alloc(), new MemRateFormater());
}
else if (si.startsWith("S:")) {
State st = State.valueOf(si.substring(2));
add(st.toString(), COMMON.threadState(st), new PercentFormater());
}
else if ("NATIVE".equals(si)) {
add("Native", COMMON.inNative(), new PercentFormater());
}
else if (Pattern.matches(".*=.*", si)) {
String[] p = si.split("[=]");
if (p.length != 2) {
badSummary(si);
}
ThreadSnapshotFilter ts = parseFilter(p[1]);
add(p[0], COMMON.threadFilter(ts), new PercentFormater());
}
else if ("FREQ".equals(si)) {
add("Freq.", COMMON.frequency(), new DecimalFormater(1));
}
else if ("FREQ_HM".equals(si)) {
add("Freq. (1/HM)", COMMON.frequencyHM(), new DecimalFormater(1));
}
else if ("GAP_CHM".equals(si)) {
add("Gap CHM", COMMON.periodCHM(), new DecimalFormater(3));
}
else {
badSummary(si);
}
}
private void badSummary(String si) {
host.fail("Unknown summary '" + si + "'",
"Allowed summaries are",
" NAME",
" NAME<len>",
" COUNT",
" TSMIN",
" TSMAX",
" CPU",
" ALLOC",
" NATIVE",
" FREQ",
" FREQ_HM",
" GAP_CHM",
" S:[RUNNABLE|BLOCKED|WAITING|TIMED_WAITING]",
" <name>=<filter expression>"
);
}
public String toString() {
return "--categorize";
}
}
public class HelpCmd extends SsaCmd {
@Parameter(names={"--ssa-help"}, description="Additional information about SSA")
boolean run;
@Override
public boolean isSelected() {
return run;
}
@Override
public void run() {
try {
InputStream is = getClass().getResourceAsStream("ssa-help.md");
if (is == null) {
System.out.println("Failed to load help");
return;
}
System.out.println();
byte[] buf = new byte[4 << 10];
while(true) {
int n = is.read(buf);
if (n < 0) {
break;
}
else {
System.out.write(buf, 0, n);
}
}
System.out.println();
} catch (IOException e) {
System.out.println("Failed to load help");
}
}
public String toString() {
return "--ssa-help";
}
}
}
interface SummaryFormater {
public String toString(Object summary);
}
static class DefaultFormater implements SummaryFormater {
@Override
public String toString(Object summary) {
return String.valueOf(summary);
}
}
static class RightFormater implements SummaryFormater {
@Override
public String toString(Object summary) {
return "\t" + String.valueOf(summary);
}
}
static class DecimalFormater implements SummaryFormater {
int n;
public DecimalFormater(int n) {
this.n = n;
}
@Override
public String toString(Object summary) {
if (summary instanceof Long) {
return "\t" + summary;
}
else if (summary instanceof Number) {
return "\t" + String.format("%." + n +"f", ((Number) summary).doubleValue());
}
else {
return "";
}
}
}
static class PercentFormater implements SummaryFormater {
@Override
public String toString(Object summary) {
if (summary instanceof Number) {
double d = ((Number) summary).doubleValue();
if (!Double.isNaN(d)) {
return String.format("\t%.1f%%", 100 * d);
}
}
return "";
}
}
static class MemRateFormater implements SummaryFormater {
@Override
public String toString(Object summary) {
if (summary instanceof Number) {
double d = ((Number) summary).doubleValue();
if (!Double.isNaN(d)) {
return Formats.toMemorySize((long)d) + "/s";
}
}
return "";
}
}
static class DateFormater implements SummaryFormater {
SimpleDateFormat fmt;
public DateFormater(TimeZone tz) {
fmt = new SimpleDateFormat("yyyy.MM.dd_HH:mm:ss");
fmt.setTimeZone(tz);
}
@Override
public String toString(Object summary) {
if (summary instanceof Long) {
return fmt.format(summary);
}
else {
return "";
}
}
}
}