Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.oracle.truffle.tools.profiler.impl.CPUSamplerCLI Maven / Gradle / Ivy
/*
* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.truffle.tools.profiler.impl;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import org.graalvm.options.OptionCategory;
import org.graalvm.options.OptionKey;
import org.graalvm.options.OptionStability;
import org.graalvm.options.OptionType;
import org.graalvm.options.OptionValues;
import org.graalvm.shadowed.org.json.JSONArray;
import org.graalvm.shadowed.org.json.JSONObject;
import com.oracle.truffle.api.Option;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.tools.profiler.CPUSampler;
import com.oracle.truffle.tools.profiler.CPUSamplerData;
import com.oracle.truffle.tools.profiler.ProfilerNode;
@Option.Group(CPUSamplerInstrument.ID)
class CPUSamplerCLI extends ProfilerCLI {
public static final long MILLIS_TO_NANOS = 1_000_000L;
public static final double MAX_OVERHEAD_WARNING_THRESHOLD = 0.2;
public static final String DEFAULT_FLAMEGRAPH_FILE = "flamegraph.svg";
enum Output {
HISTOGRAM,
CALLTREE,
JSON,
FLAMEGRAPH;
private static String valueList() {
StringBuilder message = new StringBuilder();
Output[] values = Output.values();
for (int i = 0; i < values.length; i++) {
Output value = values[i];
message.append(value.name().toLowerCase());
message.append(i < values.length - 1 ? ", " : "");
}
return message.toString();
}
private static Output fromString(String s) {
try {
return Output.valueOf(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Output can be: " + Output.valueList() + ".");
}
}
@Override
public String toString() {
return this.name().toLowerCase();
}
}
static final OptionType ENABLE_OPTION_TYPE = new OptionType<>("Enable",
s -> {
switch (s) {
case "":
case "true":
return new EnableOptionData(true, null);
case "false":
return new EnableOptionData(false, null);
default: {
try {
Output output = Output.fromString(s);
return new EnableOptionData(true, output);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("CPUSampler can be configured with the following values: true, false, " + Output.valueList() + ".");
}
}
}
});
static final class EnableOptionData {
final boolean enabled;
final Output output;
private EnableOptionData(boolean enabled, Output output) {
this.enabled = enabled;
this.output = output;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EnableOptionData that = (EnableOptionData) o;
return enabled == that.enabled && output == that.output;
}
@Override
public int hashCode() {
return Objects.hash(enabled, output);
}
@Override
public String toString() {
return enabled ? output.toString() : "false";
}
}
static final OptionType CLI_OUTPUT_TYPE = new OptionType<>("Output", Output::fromString);
static final OptionType SHOW_TIERS_OUTPUT_TYPE = new OptionType<>("ShowTiers",
new Function() {
@Override
public int[] apply(String s) {
if ("false".equals(s)) {
return null;
}
if ("true".equals(s)) {
return new int[0];
}
try {
String[] tierStrings = s.split(",");
int[] tiers = new int[tierStrings.length];
for (int i = 0; i < tierStrings.length; i++) {
tiers[i] = Integer.parseInt(tierStrings[i]);
}
return tiers;
} catch (NumberFormatException e) {
// Ignored
}
throw new IllegalArgumentException("ShowTiers can be: true, false or a comma separated list of integers");
}
});
@Option(name = "", help = "Enable/Disable the CPU sampler, or enable with specific Output - as specified by the Output option (default: false). Choosing an output with this options defaults to printing the output to std out, " +
"except for the flamegraph which is printed to a flamegraph.svg file.", usageSyntax = "true|false|", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey ENABLED = new OptionKey<>(new EnableOptionData(false, null), ENABLE_OPTION_TYPE);
@Option(name = "Period", help = "Period in milliseconds to sample the stack (default: 10)", usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey SAMPLE_PERIOD = new OptionKey<>(10L);
@Option(name = "Delay", help = "Delay the sampling for this many milliseconds (default: 0).", usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey DELAY_PERIOD = new OptionKey<>(0L);
@Option(name = "StackLimit", help = "Maximum number of maximum stack elements (default: 10000).", usageSyntax = "[1, inf)", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey STACK_LIMIT = new OptionKey<>(10000);
@Option(name = "Output", help = "Specify the output format to one of: histogram, calltree, json or flamegraph (default: histogram).", //
usageSyntax = "histogram|calltree|json|flamegraph", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey OUTPUT = new OptionKey<>(Output.HISTOGRAM, CLI_OUTPUT_TYPE);
@Option(help = "Specify whether to show compilation information for entries. You can specify 'true' to show all compilation information, 'false' for none, or a comma separated list of compilation tiers. " +
"Note: Interpreter is considered Tier 0. (default: false)", usageSyntax = "true|false|0,1,2", category = OptionCategory.EXPERT, stability = OptionStability.STABLE) //
static final OptionKey ShowTiers = new OptionKey<>(null, SHOW_TIERS_OUTPUT_TYPE);
@Option(name = "FilterRootName", help = "Wildcard filter for program roots. (for example, Math.*) (default: no filter).", usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey FILTER_ROOT = new OptionKey<>(WildcardFilter.DEFAULT, WildcardFilter.WILDCARD_FILTER_TYPE);
@Option(name = "FilterFile", help = "Wildcard filter for source file paths. (for example, *program*.sl) (default: no filter).", usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey FILTER_FILE = new OptionKey<>(WildcardFilter.DEFAULT, WildcardFilter.WILDCARD_FILTER_TYPE);
@Option(name = "FilterMimeType", help = "Only profile the language with given mime-type. (for example, application/javascript) (default: profile all)", //
usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey FILTER_MIME_TYPE = new OptionKey<>("");
@Option(name = "FilterLanguage", help = "Only profile the language with given ID. (for example, js) (default: profile all).", usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey FILTER_LANGUAGE = new OptionKey<>("");
@Option(name = "SampleInternal", help = "Capture internal elements.", category = OptionCategory.INTERNAL) //
static final OptionKey SAMPLE_INTERNAL = new OptionKey<>(false);
@Option(name = "SummariseThreads", help = "Print output as a summary of all 'per thread' profiles.", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey SUMMARISE_THREADS = new OptionKey<>(false);
@Option(name = "GatherHitTimes", help = "Save a timestamp for each taken sample.", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey GATHER_HIT_TIMES = new OptionKey<>(false);
@Option(name = "OutputFile", help = "Save output to the given file. Output is printed to output stream by default.", usageSyntax = "", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey OUTPUT_FILE = new OptionKey<>("");
@Option(name = "MinSamples", help = "Remove elements from output if they have less samples than this value (default: 0)", usageSyntax = "[0, inf)", category = OptionCategory.USER, stability = OptionStability.STABLE) //
static final OptionKey MIN_SAMPLES = new OptionKey<>(0);
@Option(name = "SampleContextInitialization", help = "Enables sampling of code executed during context initialization", category = OptionCategory.EXPERT, stability = OptionStability.STABLE) //
static final OptionKey SAMPLE_CONTEXT_INITIALIZATION = new OptionKey<>(false);
@Option(name = "GatherAsyncStackTrace", help = "Try to gather async stack trace elements for each sample (default: true). " +
"Disabling this option may reduce sampling overhead.", category = OptionCategory.EXPERT, stability = OptionStability.STABLE) //
static final OptionKey GATHER_ASYNC_STACK_TRACE = new OptionKey<>(true);
static void handleOutput(TruffleInstrument.Env env, CPUSampler sampler, String absoluteOutputPath) {
PrintStream out = chooseOutputStream(env, absoluteOutputPath);
List data = sampler.getDataList();
OptionValues options = env.getOptions();
switch (chooseOutput(options)) {
case HISTOGRAM:
printWarnings(sampler, out);
printSamplingHistogram(out, options, data);
break;
case CALLTREE:
printWarnings(sampler, out);
printSamplingCallTree(out, options, data);
break;
case JSON:
printSamplingJson(out, options, data);
break;
case FLAMEGRAPH:
SVGSamplerOutput.printSamplingFlameGraph(out, data);
}
}
private static PrintStream chooseOutputStream(TruffleInstrument.Env env, String absoluteOutputPath) {
if (absoluteOutputPath != null) {
try {
final File file = new File(absoluteOutputPath);
new PrintStream(env.out()).println("Printing output to " + file.getAbsolutePath());
return new PrintStream(new FileOutputStream(file));
} catch (FileNotFoundException e) {
throw handleFileNotFound();
}
}
return new PrintStream(env.out());
}
static String getOutputPath(OptionValues options) {
if (OUTPUT_FILE.hasBeenSet(options)) {
return OUTPUT_FILE.getValue(options);
}
if (ENABLED.getValue(options).output == Output.FLAMEGRAPH) {
return DEFAULT_FLAMEGRAPH_FILE;
}
return null;
}
private static Output chooseOutput(OptionValues options) {
if (OUTPUT.hasBeenSet(options)) {
return options.get(OUTPUT);
}
EnableOptionData enabled = ENABLED.getValue(options);
if (enabled.output != null) {
return enabled.output;
}
return OUTPUT.getDefaultValue();
}
private static void printSamplingCallTree(PrintStream out, OptionValues options, List data) {
for (CPUSamplerData entry : data) {
new SamplingCallTree(entry, options).print(out);
}
}
private static void printSamplingHistogram(PrintStream out, OptionValues options, List data) {
for (CPUSamplerData entry : data) {
new SamplingHistogram(entry, options).print(out);
}
}
private static void printWarnings(CPUSampler sampler, PrintStream out) {
if (sampler.hasStackOverflowed()) {
printDiv(out);
out.println("Warning: The stack has overflowed the sampled stack limit of " + sampler.getStackLimit() + " during execution!");
out.println(" The printed data is incomplete or incorrect!");
out.println(" Use --" + CPUSamplerInstrument.ID + ".StackLimit=<" + STACK_LIMIT.getType().getName() + "> to set the sampled stack limit.");
printDiv(out);
}
if (sampleDurationTooLong(sampler)) {
printDiv(out);
out.println("Warning: Average sample duration took over 20% of the sampling period.");
out.println(" An overhead above 20% can severely impact the reliability of the sampling data. Use one of these approaches to reduce the overhead:");
out.println(" Use --" + CPUSamplerInstrument.ID + ".StackLimit=<" + STACK_LIMIT.getType().getName() + "> to reduce the number of frames sampled,");
out.println(" or use --" + CPUSamplerInstrument.ID + ".Period=<" + SAMPLE_PERIOD.getType().getName() + "> to increase the sampling period.");
printDiv(out);
}
}
private static boolean sampleDurationTooLong(CPUSampler sampler) {
for (CPUSamplerData value : sampler.getDataList()) {
if (value.getSampleDuration().getAverage() > MAX_OVERHEAD_WARNING_THRESHOLD * sampler.getPeriod() * MILLIS_TO_NANOS) {
return true;
}
}
return false;
}
private static void printDiv(PrintStream out) {
out.println("-------------------------------------------------------------------------------- ");
}
private static void printSamplingJson(PrintStream out, OptionValues options, List data) {
boolean gatheredHitTimes = options.get(GATHER_HIT_TIMES);
JSONObject output = new JSONObject();
output.put("tool", CPUSamplerInstrument.ID);
output.put("version", CPUSamplerInstrument.VERSION);
JSONArray contexts = new JSONArray();
for (CPUSamplerData samplerData : data) {
contexts.put(perContextData(samplerData, gatheredHitTimes));
}
output.put("contexts", contexts);
out.println(output);
}
private static JSONObject perContextData(CPUSamplerData samplerData, boolean gatheredHitTimes) {
JSONObject output = new JSONObject();
output.put("sample_count", samplerData.getSamples());
output.put("period", samplerData.getSampleInterval());
output.put("gathered_hit_times", gatheredHitTimes);
JSONArray profile = new JSONArray();
for (Map.Entry>> entry : samplerData.getThreadData().entrySet()) {
JSONObject perThreadProfile = new JSONObject();
perThreadProfile.put("thread", entry.getKey().toString());
perThreadProfile.put("samples", getSamplesRec(entry.getValue()));
profile.put(perThreadProfile);
}
output.put("profile", profile);
return output;
}
private static JSONArray getSamplesRec(Collection> nodes) {
JSONArray samples = new JSONArray();
for (ProfilerNode node : nodes) {
JSONObject sample = new JSONObject();
sample.put("root_name", node.getRootName());
sample.put("source_section", sourceSectionToJSON(node.getSourceSection()));
CPUSampler.Payload payload = node.getPayload();
sample.put("hit_count", payload.getHitCount());
sample.put("self_hit_count", payload.getSelfHitCount());
sample.put("self_hit_times", payload.getSelfHitTimes());
int[] selfTierCount = new int[payload.getNumberOfTiers()];
for (int i = 0; i < selfTierCount.length; i++) {
selfTierCount[i] = payload.getTierSelfCount(i);
}
sample.put("self_tier_count", selfTierCount);
int[] tierCount = new int[payload.getNumberOfTiers()];
for (int i = 0; i < tierCount.length; i++) {
tierCount[i] = payload.getTierSelfCount(i);
}
sample.put("tier_count", tierCount);
sample.put("children", getSamplesRec(node.getChildren()));
samples.put(sample);
}
return samples;
}
private static void printLegend(PrintStream out, String type, long samples, long period, long missed, int[] showTiers, Integer[] tiers) {
out.printf("Sampling %s. Recorded %s samples with period %dms. Missed %s samples.%n", type, samples, period, missed);
out.println(" Self Time: Time spent on the top of the stack.");
out.println(" Total Time: Time spent somewhere on the stack.");
if (showTiers == null) {
return;
}
if (showTiers.length == 0) {
for (int i : tiers) {
out.println(" T" + i + ": Percent of time spent in " + (i == 0 ? "interpreter." : "code compiled by tier " + i + " compiler."));
}
return;
}
for (int tier : showTiers) {
if (contains(tiers, tier)) {
out.println(" T" + tier + ": Percent of time spent in " + (tier == 0 ? "interpreter." : "code compiled by tier " + tier + " compiler."));
} else {
out.println(" T" + tier + ": No samples of tier " + tier + " found during execution. It is excluded from the report.");
}
}
}
private static double percent(long samples, long totalSamples) {
if (totalSamples == 0) {
return 0.0;
}
return ((double) samples * 100) / totalSamples;
}
private static String[] makeTitleAndFormat(int nameLength, int[] showTiers, Integer[] tiers) {
StringBuilder titleBuilder = new StringBuilder(format(" %-" + nameLength + "s || Total Time ", "Name"));
StringBuilder formatBuilder = new StringBuilder(" %-" + nameLength + "s || %10dms %5.1f%% ");
maybeAddTiers(titleBuilder, formatBuilder, showTiers, tiers);
titleBuilder.append("|| Self Time ");
formatBuilder.append("|| %10dms %5.1f%% ");
maybeAddTiers(titleBuilder, formatBuilder, showTiers, tiers);
titleBuilder.append("|| Location ");
formatBuilder.append("|| %s");
String[] strings = new String[2];
strings[0] = titleBuilder.toString();
strings[1] = formatBuilder.toString();
return strings;
}
private static void maybeAddTiers(StringBuilder titleBuilder, StringBuilder formatBuilder, int[] showTiers, Integer[] tiers) {
if (showTiers == null) {
return;
}
if (showTiers.length == 0) {
for (Integer i : tiers) {
titleBuilder.append("| T").append(i).append(" ");
formatBuilder.append("| %5.1f%% ");
}
return;
}
for (int i = 0; i < showTiers.length; i++) {
int selectedTier = showTiers[i];
if (contains(tiers, selectedTier)) {
titleBuilder.append("| T").append(selectedTier).append(" ");
formatBuilder.append("| %5.1f%% ");
}
}
}
private static boolean contains(Integer[] tiers, int selectedTier) {
for (Integer tier : tiers) {
if (tier == selectedTier) {
return true;
}
}
return false;
}
private static Integer[] sortedArray(Set tiers) {
Integer[] sorted = tiers.toArray(new Integer[0]);
Arrays.sort(sorted);
return sorted;
}
private static final class SamplingHistogram {
private final Map> histogram = new HashMap<>();
private final boolean summariseThreads;
private final int minSamples;
private final int[] showTiers;
private final long samplePeriod;
private final long samplesTaken;
private final long samplesMissed;
private Set tiers = new HashSet<>();
private Integer[] sortedTiers;
private int maxNameLength = 10;
private final String title;
private final String format;
SamplingHistogram(CPUSamplerData data, OptionValues options) {
this.summariseThreads = options.get(SUMMARISE_THREADS);
this.minSamples = options.get(MIN_SAMPLES);
this.showTiers = options.get(ShowTiers);
this.samplePeriod = options.get(SAMPLE_PERIOD);
this.samplesTaken = data.getSamples();
Map perThreadSourceLocationPayloads = new HashMap<>();
this.samplesMissed = data.missedSamples();
for (Thread thread : data.getThreadData().keySet()) {
perThreadSourceLocationPayloads.put(thread, computeSourceLocationPayloads(data.getThreadData().get(thread)));
}
maybeSummarizeThreads(perThreadSourceLocationPayloads);
for (Map.Entry threadEntry : perThreadSourceLocationPayloads.entrySet()) {
histogram.put(threadEntry.getKey(), histogramEntries(threadEntry));
}
sortedTiers = sortedArray(tiers);
String[] titleAndFormat = makeTitleAndFormat(maxNameLength, showTiers, sortedTiers);
this.title = titleAndFormat[0];
this.format = titleAndFormat[1];
}
private ArrayList histogramEntries(Map.Entry threadEntry) {
ArrayList histogramEntries = new ArrayList<>();
for (Map.Entry>> sourceLocationEntry : threadEntry.getValue().locations.entrySet()) {
histogramEntries.add(histogramEntry(sourceLocationEntry));
}
histogramEntries.sort((o1, o2) -> Integer.compare(o2.totalSelfSamples, o1.totalSelfSamples));
return histogramEntries;
}
private OutputEntry histogramEntry(Map.Entry>> sourceLocationEntry) {
SourceLocation location = sourceLocationEntry.getKey();
OutputEntry outputEntry = new OutputEntry(location);
maxNameLength = Math.max(maxNameLength, location.getRootName().length());
for (ProfilerNode node : sourceLocationEntry.getValue()) {
CPUSampler.Payload payload = node.getPayload();
int numberOfTiers = payload.getNumberOfTiers();
if (outputEntry.tierToSelfSamples.length < numberOfTiers) {
outputEntry.tierToSelfSamples = Arrays.copyOf(outputEntry.tierToSelfSamples, numberOfTiers);
}
if (outputEntry.tierToSamples.length < numberOfTiers) {
outputEntry.tierToSamples = Arrays.copyOf(outputEntry.tierToSamples, numberOfTiers);
}
for (int i = 0; i < numberOfTiers; i++) {
int selfHitCountsValue = payload.getTierSelfCount(i);
outputEntry.totalSelfSamples += selfHitCountsValue;
outputEntry.tierToSelfSamples[i] += selfHitCountsValue;
tiers.add(i);
// if there is a recursive parent summed up for total time we must not sum up
// again
if (node.isRecursive()) {
continue;
}
int hitCountsValue = payload.getTierTotalCount(i);
outputEntry.totalSamples += hitCountsValue;
outputEntry.tierToSamples[i] += hitCountsValue;
}
}
return outputEntry;
}
private void maybeSummarizeThreads(Map perThreadSourceLocationPayloads) {
if (summariseThreads) {
SourceLocationNodes summary = new SourceLocationNodes(new HashMap<>());
for (SourceLocationNodes sourceLocationNodes : perThreadSourceLocationPayloads.values()) {
for (Map.Entry>> entry : sourceLocationNodes.locations.entrySet()) {
summary.locations.computeIfAbsent(entry.getKey(), s -> new ArrayList<>()).addAll(entry.getValue());
}
}
perThreadSourceLocationPayloads.clear();
perThreadSourceLocationPayloads.put(new Thread("Summary"), summary);
}
}
private static SourceLocationNodes computeSourceLocationPayloads(Collection> profilerNodes) {
Map>> histogram = new HashMap<>();
computeSourceLocationPayloadsImpl(profilerNodes, histogram);
return new SourceLocationNodes(histogram);
}
private static void computeSourceLocationPayloadsImpl(Collection> children, Map>> histogram) {
for (ProfilerNode treeNode : children) {
List> nodes = histogram.computeIfAbsent(new SourceLocation(treeNode.getSourceSection(), treeNode.getRootName()), s -> new ArrayList<>());
nodes.add(treeNode);
computeSourceLocationPayloadsImpl(treeNode.getChildren(), histogram);
}
}
void print(PrintStream out) {
String sep = repeat("-", title.length());
out.println(sep);
printLegend(out, "Histogram", samplesTaken, samplePeriod, samplesMissed, showTiers, sortedTiers);
out.println(sep);
for (Map.Entry> threadListEntry : histogram.entrySet()) {
out.println(threadListEntry.getKey());
out.println(title);
out.println(sep);
for (OutputEntry entry : threadListEntry.getValue()) {
if (minSamples > 0 && entry.totalSelfSamples < minSamples) {
continue;
}
out.println(entry.format(format, showTiers, SamplingHistogram.this.samplePeriod, 0, samplesTaken, sortedTiers));
}
out.println(sep);
}
}
private static final class SourceLocationNodes {
final Map>> locations;
SourceLocationNodes(Map>> locations) {
this.locations = locations;
}
}
}
private static class OutputEntry {
// break after 128 depth spaces to handle deep recursions
private static final int DEPTH_BREAK = 128;
final SourceLocation location;
int[] tierToSamples = new int[0];
int[] tierToSelfSamples = new int[0];
int totalSelfSamples = 0;
int totalSamples = 0;
OutputEntry(SourceLocation location) {
this.location = location;
}
OutputEntry(ProfilerNode node) {
location = new SourceLocation(node.getSourceSection(), node.getRootName());
CPUSampler.Payload payload = node.getPayload();
this.totalSamples = payload.getHitCount();
this.totalSelfSamples = payload.getSelfHitCount();
this.tierToSamples = new int[payload.getNumberOfTiers()];
for (int i = 0; i < tierToSamples.length; i++) {
tierToSamples[i] = payload.getTierTotalCount(i);
}
this.tierToSelfSamples = new int[payload.getNumberOfTiers()];
for (int i = 0; i < tierToSamples.length; i++) {
tierToSelfSamples[i] = payload.getTierSelfCount(i);
}
}
static int computeIndentSize(int depth) {
int indent = depth % DEPTH_BREAK;
if (indent != depth) {
indent += formatIndentBreakLabel(depth - indent).length();
}
return indent;
}
private static String formatIndentBreakLabel(int skippedDepth) {
return CPUSamplerCLI.format("(\u21B3%s) ", skippedDepth);
}
String format(String format, int[] showTiers, long samplePeriod, int depth, long globalTotalSamples, Integer[] tiers) {
List args = new ArrayList<>();
int indent = depth % DEPTH_BREAK;
if (indent != depth) {
args.add(formatIndentBreakLabel(depth - indent) + repeat(" ", indent) + location.getRootName());
} else {
args.add(repeat(" ", indent) + location.getRootName());
}
args.add(totalSamples * samplePeriod);
args.add(percent(totalSamples, globalTotalSamples));
maybeAddTiers(args, tierToSamples, totalSamples, showTiers, tiers);
args.add(totalSelfSamples * samplePeriod);
args.add(percent(totalSelfSamples, globalTotalSamples));
maybeAddTiers(args, tierToSelfSamples, totalSelfSamples, showTiers, tiers);
args.add(getShortDescription(location.getSourceSection()));
return CPUSamplerCLI.format(format, args.toArray());
}
private static void maybeAddTiers(List args, int[] samples, int total, int[] showTiers, Integer[] tiers) {
if (showTiers == null) {
return;
}
if (showTiers.length == 0) {
for (int i : tiers) {
if (i < samples.length) {
args.add(percent(samples[i], total));
} else {
args.add(0.0);
}
}
return;
}
for (int showTier : showTiers) {
if (contains(tiers, showTier)) {
if (showTier < samples.length) {
args.add(percent(samples[showTier], total));
} else {
args.add(0.0);
}
}
}
}
}
private static class SamplingCallTree {
private final boolean summariseThreads;
private final int minSamples;
private final int[] showTiers;
private final long samplePeriod;
private final long samplesTaken;
private final long samplesMissed;
private final String title;
private final String format;
private final Map> entries = new HashMap<>();
private int maxNameLength = 10;
private final Set tiers = new HashSet<>();
private final Integer[] sortedTiers;
SamplingCallTree(CPUSamplerData data, OptionValues options) {
this.summariseThreads = options.get(SUMMARISE_THREADS);
this.minSamples = options.get(MIN_SAMPLES);
this.showTiers = options.get(ShowTiers);
this.samplePeriod = options.get(SAMPLE_PERIOD);
this.samplesTaken = data.getSamples();
this.samplesMissed = data.missedSamples();
Map>> threadData = data.getThreadData();
makeEntries(threadData);
calculateMaxValues(threadData);
sortedTiers = sortedArray(tiers);
String[] titleAndFormat = makeTitleAndFormat(maxNameLength, showTiers, sortedTiers);
this.title = titleAndFormat[0];
this.format = titleAndFormat[1];
}
private void calculateMaxValues(Map>> threadData) {
for (Map.Entry>> entry : threadData.entrySet()) {
for (ProfilerNode node : entry.getValue()) {
calculateMaxValuesRec(node, 0);
}
}
}
private void calculateMaxValuesRec(ProfilerNode node, int depth) {
maxNameLength = Math.max(maxNameLength, node.getRootName().length() + OutputEntry.computeIndentSize(depth));
tiers.add(node.getPayload().getNumberOfTiers() - 1);
for (ProfilerNode child : node.getChildren()) {
calculateMaxValuesRec(child, depth + 1);
}
}
private void makeEntries(Map>> threadData) {
if (summariseThreads) {
List callTreeEntries = new ArrayList<>();
for (Map.Entry>> entry : threadData.entrySet()) {
for (ProfilerNode node : entry.getValue()) {
mergeEntry(callTreeEntries, node, 0);
}
}
entries.put(new Thread("Summary"), callTreeEntries);
} else {
for (Map.Entry>> entry : threadData.entrySet()) {
List callTreeEntries = new ArrayList<>();
for (ProfilerNode node : entry.getValue()) {
callTreeEntries.add(makeEntry(node, 0));
}
entries.put(entry.getKey(), callTreeEntries);
}
}
}
private void mergeEntry(List callTreeEntries, ProfilerNode node, int depth) {
for (CallTreeOutputEntry callTreeEntry : callTreeEntries) {
if (callTreeEntry.corresponds(node)) {
callTreeEntry.merge(node.getPayload());
for (ProfilerNode child : node.getChildren()) {
mergeEntry(callTreeEntry.children, child, depth + 1);
}
return;
}
}
callTreeEntries.add(makeEntry(node, depth));
}
private CallTreeOutputEntry makeEntry(ProfilerNode node, int depth) {
maxNameLength = Math.max(maxNameLength, node.getRootName().length() + OutputEntry.computeIndentSize(depth));
tiers.add(node.getPayload().getNumberOfTiers() - 1);
CallTreeOutputEntry entry = new CallTreeOutputEntry(node);
for (ProfilerNode child : node.getChildren()) {
entry.children.add(makeEntry(child, depth + 1));
}
return entry;
}
void print(PrintStream out) {
String sep = repeat("-", title.length());
out.println(sep);
printLegend(out, "Call Tree", samplesTaken, samplePeriod, samplesMissed, showTiers, sortedTiers);
out.println(sep);
out.println(title);
out.println(sep);
for (Map.Entry> threadData : entries.entrySet()) {
for (CallTreeOutputEntry entry : threadData.getValue()) {
recursivePrint(out, entry, 0);
}
}
out.println(sep);
}
private void recursivePrint(PrintStream out, CallTreeOutputEntry entry, int depth) {
if (minSamples > 0 && entry.totalSelfSamples < minSamples) {
return;
}
out.println(entry.format(format, showTiers, samplePeriod, depth, samplesTaken, sortedTiers));
List sortedChildren = new ArrayList<>(entry.children);
sortedChildren.sort((o1, o2) -> Long.compare(o2.totalSamples, o1.totalSamples));
for (CallTreeOutputEntry child : sortedChildren) {
recursivePrint(out, child, depth + 1);
}
}
private static class CallTreeOutputEntry extends OutputEntry {
List children = new ArrayList<>();
CallTreeOutputEntry(ProfilerNode node) {
super(node);
}
boolean corresponds(ProfilerNode node) {
return location.getSourceSection().equals(node.getSourceSection()) && location.getRootName().equals(node.getRootName());
}
void merge(CPUSampler.Payload payload) {
this.totalSamples += payload.getHitCount();
this.totalSelfSamples += payload.getSelfHitCount();
if (payload.getNumberOfTiers() > tierToSamples.length) {
tierToSamples = Arrays.copyOf(tierToSamples, payload.getNumberOfTiers());
}
for (int i = 0; i < payload.getNumberOfTiers(); i++) {
tierToSamples[i] += payload.getTierTotalCount(i);
}
if (payload.getNumberOfTiers() > tierToSelfSamples.length) {
tierToSelfSamples = Arrays.copyOf(tierToSelfSamples, payload.getNumberOfTiers());
}
for (int i = 0; i < payload.getNumberOfTiers(); i++) {
tierToSamples[i] += payload.getTierTotalCount(i);
}
}
}
}
private static String format(String format, Object... args) {
return String.format(Locale.ENGLISH, format, args);
}
}