package com.laytonsmith.tools;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.TermColors;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This tool reads a file that contains full verbosity profiling data and
* creates a summary of the data.
*/
public class ProfilerSummary {
private static final Pattern PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{1,2}:\\d{2}\\.\\d{2}:\\s*\\[(\\d+\\.\\d+)ms\\]\\[Lvl:(4|5)\\]:\\s*Executing function: ([a-zA-Z_]+[a-zA-Z0-9_]*)\\(.*\\).*");
/**
* The actual profile file data
*/
private String data;
/**
* Data that is below this percentage of total time is ignored. Defaults to
* 0.
*/
private double ignorePercentage = 0D;
/**
* Creates a new ProfileAnalyzer object from an InputStream. It is expected
* that the input stream will contain UTF-8 encoded data.
*
* @param input
*/
public ProfilerSummary(InputStream input) {
this(StreamUtils.GetString(input));
}
/**
* Creates a new ProfileAnalyzer object from string data.
*
* @param data
*/
public ProfilerSummary(String data) {
this.data = data;
}
/**
* Any functions that use up less than this percentage of total time are
* omitted from the report. This allows summaries to be more concise and
* show only the largest bottlenecks, instead of all data.
*
* @param percentage
*/
public void setIgnorePercentage(double percentage) {
if (percentage < 0 || percentage > 1) {
throw new IllegalArgumentException("The percentage must be between 0 and 1.");
}
this.ignorePercentage = percentage;
}
/**
* Returns a human readable getAnalysis of functions, given the arguments
* provided.
*
* @return Human readable results.
*/
public String getAnalysis() {
Map<String, Double> functionData = new HashMap<>();
Matcher m = PATTERN.matcher(data);
double totalTime = 0;
boolean foundLevel5 = false;
while (m.find()) {
String function = m.group(3);
if("5".equals(m.group(2))){
foundLevel5 = true;
}
Double time = Double.parseDouble(m.group(1));
totalTime += time;
if (functionData.containsKey(function)) {
functionData.put(function, functionData.get(function) + time);
} else {
functionData.put(function, time);
}
}
if(!foundLevel5){
return "Analysis can only be done on a profile summary file, which was created with verbosity level 5.";
}
int originalSize = functionData.size();
//Now, figure out which results to ignore
for (String f : new ArrayList<>(functionData.keySet())) {
if (functionData.get(f) / totalTime <= ignorePercentage) {
//Remove it
functionData.remove(f);
}
}
//Now print out the data that remains
StringBuilder b = new StringBuilder();
b.append("Profiler data summary:\n\n");
b.append(StringUtils.PluralTemplateHelper(functionData.size(), "One function was", "%d functions were")).append(" profiled in total");
if(originalSize == functionData.size()){
b.append(".");
} else {
b.append(", and ");
b.append(StringUtils.PluralTemplateHelper(originalSize - functionData.size(), "one function is", "%d functions are"));
b.append(" being hidden from the report, as they are less than ").append((int) (ignorePercentage * 100)).append("% of the total time spent.");
}
b.append("\n\n");
//Reverse the order, so higher usage functions go on top.
Map<String, Double> sortedMap = sortByValue(functionData);
List<String> keySet = new ArrayList<>(sortedMap.keySet());
Collections.reverse(keySet);
for(String f : keySet){
b.append(TermColors.WHITE).append(f).append(TermColors.RESET).append(": ").append(String.format("%.3f", sortedMap.get(f))).append(" ms\n");
}
return b.toString();
}
private static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
List<Map.Entry<K, V>> list = new LinkedList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry<K, V>>() {
@Override
public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
return (o1.getValue()).compareTo(o2.getValue());
}
});
Map<K, V> result = new LinkedHashMap<>();
for (Map.Entry<K, V> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
}