package com.laytonsmith.core.profiler; import com.laytonsmith.PureUtilities.Common.DateUtils; import com.laytonsmith.PureUtilities.Common.FileUtil; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.PureUtilities.ExecutionQueue; import com.laytonsmith.PureUtilities.Preferences; import com.laytonsmith.PureUtilities.Preferences.Preference; import com.laytonsmith.core.LogLevel; import com.laytonsmith.core.MethodScriptExecutionQueue; import com.laytonsmith.core.Static; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; /** * TODO: The following points need profile hooks: * 1 - Execution Queue task run times * 3 - Procedure execution run times (with parameters) */ /** * * */ public final class Profiler { /** * Useful for test cases and other places where a profiler is needed, but not desired, * this can be called to generate a fake Profiler. * @return */ public static Profiler FakeProfiler(){ Profiler p = new Profiler(); p.profilerOn = false; return p; } public static void Install(File initFile) throws IOException { //We just want to create the config file initially GetPrefs(initFile); } private static Preferences GetPrefs(File initFile) throws IOException { List<Preference> defaults = new ArrayList<Preference>(Arrays.asList(new Preference[]{ new Preference("profiler-on", "false", Preferences.Type.BOOLEAN, "Turns the profiler on or off. The profiler can cause a slight amount of lag, so generally speaking" + " you don't leave it on during normal operation."), new Preference("profiler-granularity", "1", Preferences.Type.INT, "Sets the granularity of the profiler. 1 logs some things, while 5 logs everything possible."), new Preference("profiler-log", "logs/profiling/internal/%Y-%M-%D-profiler.log", Preferences.Type.STRING, "The location of the profiler output log. The following macros are supported" + " and will expand to the specified values: %Y - Year, %M - Month, %D - Day, %h - Hour, %m - Minute, %s - Second"), new Preference("write-to-file", "true", Preferences.Type.BOOLEAN, "If true, will write results out to file."), new Preference("write-to-screen", "false", Preferences.Type.BOOLEAN, "If true, will write results out to screen."), new Preference("profile-log-threshold", "0.005", Preferences.Type.DOUBLE, "If a profile point took less than this amount of time (in ms) to run, it won't be logged. This is good for reducing data blindness" + " caused by too much data being displayed. Normally you only care about things that took longer than a certain amount, not things that took less than a certain amount. Setting this to 0" + " will trigger everything."), })); Preferences prefs = new Preferences("CommandHelper", Static.getLogger(), defaults, "These settings control the integrated profiler"); prefs.init(initFile); return prefs; } //Needs to be package protected Map<ProfilePoint, Long> operations; long queuedProfilePoints = 0; private LogLevel configGranularity; private boolean profilerOn; private String logFile; private boolean writeToFile; private boolean writeToScreen; private Preferences prefs; private File initFile; private double logThreshold; //To prevent file fights across threads, we only want one outputQueue. private static ExecutionQueue outputQueue; private final ProfilePoint NULL_OP = new ProfilePoint("NULL_OP", this); private Profiler(){ //Private constructor for FakeProfiler if (outputQueue == null) { outputQueue = new MethodScriptExecutionQueue("CommandHelper-Profiler", "default"); } } public Profiler(File initFile) throws IOException { this(); prefs = GetPrefs(initFile); //We want speed here, not memory usage, so lets put an excessively large capacity, and excessively low load factor operations = new HashMap<ProfilePoint, Long>(1024, 0.25f); this.initFile = initFile; configGranularity = LogLevel.getEnum((Integer) prefs.getPreference("profiler-granularity")); if (configGranularity == null) { configGranularity = LogLevel.ERROR; } profilerOn = (Boolean) prefs.getPreference("profiler-on"); logFile = (String) prefs.getPreference("profiler-log"); writeToFile = (Boolean) prefs.getPreference("write-to-file"); writeToScreen = (Boolean) prefs.getPreference("write-to-screen"); logThreshold = (Double) prefs.getPreference("profile-log-threshold"); new GarbageCollectionDetector(this); //As a form of calibration, we want to "warm up" a point. //For whatever reason, this levels out the profile points pretty well. ProfilePoint warmupPoint = this.start("Warming up the profiler", LogLevel.VERBOSE); this.stop(warmupPoint); } /** * Starts a timer, and returns a profile point object, which should be used * to stop this timer later. A special ProfilePoint is returned if this * profile point shouldn't be logged based on the granularity settings, * which short circuits the entire profiling process, for non-trigger * points, which should speed operation considerably. * * @param name The name to be used during logging * @param granularity * @return */ public ProfilePoint start(String name, LogLevel granularity) { if (!isLoggable(granularity)) { return NULL_OP; } ProfilePoint p = new ProfilePoint(name, this); start0(p, granularity); return p; } /** * "Starts" an operation. Note that for each start, you must use EXACTLY one * stop, with exactly the same object for operationName. Multiple profile * points can share the same name, and they will be stacked and lined up * accordingly. * * @param operationName The name of the operation. A corresponding call to * DoStop must be called with this exact same object. * @param granularity The granularity at which to log. */ private void start0(ProfilePoint operationName, LogLevel granularity) { if (operations.containsKey(operationName)) { //Nope. Can't queue up multiple versions of the same //id throw new RuntimeException("Cannot queue the same profile point multiple times!"); } queuedProfilePoints++; operationName.setGranularity(granularity); //This line should ALWAYS be last in the function operations.put(operationName, System.nanoTime()); } private final static Map<Long, String> indents = new TreeMap<Long, String>(); static{ //Let's just warm it up some for(int i = 0; i < 10; i++){ getIndent(i); } } private static String getIndent(long count){ if(!indents.containsKey(count)){ StringBuilder b = new StringBuilder(); for(int i = 0; i < count; i++){ b.append(" "); } indents.put(count, b.toString()); } return indents.get(count); } private final static String gcString = " (however, the garbage collector was run during this profile point)"; public void stop(ProfilePoint operationName) { //This line should ALWAYS be first in the function long stop = System.nanoTime(); if (operationName == NULL_OP) { return; } if (!operations.containsKey(operationName)) { return; } long total = stop - operations.get(operationName); //1 million nano seconds in 1 ms. We want x.xxx ms shown, so divide by 1000, round (well, integer truncate, since it's faster), then divide by 1000 again. //voila, significant figure to the 3rd degree. double time = (total / 1000) / 1000.0; if(time >= logThreshold){ String stringTime = Double.toString(time); if(stringTime.length() < 6 && stringTime.contains(".")){ while(stringTime.length() < 6){ stringTime += "0"; } } stringTime += "ms"; if(time > 1000){ //Let's change this to seconds, actually. stringTime = Double.toString(((long)time) / 1000.0) + "sec"; } String operationMessage = operationName.getMessage()!=null?" Message: "+operationName.getMessage():""; doLog("[" + stringTime + "][Lvl:" + (operationName.getGranularity().getLevel()) + "]:" + getIndent(queuedProfilePoints) + operationName.toString() + operationMessage + (operationName.wasGCd()?gcString:"")); } queuedProfilePoints--; } public boolean isLoggable(LogLevel granularity) { if (!profilerOn || granularity == null) { return false; } return granularity.getLevel() <= configGranularity.getLevel(); } /** * Pushes a log to either the screen or the log file, depending on config * settings. Arbitrary messages can be logged using this method. * * @param message */ public void doLog(final String message) { outputQueue.push(null, null, new Runnable() { @Override public void run() { if (writeToScreen) { StreamUtils.GetSystemOut().println(message); } if (writeToFile) { File file = new File(initFile.getParentFile(), DateUtils.ParseCalendarNotation(logFile)); try { FileUtil.write(DateUtils.ParseCalendarNotation("%Y-%M-%D %h:%m.%s") + ": " + message + Static.LF(), //Message to log file, //File to output to FileUtil.APPEND, //We want to append true); //Create it for us if it doesn't exist } catch (IOException ex) { StreamUtils.GetSystemErr().println("While trying to write to the profiler log file (" + file.getAbsolutePath() + "), recieved an IOException: " + ex.getMessage()); } } } }); } }