/* * Copyright (c) 2013-2015 mgm technology partners GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.mgmtp.perfload.perfalyzer.reportpreparation; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Lists.newArrayListWithCapacity; import static com.google.common.collect.Sets.newHashSet; import static com.google.common.io.Files.createParentDirs; import static com.google.common.io.Files.newReader; import static com.google.common.io.Files.readLines; import static com.mgmtp.perfload.perfalyzer.constants.PerfAlyzerConstants.DELIMITER; import static com.mgmtp.perfload.perfalyzer.util.IoUtilities.writeLineToChannel; import static com.mgmtp.perfload.perfalyzer.util.PerfAlyzerUtils.readDataFile; import static com.mgmtp.perfload.perfalyzer.util.StrBuilderUtils.appendEscapedAndQuoted; import static java.lang.Math.min; import static org.apache.commons.io.FileUtils.copyFile; import static org.apache.commons.io.FileUtils.writeLines; import static org.apache.commons.lang3.StringUtils.substringAfter; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.Reader; import java.nio.channels.FileChannel; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import java.util.Set; import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.text.StrBuilder; import org.apache.commons.lang3.text.StrTokenizer; import com.google.common.base.Charsets; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ListMultimap; import com.google.common.io.Files; import com.google.common.io.LineReader; import com.mgmtp.perfload.perfalyzer.constants.PerfAlyzerConstants; import com.mgmtp.perfload.perfalyzer.reportpreparation.NumberDataSet.SeriesPoint; import com.mgmtp.perfload.perfalyzer.reportpreparation.PlotCreator.AxisType; import com.mgmtp.perfload.perfalyzer.reportpreparation.PlotCreator.ChartDimensions; import com.mgmtp.perfload.perfalyzer.reportpreparation.PlotCreator.RendererType; import com.mgmtp.perfload.perfalyzer.util.PerfAlyzerFile; import com.mgmtp.perfload.perfalyzer.util.TestMetadata; /** * @author rnaegele */ public class MeasuringReportPreparationStrategy extends AbstractReportPreparationStrategy { private final int maxHistoryItems; public MeasuringReportPreparationStrategy(final NumberFormat intNumberFormat, final NumberFormat floatNumberFormat, final List<DisplayData> displayDataList, final ResourceBundle resourceBundle, final PlotCreator plotCreator, final TestMetadata testMetadata, final DataRange dataRange, final int maxHistoryItems) { super(intNumberFormat, floatNumberFormat, displayDataList, resourceBundle, plotCreator, testMetadata, dataRange); this.maxHistoryItems = maxHistoryItems; } @Override public void processFiles(final File sourceDir, final File destDir, final List<PerfAlyzerFile> files) throws IOException { List<MeasuringHandler> handlers = ImmutableList.of( new ByOperationHandler(sourceDir, destDir), new ByOperationAggregatedHandler(sourceDir, destDir), new ByOperationExecutionsPerTimeHandler(sourceDir, destDir, "execMin"), new ByOperationExecutionsPerTimeHandler(sourceDir, destDir, "exec10Min"), new DistributionHandler(sourceDir, destDir), new QuantilesHandler(sourceDir, destDir), new RequestsPerPeriodByOperationHandler(sourceDir, destDir, PerfAlyzerConstants.BIN_SIZE_MILLIS_1_MINUTE), new RequestsPerPeriodByOperationHandler(sourceDir, destDir, PerfAlyzerConstants.BIN_SIZE_MILLIS_1_SECOND), new ErrorsHandler(sourceDir, destDir) ); for (PerfAlyzerFile f : files) { log.info("Processing file '{}'...", f); for (MeasuringHandler handler : handlers) { handler.processFile(f); } } for (MeasuringHandler handler : handlers) { handler.finishProcessing(); } } /* * Abstract base class for measuring file handlers. */ abstract class MeasuringHandler { protected final File sourceDir; protected final File destDir; public MeasuringHandler(final File sourceDir, final File destDir) { this.sourceDir = sourceDir; this.destDir = destDir; } abstract void processFile(final PerfAlyzerFile paFile) throws IOException; abstract void finishProcessing() throws IOException; } /** * Copies the quantiles files. * <p> * <pre> * Input: [measuring][<operation>][quantiles].csv * Output: [measuring][<operation>][distribution].csv * </pre> */ class QuantilesHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> byTypeMultimap = ArrayListMultimap.create(); StrTokenizer tokenizer = StrTokenizer.getCSVInstance(); public QuantilesHandler(final File sourceDir, final File destDir) { super(sourceDir, destDir); tokenizer.setDelimiterChar(DELIMITER); } @Override void processFile(final PerfAlyzerFile f) throws IOException { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 3 && "quantiles".equals(fileNameParts.get(2))) { // Simply copy the file renaming it in order to align it to the plot file File destFile = new File(destDir, f.copy().removeFileNamePart("quantiles").addFileNamePart("distribution").getFile().getPath()); copyFile(new File(sourceDir, f.getFile().getPath()), destFile); } } @Override void finishProcessing() throws IOException { // no-op } } /** * Creates response time distribution plots. * <p> * <pre> * Input: [measuring][<operation>][distribution_<index>].csv * Output: [measuring][<operation>][distribution].png * </pre> */ class DistributionHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> byTypeMultimap = ArrayListMultimap.create(); public DistributionHandler(final File sourceDir, final File destDir) { super(sourceDir, destDir); } @Override void processFile(final PerfAlyzerFile f) throws IOException { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 3 && fileNameParts.get(2).startsWith("distribution_")) { // key is the plot file name, i. e. we group effectively by operation String key = f.copy().removeFileNamePart("distribution_*").addFileNamePart("distribution").setExtension("png").getFile().getPath(); byTypeMultimap.put(key, f); } } @Override void finishProcessing() throws IOException { for (String key : byTypeMultimap.keySet()) { NumberDataSet dataSet = new NumberDataSet(); File destFile = new File(destDir, key); // one file for each request type for (PerfAlyzerFile f : byTypeMultimap.get(key)) { File file = new File(sourceDir, f.getFile().getPath()); List<SeriesPoint> dataList = readDataFile(file, Charsets.UTF_8, intNumberFormat); // extract three-digit mapping key as series name which is then shown on the chart's legend String mappingKey = substringAfter(f.getFileNameParts().get(2), "_"); dataSet.addSeries(mappingKey, dataList); } if (!dataSet.isEmpty()) { plotCreator.writePlotFile(destFile, AxisType.LOGARITHMIC, AxisType.LOGARITHMIC, RendererType.SHAPES, ChartDimensions.LARGE, null, false, dataSet); } } } } /** * Binned response times plots. * <p> * <pre> * Input: [measuring][<operation>][executions].csv * Output: [measuring][executions].png * </pre> */ class ByOperationHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> byOperationMap = ArrayListMultimap.create(); public ByOperationHandler(final File sourceDir, final File destDir) { super(sourceDir, destDir); } @Override void processFile(final PerfAlyzerFile f) { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 3 && "executions".equals(fileNameParts.get(2))) { String operation = fileNameParts.get(1); byOperationMap.put(operation, f); } } @Override void finishProcessing() throws IOException { NumberDataSet dataSet = new NumberDataSet(); PerfAlyzerFile destFile = null; for (String key : byOperationMap.keySet()) { for (PerfAlyzerFile f : byOperationMap.get(key)) { PerfAlyzerFile tmp = f.copy().removeFileNamePart(1).setExtension("png"); if (destFile == null) { destFile = tmp; } else { // safety check checkState(destFile.getFile().equals(tmp.getFile())); } File file = new File(sourceDir, f.getFile().getPath()); List<SeriesPoint> dataList = readDataFile(file, Charsets.UTF_8, intNumberFormat); dataSet.addSeries(key, dataList); } } if (!dataSet.isEmpty()) { plotCreator.writePlotFile(new File(destDir, destFile.getFile().getPath()), AxisType.LINEAR, AxisType.LINEAR, RendererType.LINES, ChartDimensions.WIDE, dataRange, false, dataSet); } } } /** * Binned executions by time plots. * <p> * <pre> * Input: [measuring][<operation>][<fileNamePart>].csv * Output: [measuring][<fileNamePart>].png * </pre> */ class ByOperationExecutionsPerTimeHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> byOperationMap = ArrayListMultimap.create(); private final String fileNamePart; public ByOperationExecutionsPerTimeHandler(final File sourceDir, final File destDir, final String fileNamePart) { super(sourceDir, destDir); this.fileNamePart = fileNamePart; } @Override void processFile(final PerfAlyzerFile f) { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 3 && fileNameParts.get(2).equals(fileNamePart)) { String operation = fileNameParts.get(1); byOperationMap.put(operation, f); } } @Override void finishProcessing() throws IOException { NumberDataSet dataSet = new NumberDataSet(); PerfAlyzerFile destFile = null; for (String key : byOperationMap.keySet()) { for (PerfAlyzerFile f : byOperationMap.get(key)) { PerfAlyzerFile tmp = f.copy().removeFileNamePart(1).setExtension("png"); if (destFile == null) { destFile = tmp; } else { // safety check checkState(destFile.getFile().equals(tmp.getFile())); } File file = new File(sourceDir, f.getFile().getPath()); List<SeriesPoint> dataList = readDataFile(file, Charsets.UTF_8, intNumberFormat); dataSet.addSeries(key, dataList); } } if (!dataSet.isEmpty()) { plotCreator.writePlotFile(new File(destDir, destFile.getFile().getPath()), AxisType.LINEAR, AxisType.LINEAR, RendererType.LINES, ChartDimensions.WIDE, dataRange, false, dataSet); } } } /** * Creates response time csv files. The contents are considered for comparison, thus comparison * files are updated as well. * <p> * <pre> * Input: [measuring][<operation>][aggregated].csv * Output: [measuring][executions].csv * </pre> */ class ByOperationAggregatedHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> byOperationAggregatedMap = ArrayListMultimap.create(); public ByOperationAggregatedHandler(final File sourceDir, final File destDir) { super(sourceDir, destDir); } @Override void processFile(final PerfAlyzerFile f) { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 3 && "aggregated".equals(fileNameParts.get(2))) { String operation = fileNameParts.get(1); byOperationAggregatedMap.put(operation, f); } } private File createDestFile(final File parentDir, final PerfAlyzerFile sourceFile, final String baseDir, final boolean dropOperationPart) { PerfAlyzerFile result = sourceFile.copy().removeFileNamePart(1); if (dropOperationPart) { result.removeFileNamePart(1); } result.addFileNamePart("executions"); result.setExtension("csv"); return new File(parentDir, new File(baseDir, result.getFile().getName()).getPath()); } @Override void finishProcessing() throws IOException { // files in this set already have a header Set<File> overallFiles = newHashSet(); for (String key : byOperationAggregatedMap.keySet()) { for (PerfAlyzerFile f : byOperationAggregatedMap.get(key)) { File destFile = createDestFile(destDir, f, "global", true); try (FileOutputStream fosOverall = new FileOutputStream(destFile, true)) { FileChannel overallChannel = fosOverall.getChannel(); StrTokenizer tokenizer = StrTokenizer.getCSVInstance(); tokenizer.setDelimiterChar(DELIMITER); File globalComparisonFile; try (Reader r = newReader(new File(sourceDir, f.getFile().getPath()), Charsets.UTF_8)) { createParentDirs(destFile); String operation = f.getFileNameParts().get(1); globalComparisonFile = createDestFile(destDir.getParentFile().getParentFile(), f, ".comparison", false); List<String> comparisonLines; // if file does not exist yet, simply write the header to it first StrBuilder sb = new StrBuilder(50); appendEscapedAndQuoted(sb, DELIMITER, "time", "numRequests", "numErrors", "minReqPerSec", "medianReqPerSec", "maxReqPerSec", "minReqPerMin", "medianReqPerMin", "maxReqPerMin", "minExecutionTime", "medianExecutionTime", "maxExecutionTime"); String comparisonHeader = sb.toString(); if (!globalComparisonFile.exists()) { createParentDirs(globalComparisonFile); comparisonLines = newArrayListWithCapacity(2); comparisonLines.add(comparisonHeader); } else { comparisonLines = readLines(globalComparisonFile, Charsets.UTF_8); // Update header in case it has changed comparisonLines.set(0, comparisonHeader); String line = comparisonLines.get(1); tokenizer.reset(line); String timestamp = tokenizer.nextToken(); if (testMetadata.getTestStart().toString().equals(timestamp)) { // report already existed for this test, i. e. we remove the last entry to create it anew comparisonLines.remove(1); } } boolean isHeaderLine = true; // files contain only two lines LineReader lineReader = new LineReader(r); for (String line; (line = lineReader.readLine()) != null; ) { if (isHeaderLine) { isHeaderLine = false; if (!overallFiles.contains(destFile)) { overallFiles.add(destFile); writeLineToChannel(overallChannel, "\"operation\"" + DELIMITER + line, Charsets.UTF_8); } } else { tokenizer.reset(line); String[] tokens = tokenizer.getTokenArray(); StrBuilder sbAggregated = new StrBuilder(line.length() + 10); appendEscapedAndQuoted(sbAggregated, DELIMITER, operation, tokens); writeLineToChannel(overallChannel, sbAggregated.toString(), Charsets.UTF_8); StrBuilder sbComparison = new StrBuilder(line.length() + 10); appendEscapedAndQuoted(sbComparison, DELIMITER, testMetadata.getTestStart().toString(), tokens); comparisonLines.add(1, sbComparison.toString()); // apply max restriction, add 1 for header comparisonLines = comparisonLines.subList(0, min(maxHistoryItems + 1, comparisonLines.size())); writeLines(globalComparisonFile, Charsets.UTF_8.name(), comparisonLines); } } } File comparisonFile = new File(destDir, "comparison" + SystemUtils.FILE_SEPARATOR + globalComparisonFile.getName()); // copy global file to this test's result files copyFile(globalComparisonFile, comparisonFile); } } } } } /** * Creates requests per minute plots. * <p> * <pre> * Input: [measuring][<operation>][requests].csv * Output: [measuring][requests].png * </pre> */ class RequestsPerPeriodByOperationHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> requestsByOperationMap = ArrayListMultimap.create(); private final int binSize; public RequestsPerPeriodByOperationHandler(final File sourceDir, final File destDir, final int binSize) { super(sourceDir, destDir); this.binSize = binSize; } @Override void processFile(final PerfAlyzerFile f) { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 4 && "requests".equals(fileNameParts.get(2)) && fileNameParts.get(3).equals(String.valueOf(binSize))) { String operation = fileNameParts.get(1); requestsByOperationMap.put(operation, f); } } @Override void finishProcessing() throws IOException { NumberDataSet dataSet = new NumberDataSet(); PerfAlyzerFile destFile = null; for (String key : requestsByOperationMap.keySet()) { for (PerfAlyzerFile f : requestsByOperationMap.get(key)) { PerfAlyzerFile tmp = f.copy().removeFileNamePart(1).setExtension("png"); if (destFile == null) { destFile = tmp; } else { // safety check checkState(destFile.getFile().equals(tmp.getFile())); } File file = new File(sourceDir, f.getFile().getPath()); List<SeriesPoint> dataList = readDataFile(file, Charsets.UTF_8, intNumberFormat); dataSet.addSeries(key, dataList); } } if (!dataSet.isEmpty()) { plotCreator.writePlotFile(new File(destDir, destFile.getFile().getPath()), AxisType.LINEAR, AxisType.LINEAR, RendererType.LINES, ChartDimensions.WIDE, dataRange, false, dataSet); } } } /** * Creates error files. * <p> * <pre> * Input: [measuring][<operation>][errorCount].csv, [measuring][<operation>][errorsByType].csv * Output: [measuring][errors].png, [measuring][errors].csv * </pre> */ class ErrorsHandler extends MeasuringHandler { ListMultimap<String, PerfAlyzerFile> errorCountsByOperationMultimap = ArrayListMultimap.create(); List<PerfAlyzerFile> errorsByType = new ArrayList<>(); public ErrorsHandler(final File sourceDir, final File destDir) { super(sourceDir, destDir); } @Override void processFile(final PerfAlyzerFile f) throws IOException { List<String> fileNameParts = f.getFileNameParts(); if (fileNameParts.size() == 3 && "errorCount".equals(fileNameParts.get(2))) { errorCountsByOperationMultimap.put(fileNameParts.get(1), f); } else if (fileNameParts.size() == 3 && "errorsByType".equals(fileNameParts.get(2))) { errorsByType.add(f); } } @Override void finishProcessing() throws IOException { NumberDataSet dataSet = new NumberDataSet(); PerfAlyzerFile destFile = null; for (String key : errorCountsByOperationMultimap.keySet()) { for (PerfAlyzerFile f : errorCountsByOperationMultimap.get(key)) { PerfAlyzerFile tmp = f.copy().removeFileNamePart(1).removeFileNamePart(1).addFileNamePart("errors").setExtension("png"); if (destFile == null) { destFile = tmp; } else { // safety check checkState(destFile.getFile().equals(tmp.getFile())); } File file = new File(sourceDir, f.getFile().getPath()); List<SeriesPoint> dataList = readDataFile(file, Charsets.UTF_8, intNumberFormat); dataSet.addSeries(key, dataList); } } if (!dataSet.isEmpty()) { File targetFile = new File(destDir, destFile.getFile().getPath()); plotCreator.writePlotFile(targetFile, AxisType.LINEAR, AxisType.LINEAR, RendererType.LINES, ChartDimensions.DEFAULT, dataRange, false, dataSet); targetFile = new File(destDir, destFile.copy().setExtension("csv").getFile().getPath()); try (FileOutputStream fos = new FileOutputStream(targetFile)) { FileChannel channel = fos.getChannel(); boolean needsHeader = true; for (PerfAlyzerFile paf : errorsByType) { try (BufferedReader br = Files.newReader(new File(sourceDir, paf.getFile().getPath()), Charsets.UTF_8)) { String header = br.readLine(); if (needsHeader) { writeLineToChannel(channel, "\"operation\"" + DELIMITER + header, Charsets.UTF_8); needsHeader = false; } String operation = paf.getFileNameParts().get(1); for (String line; (line = br.readLine()) != null; ) { writeLineToChannel(channel, "\"" + operation + "\"" + DELIMITER + line, Charsets.UTF_8); } } } } } } } }