diff --git a/utilities/load-testing/build.xml b/utilities/load-testing/build.xml index 0e86e5f4..f29a07b3 100644 --- a/utilities/load-testing/build.xml +++ b/utilities/load-testing/build.xml @@ -48,6 +48,8 @@ report - Just summarize output from previously run tests. target: properties - - - - - - - - - - - - - - - - - --> + + @@ -96,6 +98,7 @@ report - Just summarize output from previously run tests. target: prepare - - - - - - - - - - - - - - - - - --> + @@ -189,7 +192,32 @@ report - Just summarize output from previously run tests. target: merge ================================= --> + + + + + + + + + + + + + + diff --git a/utilities/load-testing/mergedResults.css b/utilities/load-testing/mergedResults.css new file mode 100644 index 00000000..9de6b08a --- /dev/null +++ b/utilities/load-testing/mergedResults.css @@ -0,0 +1,40 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +table.testData { +} + +table.testData td,th { + border: thin solid black; + font-family: sans-serif; + text-align: center; +} + +table.testData td.left { + text-align: left; +} + +table.testData td.open { + border-right: none; +} + +table.testData td.middle { + border-left: none; + border-right: none; +} + +table.testData td.close { + border-left: none; +} + +table.oneResult td { + border: none; +} + +table.oneResult { + border: thin solid black; + width: 100%; +} + +table.oneResult td.minmax { + font-size: smaller; +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/OutputMarshaller.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/OutputMarshaller.java new file mode 100644 index 00000000..689267a5 --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/OutputMarshaller.java @@ -0,0 +1,142 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Write the merged data to an HTML page. + */ +public class OutputMarshaller { + private final List reportData; + private final PrintWriter w; + private final List testNames; + + public OutputMarshaller(List reportData, PrintWriter w) { + this.reportData = reportData; + this.w = w; + this.testNames = assembleListOfTestNames(); + } + + public void marshall() { + writePageHeader(); + writeTestDataTable(); + writePageFooter(); + } + + private List assembleListOfTestNames() { + Set names = new TreeSet(); + for (TestResultsFileData filedata : reportData) { + names.addAll(filedata.getTestMap().keySet()); + } + return new ArrayList(names); + } + + private void writePageHeader() { + w.println(""); + w.println(""); + w.println(" "); + w.println(""); + w.println(""); + } + + private void writeTestDataTable() { + w.println(""); + writeTestDataHeader(); + for (String testName : testNames) { + writeTestDataRow(testName); + } + w.println("
"); + } + + private void writeTestDataHeader() { + w.println(" "); + w.println("  "); + for (TestResultsFileData fileData : reportData) { + w.println(" " + fileData.getVivoVersion() + + "
" + fileData.getResultsFilename() + "
" + + formatDate(fileData.getCreated()) + ""); + } + w.println(" "); + + w.println(" "); + w.println(" Test Name"); + for (TestResultsFileData fileData : reportData) { + w.println(" iterations"); + w.println(" time (min/max)"); + w.println(" ratio"); + } + w.println(" "); + } + + private void writeTestDataRow(String testName) { + w.println(" "); + w.println(" " + testName + ""); + for (TestResultsFileData fileData : reportData) { + writeTestDataCellForFile(fileData, testName); + } + w.println(" "); + } + + private void writeTestDataCellForFile(TestResultsFileData fileData, + String testName) { + TestResultInfo testData = fileData.getTestMap().get(testName); + TestResultInfo baselineTestData = reportData.get(0).getTestMap() + .get(testName); + + String count = (testData == null) ? " " : ("" + testData + .getCount()); + String averageTime = (testData == null) ? " " + : ("" + formatTime(testData.getAverageTime())); + String minTime = (testData == null) ? " " + : ("" + formatTime(testData.getMinTime())); + String maxTime = (testData == null) ? " " + : ("" + formatTime(testData.getMaxTime())); + + String ratioWithBaseline = " "; + if ((testData != null) && (baselineTestData != null)) { + ratioWithBaseline = percentage(testData.getAverageTime(), + baselineTestData.getAverageTime()); + } + + w.println(" " + count + ""); + w.println(" "); + w.println(" "); + w.println(" "); + w.println(" "); + w.println(" "); + w.println(" "); + w.println(" "); + w.println(" "); + w.println(" "); + w.println("
" + averageTime + "" + minTime + "
" + maxTime + "
"); + w.println(" " + ratioWithBaseline + ""); + w.println(" "); + } + + private String percentage(float value, float baseline) { + float ratio = value / baseline; + return String.format("%1$8.2f%%", ratio * 100.0); + } + + public String formatTime(float time) { + return String.format("%1$8.3f", time); + } + + public String formatDate(long date) { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + .format(new Date(date)); + } + + private void writePageFooter() { + w.println(""); + w.println(""); + } + +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMerger.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMerger.java new file mode 100644 index 00000000..16008b9d --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMerger.java @@ -0,0 +1,70 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Take two or more reports from JMeter's test results, and merge them into a + * unified HTML report. + */ +public class ReportsMerger { + + /** + * Start with list of filenames in command line For each contributing file, + * heading is from parsing the filename. Get the one after LoadTesting, and + * the last one, minus the extension. + * + * For each file, build a structure with header info and a LinkedMap of the + * desired info, testname -> info structure Build a list of these. Build a + * unified list of testnames. + * + * List + * + * TestResultsFile: String version; String filename; Date timestamp; + * LinkedMap testMap; + * + * TestResultInfo: boolean success; int count; float averageTime; float + * maxTime; float minTime; + */ + + private final ReportsMergerParameters parms; + private List reportData; + + public ReportsMerger(ReportsMergerParameters parms) { + this.parms = parms; + } + + private void parseReports() { + List reportData = new ArrayList(); + for (File reportFile : parms.getReportFiles()) { + TestResultsFileData fileData = new TestResultsParser(reportFile) + .parse(); + System.out.println("File data: " + fileData); + reportData.add(fileData); + } + this.reportData = reportData; + } + + private void produceOutput() { + PrintWriter writer = parms.getOutputWriter(); + new OutputMarshaller(reportData, writer).marshall(); + writer.flush(); + writer.close(); + } + + /** + * @param args + */ + public static void main(String[] args) { + ReportsMergerParameters parms = ReportsMergerParameters + .getInstance(args); + ReportsMerger rm = new ReportsMerger(parms); + rm.parseReports(); + rm.produceOutput(); + } + +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMergerParameters.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMergerParameters.java new file mode 100644 index 00000000..d3ba073c --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMergerParameters.java @@ -0,0 +1,21 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +import java.io.File; +import java.io.PrintWriter; +import java.util.List; + +/** + * Parse the command-line parameters for the ReportManager + */ +public abstract class ReportsMergerParameters { + + public static ReportsMergerParameters getInstance(String[] args) { + return new ReportsMergerParametersImpl(args); + } + + public abstract List getReportFiles(); + + public abstract PrintWriter getOutputWriter(); +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMergerParametersImpl.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMergerParametersImpl.java new file mode 100644 index 00000000..6a77a88e --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/ReportsMergerParametersImpl.java @@ -0,0 +1,58 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A base implementation of ReportsMergerParameters + */ +public class ReportsMergerParametersImpl extends ReportsMergerParameters { + + private final String outputDirectoryPath; + private final String inputDirectoryPath; + private final List inputFilenames; + + /** + * The first arg is the output directory. The second arg is an input + * directory. The third arg is a comma-separated list of input filenames. + */ + public ReportsMergerParametersImpl(String[] args) { + this.outputDirectoryPath = args[0]; + this.inputDirectoryPath = args[1]; + this.inputFilenames = Arrays.asList(args[2].split("[, ]+")); + } + + @Override + public List getReportFiles() { + List files = new ArrayList(); + for (String filename : inputFilenames) { + files.add(new File(inputDirectoryPath, filename)); + } + // files.add(new File( + // "/Development/JIRA issues/NIHVIVO-1129_Load_testing/mergerFiles/LoadTesting/release1.1.1/SecondTests-rel-1-1-1.html")); + // files.add(new File( + // "/Development/JIRA issues/NIHVIVO-1129_Load_testing/mergerFiles/LoadTesting/trunkNoSdb/SecondTests-rel-1-2.html")); + // files.add(new File( + // "/Development/JIRA issues/NIHVIVO-1129_Load_testing/mergerFiles/LoadTesting/trunkSdb/SecondTests-rel-1-2.html")); + return files; + } + + @Override + public PrintWriter getOutputWriter() { + try { + File outputFile = new File(outputDirectoryPath, + "mergedResults.html"); + return new PrintWriter(new FileWriter(outputFile)); + } catch (IOException e) { + throw new RuntimeException("Can't open the output writer.", e); + } + } + +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultInfo.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultInfo.java new file mode 100644 index 00000000..a5510f8c --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultInfo.java @@ -0,0 +1,56 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +/** + * Info about the executions of a single test in a single file. + */ +public class TestResultInfo { + private final String testName; + private final boolean success; + private final int count; + private final float averageTime; + private final float maxTime; + private final float minTime; + + public TestResultInfo(String testName, boolean success, int count, + float averageTime, float maxTime, float minTime) { + this.testName = testName; + this.success = success; + this.count = count; + this.averageTime = averageTime; + this.maxTime = maxTime; + this.minTime = minTime; + } + + public String getTestName() { + return testName; + } + + public boolean isSuccess() { + return success; + } + + public int getCount() { + return count; + } + + public float getAverageTime() { + return averageTime; + } + + public float getMaxTime() { + return maxTime; + } + + public float getMinTime() { + return minTime; + } + + @Override + public String toString() { + return "TestResultInfo[testName=" + testName + ", success=" + success + + ", count=" + count + ", averageTime=" + averageTime + + ", maxTime=" + maxTime + ", minTime=" + minTime + "]"; + } +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultsFileData.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultsFileData.java new file mode 100644 index 00000000..ad7bca00 --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultsFileData.java @@ -0,0 +1,55 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Holds the data that was parsed from a single results file. + */ +public class TestResultsFileData { + private final String vivoVersion; + private final String resultsFilename; + private final long created; + private final LinkedHashMap testMap; + + public TestResultsFileData(String vivoVersion, String resultsFilename, + long created, Map testMap) { + this.vivoVersion = vivoVersion; + this.resultsFilename = resultsFilename; + this.created = created; + this.testMap = new LinkedHashMap(testMap); + } + + public String getTimeStamp() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date( + created)); + } + + public String getVivoVersion() { + return vivoVersion; + } + + public String getResultsFilename() { + return resultsFilename; + } + + public long getCreated() { + return created; + } + + public LinkedHashMap getTestMap() { + return testMap; + } + + @Override + public String toString() { + return "TestResultsFileData[vivoVersion=" + vivoVersion + + ", resultsFilename=" + resultsFilename + ", created=" + + getTimeStamp() + ", testMap=" + testMap + "]"; + } + +} diff --git a/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultsParser.java b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultsParser.java new file mode 100644 index 00000000..95fe5fe0 --- /dev/null +++ b/utilities/load-testing/src/edu/cornell/mannlib/vitro/utilities/loadtesting/TestResultsParser.java @@ -0,0 +1,168 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.loadtesting; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parse a single test results file. + */ +public class TestResultsParser { + /** Find the "Pages" table in the file. */ + private static final String PATTERN_PAGES_TABLE = "

Pages

]*>(.*?)"; + + /** Find a row in the table. */ + private static final String PATTERN_TABLE_ROW = "]*>(.*?)"; + + private final File reportFile; + private final String filePath; + + public TestResultsParser(File reportFile) { + this.reportFile = reportFile; + this.filePath = reportFile.getAbsolutePath(); + } + + /** + * Get info from the file path, the create date, and the contents of the + * file. + */ + public TestResultsFileData parse() { + String[] pathInfo = parseInfoFromFilePath(); + Map testMap = extractTestMap(); + return new TestResultsFileData(pathInfo[0], pathInfo[1], + reportFile.lastModified(), testMap); + } + + private String[] parseInfoFromFilePath() { + String vivoVersion = "--"; + String[] pathParts = filePath.split(Pattern.quote(File.separator)); + for (int i = 0; i < pathParts.length; i++) { + if (pathParts[i].startsWith("ver_")) { + vivoVersion = pathParts[i].substring(4); + } + } + + String trimmedFilename = reportFile.getName().substring(0, + reportFile.getName().lastIndexOf('.')); + + return new String[] { vivoVersion, trimmedFilename }; + } + + /** + * Scan through the contents of the file for info + */ + private Map extractTestMap() { + String contents = readEntireFileWithoutLineFeeds(); + + String pagesTable = findPagesTableInFile(contents); + // System.out.println("PagesTable: " + pagesTable); + + return parsePagesTable(pagesTable); + } + + private String readEntireFileWithoutLineFeeds() { + StringBuilder result = new StringBuilder(); + BufferedReader reader = null; + + try { + reader = new BufferedReader(new FileReader(reportFile)); + + String line; + while (null != (line = reader.readLine())) { + result.append(line); + } + return result.toString(); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "File doesn't exist: '" + filePath + "'", e); + } catch (IOException e) { + throw new RuntimeException("Failed to read the file: '" + filePath + + "'", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private String findPagesTableInFile(String contents) { + Pattern p = Pattern.compile(PATTERN_PAGES_TABLE); + Matcher m = p.matcher(contents); + if (m.find()) { + return m.group(1); + } + throw new RuntimeException("Failed to find the 'Pages' " + + "table in file: '" + filePath + "'"); + } + + private Map parsePagesTable(String pagesTable) { + Map map = new LinkedHashMap(); + + Pattern p = Pattern.compile(PATTERN_TABLE_ROW); + Matcher m = p.matcher(pagesTable); + + discardHeaderRowFromPagesTable(m); + + while (m.find()) { + TestResultInfo info = parseTestRowFromPagesTable(m.group(1)); + map.put(info.getTestName(), info); + } + return map; + } + + private void discardHeaderRowFromPagesTable(Matcher m) { + if (!m.find()) { + throw new RuntimeException("Failed to find a header row " + + "in the 'Pages' table, in file: '" + filePath + "'"); + } + } + + private TestResultInfo parseTestRowFromPagesTable(String tableRow) { + // System.out.println("Table Row: " + tableRow); + + List cells = new ArrayList(); + + Pattern p = Pattern.compile("(.*?)"); + Matcher m = p.matcher(tableRow); + while (m.find()) { + cells.add(m.group(1)); + } + // System.out.println("Cells: " + cells); + + if (cells.size() < 7) { + throw new RuntimeException("Only " + cells.size() + + " cells in this table row: '" + tableRow + + "', in file: '" + filePath + "'"); + } + + String testName = cells.get(0); + int count = Integer.parseInt(cells.get(1)); + boolean success = cells.get(2).equals("0"); + float averageTime = parseTimeFromCell(cells.get(4)); + float minTime = parseTimeFromCell(cells.get(5)); + float maxTime = parseTimeFromCell(cells.get(6)); + + return new TestResultInfo(testName, success, count, averageTime, + maxTime, minTime); + } + + private float parseTimeFromCell(String cell) { + String[] parts = cell.split(" "); + return Integer.parseInt(parts[0]) / 1000.0F; + } + +}