package org.radargun.stages.test; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.radargun.DistStageAck; import org.radargun.config.DocumentedValue; import org.radargun.config.Property; import org.radargun.config.Stage; import org.radargun.reporting.Report; import org.radargun.stages.AbstractDistStage; import org.radargun.stats.Statistics; import org.radargun.stats.representation.RepresentationType; import org.radargun.utils.Utils; /** * @author Radim Vansa <rvansa@redhat.com> */ @Stage(doc = "Analyzes results of already executed test.") public class AnalyzeTestStage extends AbstractDistStage { @Property(doc = "Name of the test whose result should be analyzed.", optional = false) protected String testName; @Property(doc = "Operation that should be analyzed (e.g. BasicOperations.Get).", optional = false) protected String operation; @Property(doc = "What should be results of this analysis. Default is VALUE.") protected ResultType resultType = ResultType.VALUE; @Property(doc = "Name of the target property where the result should be stored.", optional = false) protected String storeResultTo; @Property(doc = "How should the thread statistics be aggregated. By default all statistics are merged.") protected ThreadGrouping threadGrouping = ThreadGrouping.GROUP_ALL; @Property(doc = "Which iterations should be included in the analysis. By default we iterate over all iterations.") protected IterationSelection iterationSelection = IterationSelection.EACH_ITERATION; @Property(doc = "How do we process the data. We can search for maximum, minimum or average.", optional = false) protected AnalyzisType analyzisType; @Property(doc = "What value do we we retrieve from the statistics.", optional = false, complexConverter = RepresentationType.ComplexConverter.class) protected RepresentationType statisticsType; @Override public Map<String, Object> createMasterData() { Report.Test test = masterState.getReport().getTest(testName); if (test == null) throw new IllegalArgumentException("No test '" + testName + "' found."); Number result = analyze(test); log.infof("Result of analysis is %s, storing into %s", result, storeResultTo); masterState.put(storeResultTo, result); return Collections.singletonMap(storeResultTo, (Object) result); } protected Number analyze(Report.Test test) { List<Group> groups = group(test); log.infof("Grouped test results into %d groups", groups.size()); if (groups.size() == 0) return Double.NaN; double min = Double.MAX_VALUE, max = -Double.MAX_VALUE, sum = 0; Group minGroup = null, maxGroup = null; for (Group g : groups) { double value = statisticsType.getValue(g.statistics, operation, g.duration); log.tracef("iteration %d, node %d, thread %d: %d threads, duration %s -> value %f", g.origin.iteration, g.origin.node, g.origin.thread, g.threads, Utils.prettyPrintTime(g.duration, TimeUnit.NANOSECONDS), value); if (value < min) { min = value; minGroup = g; } if (value > max) { max = value; maxGroup = g; } sum += value; } switch (analyzisType) { case MAX: return getResult(max, maxGroup); case MIN: return getResult(min, minGroup); case AVERAGE: if (resultType != ResultType.VALUE) { throw new IllegalArgumentException("Cannot compute average with result type different than VALUE"); } return sum / groups.size(); } throw new IllegalStateException("Unexpected analysis type: " + analyzisType); } private Number getResult(double value, Group group) { switch (resultType) { case VALUE: return value; case ITERATION: return group.origin.iteration; case NODE: if (threadGrouping == ThreadGrouping.GROUP_ALL) { throw new IllegalArgumentException("Cannot find node when grouping all results"); } return group.origin.node; case THREAD: if (threadGrouping != ThreadGrouping.EACH_THREAD) { throw new IllegalArgumentException("Cannot find thread when grouping all results"); } return group.origin.thread; default: throw new IllegalArgumentException("Unexpected result type: " + resultType); } } protected List<Group> group(Report.Test test) { switch (iterationSelection) { case EACH_ITERATION: List<Group> groups = new ArrayList<>(); int iterationCounter = 0; for (Report.TestIteration it : test.getIterations()) { groups.addAll(group(iterationCounter++, it.getStatistics())); } return groups; case LAST_ITERATION: List<Report.TestIteration> iterations = test.getIterations(); if (iterations.size() > 0) { int iteration = iterations.size() - 1; return group(iteration, iterations.get(iteration).getStatistics()); } else { return Collections.EMPTY_LIST; } default: throw new IllegalStateException("Unexpected iteration grouping: " + iterationSelection); } } private List<Group> group(int iteration, Set<Map.Entry<Integer, List<Statistics>>> statistics) { List<Group> groups = new ArrayList<>(); switch (threadGrouping) { case EACH_THREAD: for (Map.Entry<Integer, List<Statistics>> entry : statistics) { int threadCounter = 0; for (Map.Entry<Integer, List<Statistics>> e : statistics) { if (e.getKey() < entry.getKey()) { threadCounter += e.getValue().size(); } } for (Statistics s : entry.getValue()) { groups.add(new Group(s, 1, duration(s), new Origin(iteration, entry.getKey(), threadCounter))); } } break; case GROUP_BY_NODE: for (Map.Entry<Integer, List<Statistics>> entry : statistics) { entry.getValue().stream().reduce(Statistics.MERGE).map(aggregation -> groups.add(new Group(aggregation, entry.getValue().size(), duration(aggregation), new Origin(iteration, entry.getKey(), -1))) ); } break; case GROUP_ALL: int threads = statistics.stream().mapToInt(e -> e.getValue().size()).sum(); statistics.stream().flatMap(e -> e.getValue().stream()).reduce(Statistics.MERGE).map(aggregation -> groups.add(new Group(aggregation, threads, duration(aggregation), new Origin(iteration, -1, -1))) ); break; default: throw new IllegalStateException("Unexpected thread grouping: " + threadGrouping); } return groups; } private long duration(Statistics s) { return TimeUnit.MILLISECONDS.toNanos(s.getEnd() - s.getBegin()); } @Override public DistStageAck executeOnSlave() { return successfulResponse(); } protected static class Group { public final Statistics statistics; public final int threads; public final long duration; public final Origin origin; public Group(Statistics statistics, int threads, long duration, Origin origin) { this.statistics = statistics; this.threads = threads; this.duration = duration; this.origin = origin; } } protected static class Origin { public final int iteration; public final int node; public final int thread; public Origin(int iteration, int node, int thread) { this.iteration = iteration; this.node = node; this.thread = thread; } } public enum IterationSelection { @DocumentedValue("The analysis will run on all iterations.") EACH_ITERATION, @DocumentedValue("Only the last iteration will be analyzed.") LAST_ITERATION } public enum ThreadGrouping { @DocumentedValue("Consider statistics of each thread.") EACH_THREAD, @DocumentedValue("Merge statistics of all thread on one node and analyze the result value.") GROUP_BY_NODE, @DocumentedValue("Merge statistics of all thread on all nodes and analyze the result value.") GROUP_ALL } public enum AnalyzisType { @DocumentedValue("Compute maximal value from all the results.") MAX, @DocumentedValue("Compute minimal value from all the results.") MIN, @DocumentedValue("Compute average value from all the results.") AVERAGE } public enum ResultType { @DocumentedValue("Report directly the value that was computed during this analysis.") VALUE, @DocumentedValue("Report the iteration number where we have found the desired value. Works for analyzis-type MAX or MIN.") ITERATION, @DocumentedValue("Report the node (slave index) where we have found the desired value. Works for analyzis-type MAX or MIN.") NODE, @DocumentedValue("Report the global thread id where we have found the desired value. Works for analyzis-type MAX or MIN.") THREAD } }