package org.jboss.windup.util; import org.jboss.windup.util.threading.WindupChildThread; import java.io.FileWriter; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import org.apache.commons.lang3.StringEscapeUtils; /** * <p> * {@link ExecutionStatistics} provides a simple system for storing the time taken to perform operations. * </p> * * <p> * Example usage: * * <pre> * ExecutionStatistics.get().begin("Process-01"); * // ... your code to be timed goes here * ExecutionStatistics.get().end("Process-01"); * </pre> * * </p> * * @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a> * */ public class ExecutionStatistics { private static final Logger LOG = Logging.get(ExecutionStatistics.class); private static final Map<Thread,ExecutionStatistics> stats = new ConcurrentHashMap<>(); private final Map<String, TimingData> executionInfo = new HashMap<>(); private ExecutionStatistics() { } /** * Gets the instance associated with the current thread. */ public static synchronized ExecutionStatistics get() { Thread currentThread = Thread.currentThread(); if (stats.get(currentThread) == null) { stats.put(currentThread, new ExecutionStatistics()); } return stats.get(currentThread); } public Map<String, TimingData> getExecutionInfo() { return executionInfo; } /** * Merge this ExecutionStatistics with all the statistics created within the child threads. All the child threads had to be created using Windup-specific * ThreadFactory in order to contain a reference to the parent thread. */ public void merge() { Thread currentThread = Thread.currentThread(); if(!stats.get(currentThread).equals(this) || currentThread instanceof WindupChildThread) { throw new IllegalArgumentException("Trying to merge executionstatistics from a " + "different thread that is not registered as main thread of application run"); } for (Thread thread : stats.keySet()) { if(thread instanceof WindupChildThread && ((WindupChildThread) thread).getParentThread().equals(currentThread)) { merge(stats.get(thread)); } } } /** * Merge two ExecutionStatistics into one. This method is private in order not to be synchronized (merging. * @param otherStatistics */ private void merge(ExecutionStatistics otherStatistics) { for (String s : otherStatistics.executionInfo.keySet()) { TimingData thisStats = this.executionInfo.get(s); TimingData otherStats = otherStatistics.executionInfo.get(s); if(thisStats == null) { this.executionInfo.put(s,otherStats); } else { thisStats.merge(otherStats); } } } /** * Clears the current threadlocal as well as any current state. */ public void reset() { stats.remove(Thread.currentThread()); executionInfo.clear(); } /** * Serializes the timing data to a "~" delimited file at outputPath. */ public void serializeTimingData(Path outputPath) { //merge subThreads instances into the main instance merge(); try (FileWriter fw = new FileWriter(outputPath.toFile())) { fw.write("Number Of Executions, Total Milliseconds, Milliseconds per execution, Type\n"); for (Map.Entry<String, TimingData> timing : executionInfo.entrySet()) { TimingData data = timing.getValue(); long totalMillis = (data.totalNanos / 1000000); double millisPerExecution = (double) totalMillis / (double) data.numberOfExecutions; fw.write(String.format("%6d, %6d, %8.2f, %s\n", data.numberOfExecutions, totalMillis, millisPerExecution, StringEscapeUtils.escapeCsv(timing.getKey()) )); } } catch (Exception e) { e.printStackTrace(); } } public static <T> T performBenchmarked(String key, Task<T> operation) { ExecutionStatistics instance = ExecutionStatistics.get(); instance.begin(key); try { return operation.execute(); } finally { instance.end(key); } } /** * Start timing an operation with the given identifier. */ public void begin(String key) { if (key == null) { return; } TimingData data = executionInfo.get(key); if (data == null) { data = new TimingData(key); executionInfo.put(key, data); } data.begin(); } /** * Complete timing the operation with the given identifier. If you had not previously started a timing operation with this identifier, then this * will effectively be a noop. */ public void end(String key) { if (key == null) { return; } TimingData data = executionInfo.get(key); if (data == null) { LOG.info("Called end with key: " + key + " without ever calling begin"); return; } data.end(); } public class TimingData { private final String key; private long startTime; private long numberOfExecutions; private long totalNanos; public TimingData(String key) { this.key = key; } public void begin() { this.startTime = System.nanoTime(); } public void end() { if (this.startTime == 0) { LOG.info("Called end with key: " + this.key + " without ever calling begin"); return; } this.totalNanos += (System.nanoTime() - startTime); this.startTime = 0; this.numberOfExecutions++; } public void merge(TimingData other) { this.numberOfExecutions += other.numberOfExecutions; this.totalNanos = other.totalNanos; } public long getTotal() { return totalNanos; } } }