package hudson.plugins.performance.parsers; import hudson.Extension; import hudson.plugins.performance.data.HttpSample; import hudson.plugins.performance.descriptors.PerformanceReportParserDescriptor; import hudson.plugins.performance.reports.PerformanceReport; import org.kohsuke.stapler.DataBoundConstructor; import java.io.File; import java.util.Date; import java.util.Scanner; /** * Parser for wrk (https://github.com/wg/wrk) * <p> * <p> * Note that Wrk does not produce request-level data, and can only be processed * in it's summarized form (unless extended to do otherwise). * * @author John Murray me@johnmurray.io */ public class WrkSummarizerParser extends AbstractParser { private enum LineType { RUNNING, THREAD_CONN_COUNT, OUTPUT_HEADER, LATENCY_DIST, LATENCY_DIST_BUCKET_HEADER, LATENCY_DIST_BUCKET, REQ_SEC_DIST, SUMMARY, REQ_SEC, TRANSFER_SEC, ERROR_COUNT, UNKNOWN } public enum TimeUnit { MILLISECOND(1), SECOND(1000), MINUTE(1000 * 60), HOUR(1000 * 60 * 60); private final int factor; TimeUnit(int factor) { this.factor = factor; } public int getFactor() { return this.factor; } } @Extension public static class DescriptorImpl extends PerformanceReportParserDescriptor { @Override public String getDisplayName() { return "wrk"; } } @DataBoundConstructor public WrkSummarizerParser(String glob) { super(glob); } @Override public String getDefaultGlobPattern() { return "**/*.wrk"; } @Override PerformanceReport parse(File reportFile) throws Exception { final PerformanceReport r = new PerformanceReport(); r.setReportFileName(reportFile.getName()); Scanner s = null; try { s = new Scanner(reportFile); HttpSample sample = new HttpSample(); while (s.hasNextLine()) { Scanner scanner = null; try { String line = s.nextLine(); scanner = new Scanner(line.toLowerCase().replaceAll( "(\\d)s|ms|%|mb|kb(\\b)", "$1$2")); String firstToken = scanner.next(); String secondToken = scanner.next(); switch (determineLineType(firstToken, secondToken)) { case RUNNING: // extract URI scanner.next(); scanner.next(); String uri = scanner.next(); sample.setUri(uri); break; case LATENCY_DIST: Scanner latencyScanner = new Scanner(line.toLowerCase()); latencyScanner.next(); // header (skip) long latencyAvg = getTime(latencyScanner.next(), TimeUnit.MILLISECOND); latencyScanner.next(); // stdDev (skipping) long latencyMax = getTime(latencyScanner.next(), TimeUnit.MILLISECOND); sample.setDuration(latencyAvg); sample.setSummarizerMax(latencyMax); break; case REQ_SEC_DIST: // float reqSecAvg = Float.parseFloat(secondToken); // float reqSecStdDev = scanner.nextFloat(); // float reqSecMax = scanner.nextFloat(); // float reqSecPercentInOneStdDev = scanner.nextFloat(); break; case SUMMARY: long totalReq = Long.parseLong(firstToken); Scanner summaryScanner = new Scanner(line.toLowerCase()); summaryScanner.next(); summaryScanner.next(); summaryScanner.next(); // long totalTime = getTime(summaryScanner.next(), logger, // TimeUnit.SECOND); sample.setSummarizerSamples(totalReq); summaryScanner.close(); break; case ERROR_COUNT: scanner.next(); scanner.next(); int numErrors = scanner.nextInt(); sample.setSummarizerErrors(numErrors); break; case REQ_SEC: case TRANSFER_SEC: // not currently used by performance-plugin break; case THREAD_CONN_COUNT: case OUTPUT_HEADER: case LATENCY_DIST_BUCKET_HEADER: case LATENCY_DIST_BUCKET: case UNKNOWN: // do nothing, don't need output break; } } finally { if (scanner != null) scanner.close(); } } sample.setSuccessful(true); sample.setDate(new Date()); r.addSample(sample); } finally { if (s != null) s.close(); } return r; } /** * Given a time string (eg: 0ms, 1m, 2s, 3h, etc.) parse and yield the time in * a specified time unit (millisecond, second, minute, hour) * <p> * <p> * If no result can be returned, a 0 value will result and any errors * encountered will be logged. * * @param timeString String representation from `wrk` command-output * @param tu Time unit to return time string in * @return Time in seconds, as parsed from input */ public long getTime(String timeString, TimeUnit tu) { double factor = 0; timeString = timeString.trim().replaceAll("[^\\d\\.smh]", ""); String timeUnitString = timeString.replaceAll("[\\d\\.]", ""); String timeValueString = timeString.replaceAll("[smh]", ""); /* * Calculate 'factor' so that we can get the input time in ms (eg: 5m = * 300000, 3s = 3000) */ if (timeUnitString.equals("ms")) { factor = 1; } else if (timeUnitString.equals("s")) { factor = 1000; } else if (timeUnitString.equals("m")) { factor = 1000 * 60; } else if (timeUnitString.equals("h")) { factor = 1000 * 60 * 60; } double timeValue = Double.parseDouble(timeValueString); double timeInMilliSeconds = timeValue * factor; double timeInReturnFormat = timeInMilliSeconds / tu.getFactor(); return (int) Math.floor(timeInReturnFormat); } /** * Given the first couple of tokens from a line, determine what type of line * it is (by returning a LineType) so that is can be processed accordingly. * * @param t1 First token in the processed line * @param t2 Second token in the processed line * @return LineType indicating how the rest of the line should be processed */ public LineType determineLineType(String t1, String t2) { if (t1.equals("running")) { return LineType.RUNNING; } else if (t1.equals("thread")) { return LineType.OUTPUT_HEADER; } else if (t1.equals("latency")) { if (t2.equals("distribution")) return LineType.LATENCY_DIST_BUCKET_HEADER; else return LineType.LATENCY_DIST; } else if (t1.equals("req/sec")) { return LineType.REQ_SEC_DIST; } else if (t1.equals("requests/sec:")) { return LineType.REQ_SEC; } else if (t1.equals("transfer/sec:")) { return LineType.TRANSFER_SEC; } else if (t1.equals("non-2xx")) { return LineType.ERROR_COUNT; } else { try { Long.parseLong(t1); if (t2.equals("threads")) { return LineType.THREAD_CONN_COUNT; } else if (t2.equals("requests")) { return LineType.SUMMARY; } else { try { Float.parseFloat(t2); return LineType.LATENCY_DIST_BUCKET; } catch (NumberFormatException e) { return LineType.UNKNOWN; } } } catch (NumberFormatException e) { return LineType.UNKNOWN; } } } }