package org.radargun.reporting.html; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.radargun.config.Cluster; import org.radargun.config.Property; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.reporting.Timeline; /** * Presents {@link Timeline timelines} from all slaves and master. * Uses {@link TimelineChart} to generate image files. * * @author Radim Vansa <rvansa@redhat.com> */ public class TimelineDocument extends HtmlDocument { private static final Log log = LogFactory.getLog(TimelineDocument.class); private final Configuration configuration; private final String configName; private final String title; private final Cluster cluster; private List<Timeline> timelines; private Map<Timeline.Category, Double> minValues = new HashMap<>(); private Map<Timeline.Category, Double> maxValues = new HashMap<>(); private Map<Timeline.Category, Integer> valueCategories = new TreeMap<>(); private Map<String, Integer> eventCategories = new TreeMap<>(); private Timeline.Category.Type categoryType; private long startTimestamp = Long.MAX_VALUE, endTimestamp = Long.MIN_VALUE; public TimelineDocument(Configuration configuration, String directory, String configName, String title, List<Timeline> timelines, Timeline.Category.Type categoryType, Cluster cluster) { super(directory, categoryType.toString() + "_timeline_" + configName + ".html", title + " Timeline"); this.title = title; this.configuration = configuration; this.timelines = new ArrayList<>(timelines); this.categoryType = categoryType; Collections.sort(this.timelines); this.configName = configName; this.cluster = cluster; for (Timeline timeline : this.timelines) { startTimestamp = Math.min(startTimestamp, timeline.getFirstTimestamp()); endTimestamp = Math.max(endTimestamp, timeline.getLastTimestamp()); for (String category : timeline.getEventCategories()) { if (!eventCategories.containsKey(category)) { eventCategories.put(category, eventCategories.size()); } } for (Timeline.Category category : timeline.getValueCategories()) { if (!valueCategories.containsKey(category)) { valueCategories.put(category, valueCategories.size()); } List<Timeline.Value> values = timeline.getValues(category); double min = Long.MAX_VALUE, max = Long.MIN_VALUE; for (Timeline.Value value : values) { double d = value.value.doubleValue(); max = Math.max(max, d); min = Math.min(min, d); } if (min <= max) { Double prevMin = minValues.get(category); Double prevMax = maxValues.get(category); minValues.put(category, prevMin == null ? min : Math.min(prevMin, min)); maxValues.put(category, prevMax == null ? max : Math.max(prevMax, max)); } } } // in order to show event categories, we need at least one value category if (valueCategories.isEmpty()) { Timeline.Category defaultCategory = Timeline.Category.sysCategory(" "); valueCategories.put(defaultCategory, 0); minValues.put(defaultCategory, 0d); maxValues.put(defaultCategory, 0d); } } @Override public String getTitle() { return title; } public String range(final Timeline.Category valueCategory, final int valueCategoryId) { Double min = minValues.get(valueCategory); if (min == null || min > 0) min = 0d; Double max = maxValues.get(valueCategory); if (max == null || max < 0) max = 0d; minValues.put(valueCategory, min); maxValues.put(valueCategory, max); return String.format("timeline_%s_%d_range.png", configName, valueCategoryId); } public String getValueChartFile(int valueCategoryId, int slaveIndex) { return String.format("timeline_%s_v%d_%d.png", configName, valueCategoryId, slaveIndex); } public Map<Timeline.Category, Integer> getValueCategoriesOfType(String categoryType) { return valueCategories.entrySet().stream().filter(e -> e.getKey().getType().toString().equals(categoryType)).collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue(), (v1,v2) -> { throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2)); }, TreeMap::new)); } public void createTestCharts() { createReportDirectory(); final AtomicBoolean firstDomain = new AtomicBoolean(true); final String relativeDomainFile = "domain_" + configName + "_relative.png"; final String absoluteDomainFile = "domain_" + configName + "_absolute.png"; ArrayList<Future> chartTaskFutures = new ArrayList<>(); for (Map.Entry<Timeline.Category, Integer> valueEntry : getValueCategoriesOfType(categoryType.toString()).entrySet()) { final Timeline.Category valueCategory = valueEntry.getKey(); final int valueCategoryId = valueEntry.getValue(); /* Range */ final String rangeFile = range(valueCategory, valueCategoryId); /* Charts */ final AtomicBoolean firstRange = new AtomicBoolean(true); for (Timeline timeline : timelines) { List<Timeline.Value> categoryValues = timeline.getValues(valueCategory); final List<Timeline.Value> values = categoryValues != null ? categoryValues : Collections.EMPTY_LIST; final int slaveIndex = timeline.slaveIndex; final String valueChartFile = getValueChartFile(valueCategoryId, slaveIndex); chartTaskFutures.add(HtmlReporter.executor.submit(new Callable<Void>() { @Override public Void call() throws Exception { log.info("Generating chart for " + valueCategory); TimelineChart chart = new TimelineChart(); chart.setDimensions(configuration.width, configuration.height); chart.setEvents(values, slaveIndex, startTimestamp, endTimestamp, minValues.get(valueCategory) * 1.1, maxValues.get(valueCategory) * 1.1); chart.saveChart(directory + File.separator + valueChartFile); if (firstRange.compareAndSet(true, false)) { chart.saveRange(directory + File.separator + rangeFile); } if (firstDomain.compareAndSet(true, false)) { chart.saveRelativeDomain(directory + File.separator + relativeDomainFile); chart.saveAbsoluteDomain(directory + File.separator + absoluteDomainFile); } return null; } })); } } for (Timeline timeline : timelines) { final int slaveIndex = timeline.slaveIndex; for (String ec : timeline.getEventCategories()) { final String eventCategory = ec; final List<Timeline.MarkerEvent> events = timeline.getEvents(eventCategory); if (events == null) continue; chartTaskFutures.add(HtmlReporter.executor.submit(new Callable<Void>() { @Override public Void call() throws Exception { TimelineChart chart = new TimelineChart(); chart.setDimensions(configuration.width, configuration.height); chart.setEvents(events, slaveIndex, startTimestamp, endTimestamp, 0, 0); String chartFile = String.format("timeline_%s_e%d_%d.png", configName, eventCategories.get(eventCategory), slaveIndex); chart.saveChart(directory + File.separator + chartFile); return null; } })); } } /* wait until all charts are generated */ for (Future f : chartTaskFutures) { try { f.get(); } catch (Exception e) { log.error("Failed to generate on of the charts: ", e); } } } /** * The following methods are used in Freemarker templates * e.g. method getPercentiles() can be used as getPercentiles() or percentiles in template */ public Configuration getConfiguration() { return configuration; } public String getConfigName() { return configName; } public Cluster getCluster() { return cluster; } public List<Timeline> getTimelines() { return timelines; } public Map<Timeline.Category, Double> getMinValues() { return minValues; } public Map<Timeline.Category, Double> getMaxValues() { return maxValues; } public Map<Timeline.Category, Integer> getValueCategories() { return valueCategories; } public Map<String, Integer> getEventCategories() { return eventCategories; } public long getStartTimestamp() { return startTimestamp; } public long getEndTimestamp() { return endTimestamp; } public String generateEventChartFile(int eventCategoryId, int slaveIndex) { return String.format("timeline_%s_e%d_%d.png", configName, eventCategoryId, slaveIndex); } public String getCheckboxColor(Timeline timeline) { return String.format("#%06X", TimelineChart.getColorForIndex(timeline.slaveIndex)); } public static class Configuration { @Property(name = "chart.width", doc = "Width of the chart in pixels. Default is 1024.") private int width = 1024; @Property(name = "chart.height", doc = "Height of the chart in pixels. Default is 500.") private int height = 500; /** * The following methods are used in Freemarker templates * e.g. method getPercentiles() can be used as getPercentiles() or percentiles in template */ public int getWidth() { return width; } public int getHeight() { return height; } } }