Skip to content

Commit

Permalink
Merge pull request #45 from guusdk/master
Browse files Browse the repository at this point in the history
[FIXED JENKINS-26988] [FIXED JENKINS-16627][FIXED JENKINS-15736]
  • Loading branch information
manolo committed Feb 18, 2015
2 parents 3e0f379 + 2e7c03f commit d5d4c87
Show file tree
Hide file tree
Showing 19 changed files with 826 additions and 595 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Expand Up @@ -6,5 +6,5 @@ target
.classpath
.project
.settings/
src/test/resources/*.jtl.serialized
/work/
src/test/resources/*.serialized
/work/
179 changes: 179 additions & 0 deletions src/main/java/hudson/plugins/performance/AbstractParser.java
@@ -0,0 +1,179 @@
package hudson.plugins.performance;

import hudson.model.AbstractBuild;
import hudson.model.TaskListener;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* An abstraction for parsing data to PerformanceReport instances. This class
* provides functionality that optimizes the parsing process, such as caching as
* well as saving/loaded parsed data in serialized form to/from disc.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public abstract class AbstractParser extends PerformanceReportParser
{
private static final Logger LOGGER = Logger.getLogger(JMeterParser.class.getName());

/**
* A suffix to be used for files in which a serialized PerformanceReport instance is stored.
*/
private static final String SERIALIZED_DATA_FILE_SUFFIX = ".serialized";

/**
* A cache that contains serialized PerformanceReport instances. This cache intends to limit disc IO.
*/
private static final Cache<String, PerformanceReport> CACHE = CacheBuilder.newBuilder().maximumSize(1000).build();

public AbstractParser(String glob) {
super(glob);
}

@Override
public Collection<PerformanceReport> parse(AbstractBuild<?, ?> build, Collection<File> reports, TaskListener listener) throws IOException
{
final List<PerformanceReport> result = new ArrayList<PerformanceReport>();

for (File reportFile : reports)
{
// Attempt to load previously serialized instances from file or cache.
final PerformanceReport deserializedReport = loadSerializedReport(reportFile);
if (deserializedReport != null) {
result.add(deserializedReport);
continue;
}

// When serialized data cannot be used, the original JMeter files are to be processed.
try {
listener.getLogger().println("Performance: Parsing JMeter report file '" + reportFile + "'.");
final PerformanceReport report = parse(reportFile);
result.add(report);
saveSerializedReport(reportFile, report);
} catch (Throwable e) {
listener.getLogger().println("Performance: Failed to parse file '" + reportFile + "': " + e.getMessage());
e.printStackTrace(listener.getLogger());
}
}
return result;
}

/**
* Performs the actual parsing of data. When the implementation throws any
* exception, the input file is ignored. This does not abort parsing of
* subsequent files.
*
* @param reportFile
* The source file (cannot be null).
* @return The parsed data (never null).
* @throws Throwable
* On any exception.
*/
abstract PerformanceReport parse(File reportFile) throws Exception;

/**
* Returns a PerformanceReport instance for the provided report file, based on
* previously serialized data.
*
* This method first attempts to load data from an internal cache. If the data
* is not in cache, data is obtained from a file on disc.
*
* When no PerformanceReport instance has previously been serialized (or when
* such data cannot be read, for instance because of class file changes), this
* method returns null.
*
* @param reportFile
* Report for which to return data. Cannot be null.
* @return deserialized data, possibly null.
*/
protected static PerformanceReport loadSerializedReport(File reportFile)
{
if (reportFile == null) {
throw new NullPointerException("Argument 'reportFile' cannot be null.");
}
final String serialized = reportFile.getPath() + SERIALIZED_DATA_FILE_SUFFIX;

ObjectInputStream in = null;
synchronized (CACHE) {
try {
PerformanceReport report = CACHE.getIfPresent(serialized);
if (report == null) {
in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(serialized)));
report = (PerformanceReport) in.readObject();
CACHE.put(serialized, report);
}
return report;
} catch (FileNotFoundException ex) {
// That's OK
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Reading serialized PerformanceReport instance from file '" + serialized + "' failed.", ex);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unable to close inputstream after attempt to read data from file '" + serialized + "'.", ex);
}
}
}
return null;
}
}

/**
* Saves a PerformanceReport instance as serialized data into a file on disc.
*
* @param reportFile
* The file from which the original data is obtained (<em>not</em>
* the file into which serialized data is to be saved!) Cannot be
* null.
* @param report
* The instance to serialize. Cannot be null.
*/
protected static void saveSerializedReport(File reportFile, PerformanceReport report)
{
if (reportFile == null) {
throw new NullPointerException("Argument 'reportFile' cannot be null.");
}
if (report == null) {
throw new NullPointerException("Argument 'report' cannot be null.");
}
final String serialized = reportFile.getPath() + SERIALIZED_DATA_FILE_SUFFIX;

synchronized (CACHE) {
CACHE.put(serialized, report);
}

ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(serialized)));
out.writeObject(report);
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Saving serialized PerformanceReport instance to file '" + serialized + "' failed.", ex);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "Unable to close outputstream after attempt to write data to file '" + serialized + "'.", ex);
}
}
}
}
}
13 changes: 7 additions & 6 deletions src/main/java/hudson/plugins/performance/AbstractReport.java
@@ -1,19 +1,19 @@
package hudson.plugins.performance;

import org.kohsuke.stapler.Stapler;

import java.text.DecimalFormatSymbols;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;

import org.kohsuke.stapler.Stapler;

/**
* Abstract class for classes with size, error, mean, average, 90 line, min and max attributes
*/
public abstract class AbstractReport {

private final NumberFormat percentFormat;
private final NumberFormat dataFormat;
protected final DecimalFormat percentFormat;
protected final DecimalFormat dataFormat; // three decimals
protected final DecimalFormat twoDForm; // two decimals

abstract public int countErrors();

Expand All @@ -24,6 +24,7 @@ public AbstractReport() {

percentFormat = new DecimalFormat("0.0", DecimalFormatSymbols.getInstance( useThisLocale ));
dataFormat = new DecimalFormat("#,###", DecimalFormatSymbols.getInstance( useThisLocale ));
twoDForm = new DecimalFormat("#.##", DecimalFormatSymbols.getInstance( useThisLocale ));
}

public String errorPercentFormated() {
Expand Down
118 changes: 49 additions & 69 deletions src/main/java/hudson/plugins/performance/IagoParser.java

Large diffs are not rendered by default.

80 changes: 33 additions & 47 deletions src/main/java/hudson/plugins/performance/JMeterCsvParser.java
@@ -1,27 +1,20 @@
package hudson.plugins.performance;

import hudson.Extension;
import hudson.model.TaskListener;
import hudson.model.AbstractBuild;
import hudson.util.FormValidation;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.xml.sax.SAXException;

public class JMeterCsvParser extends PerformanceReportParser {
public class JMeterCsvParser extends AbstractParser {

public final boolean skipFirstLine;
public final String delimiter;
Expand All @@ -33,8 +26,7 @@ public class JMeterCsvParser extends PerformanceReportParser {
public final String pattern;

@DataBoundConstructor
public JMeterCsvParser(String glob, String pattern, String delimiter,
Boolean skipFirstLine) throws Exception {
public JMeterCsvParser(String glob, String pattern, String delimiter, Boolean skipFirstLine) throws Exception {
super(glob);
this.skipFirstLine = skipFirstLine;
this.delimiter = delimiter;
Expand Down Expand Up @@ -111,65 +103,59 @@ public String getDefaultGlobPattern() {
return "**/*.csv";
}

// This may be unneccesary. I tried many things getting the pattern to show up
// This may be unnecessary. I tried many things getting the pattern to show up
// correctly in the UI and this was one of them.
public String getDefaultPattern() {
return "timestamp,elapsed,responseCode,threadName,success,failureMessage,grpThreads,allThreads,URL,Latency,SampleCount,ErrorCount";
}

@Override
public Collection<PerformanceReport> parse(AbstractBuild<?, ?> build,
Collection<File> reports, TaskListener listener) throws IOException {
List<PerformanceReport> result = new ArrayList<PerformanceReport>();

PrintStream logger = listener.getLogger();
for (File f : reports) {
final PerformanceReport r = new PerformanceReport();
r.setReportFileName(f.getName());
logger.println("Performance: Parsing JMeter report file " + f.getName());
BufferedReader reader = new BufferedReader(new FileReader(f));
try {
String line = reader.readLine();
if (line != null && skipFirstLine) {
logger.println("Performance: Skipping first line");
line = reader.readLine();
}
while (line != null) {
HttpSample sample = getSample(line);
if (sample != null) {
try {
r.addSample(sample);
} catch (SAXException e) {
throw new RuntimeException("Unnable to add sample for line "
+ line, e);
}
PerformanceReport parse(File reportFile) throws Exception {
final PerformanceReport report = new PerformanceReport();
report.setReportFileName(reportFile.getName());

final BufferedReader reader = new BufferedReader(new FileReader(reportFile));
try {
String line = reader.readLine();
if (line != null && skipFirstLine) {
line = reader.readLine();
}
while (line != null) {
final HttpSample sample = getSample(line);
if (sample != null) {
try {
report.addSample(sample);
} catch (SAXException e) {
throw new RuntimeException("Error parsing file '"+ reportFile +"': Unable to add sample for line " + line, e);
}
line = reader.readLine();
}
} finally {
if (reader != null)
reader.close();
line = reader.readLine();
}

return report;
} finally {
if (reader != null) {
reader.close();
}
result.add(r);
}
return result;
}

/**
* Parses a single HttpSample instance from a single CSV line.
*
* @param line
* file line with the provided pattern
* @return
* file line with the provided pattern (cannot be null).
* @return An sample instance (never null).
*/
private HttpSample getSample(String line) {
HttpSample sample = new HttpSample();
final HttpSample sample = new HttpSample();
final String commasNotInsideQuotes = ",(?=([^\"]*\"[^\"]*\")*[^\"]*$)";
String[] values = line.split(commasNotInsideQuotes);
final String[] values = line.split(commasNotInsideQuotes);
sample.setDate(new Date(Long.valueOf(values[timestampIdx])));
sample.setDuration(Long.valueOf(values[elapsedIdx]));
sample.setHttpCode(values[responseCodeIdx]);
sample.setSuccessful(Boolean.valueOf(values[successIdx]));
sample.setUri(values[urlIdx]);
return sample;
}

}

0 comments on commit d5d4c87

Please sign in to comment.