package hudson.plugins.performance.reports; import hudson.model.ModelObject; import hudson.model.Run; import hudson.plugins.performance.actions.PerformanceProjectAction; import hudson.plugins.performance.data.HttpSample; import hudson.plugins.performance.data.TaurusFinalStats; import hudson.plugins.performance.details.GraphConfigurationDetail; import hudson.util.ChartUtil; import org.apache.commons.lang.StringUtils; import org.jfree.data.time.FixedMillisecond; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.data.xy.XYDataset; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A report about a particular tested URI. * <p> * This object belongs under {@link PerformanceReport}. */ public class UriReport extends AbstractReport implements Serializable, ModelObject, Comparable<UriReport> { private static final long serialVersionUID = -5269155428479638524L; public final static String END_PERFORMANCE_PARAMETER = ".endperformanceparameter"; /** * Escaped {@link #uri} that doesn't contain any letters that cannot be used * as a token in URL. */ private final String staplerUri; private UriReport lastBuildUriReport; /** * The parent object to which this object belongs. */ private final PerformanceReport performanceReport; private String uri; /** * The amount of http samples that are not successful. */ private int nbError = 0; /** * A list that contains the date and duration (in milliseconds) of all individual samples. */ private final List<Sample> samples = new ArrayList<Sample>(); // retain insertion order. /** * A lazy cache of all duration values in {@link #samples}, insertion order (same as {@link #samples} */ private transient List<Long> durationsIO = new ArrayList<Long>(); /** * A lazy cache of all duration values in {@link #samples}, ordered by duration. */ private transient List<Long> durationsSortedBySize = new ArrayList<Long>(); /** * Indicates if the collection {@link #durationsSortedBySize} is in a sorted state. */ private transient boolean isSorted = false; /** * The duration of all samples combined, in milliseconds. */ private long totalDuration = 0; // note that this is the sum of all elements in #durations, but need not be recalculated every time. /** * The set of (unique) HTTP status codes from all samples. */ private Set<String> httpCodes = new HashSet<String>(); /** * The sum of summarizerSample values from all samples; */ private long summarizerSize = 0; /** * The sum of summarizerErrors values from all samples; */ private float summarizerErrors = 0; /** * The point in time of the start of the oldest sample. */ private Date start = null; /** * The point in time of the end of the youngest sample. */ private Date end = null; private Long average; private Long perc0; private Long perc50; private Long perc90; private Long perc100; private Long throughput; private int samplesCount; public UriReport(PerformanceReport performanceReport, String staplerUri, String uri) { this.performanceReport = performanceReport; this.staplerUri = staplerUri; this.uri = uri; } public void addHttpSample(HttpSample sample) { if (!sample.isSuccessful()) { nbError++; } synchronized (samples) { if (samples.add(new Sample(sample.getHttpCode(), sample.getDate(), sample.getDuration()))) { isSorted = false; samplesCount++; } } totalDuration += sample.getDuration(); httpCodes.add(sample.getHttpCode()); // The Set implementation will ensure that no duplicates will be saved. summarizerSize += sample.getSummarizerSamples(); summarizerErrors += sample.getSummarizerErrors(); if (start == null || sample.getDate().before(start)) { start = sample.getDate(); } Date finish = new Date(sample.getDate().getTime() + sample.getDuration()); if (end == null || finish.after(end)) { end = finish; } } public void setFromTaurusFinalStats(TaurusFinalStats report) { average = (long) report.getAverageResponseTime(); perc0 = (long) report.getPerc0(); perc50 = (long) report.getPerc50(); perc90 = (long) report.getPerc90(); perc100 = (long) report.getPerc100(); throughput = report.getThroughput(); summarizerSize = report.getBytes(); summarizerErrors = report.getFail(); nbError = report.getFail(); synchronized (samples) { samplesCount = report.getSucc() + report.getFail(); } } public int compareTo(UriReport uriReport) { if (uriReport == this) { return 0; } return uriReport.getUri().compareTo(this.getUri()); } public int countErrors() { return nbError; } public double errorPercent() { return Math.round((((double) countErrors()) / samplesCount() * 100) * 1000.0) / 1000.0; } public long getAverage() { if (average == null) { int samplesCount = samplesCount(); average = (samplesCount == 0) ? 0 : totalDuration / samplesCount; } return average; } private long getDurationAt(double percentage) { if (percentage < 0 || percentage > 1) { throw new IllegalArgumentException("Argument 'percentage' must be a value between 0 and 1 (inclusive)"); } synchronized (samples) { final List<Long> durations = getSortedDuration(); if (durations.isEmpty()) { return 0; } return durations.get((int) (samples.size() * percentage)); } } public long get90Line() { if (perc90 == null) { perc90 = getDurationAt(0.9); } return perc90; } public String getHttpCode() { return StringUtils.join(httpCodes, ','); } public long getMedian() { if (perc50 == null) { perc50 = getDurationAt(0.5); } return perc50; } public Run<?, ?> getBuild() { return performanceReport.getBuild(); } public String getDisplayName() { return getUri(); } public List<Sample> getHttpSampleList() { return samples; } public PerformanceReport getPerformanceReport() { return performanceReport; } protected List<Long> getSortedDuration() { synchronized (samples) { if (!isSorted || durationsSortedBySize == null || durationsSortedBySize.size() != samples.size()) { durationsSortedBySize = new ArrayList<Long>(samplesCount()); for (Sample sample : samples) { durationsSortedBySize.add(sample.duration); } Collections.sort(durationsSortedBySize); isSorted = true; } return durationsSortedBySize; } } public List<Long> getDurations() { synchronized (samples) { if (durationsIO == null || durationsIO.size() != samples.size()) { durationsIO = new ArrayList<Long>(samples.size()); for (Sample sample : samples) { durationsIO.add(sample.duration); } } return durationsIO; } } public long getMax() { if (perc100 == null) { final List<Long> durations = getSortedDuration(); perc100 = durations.isEmpty() ? 0 : durations.get(durations.size() - 1); } return perc100; } public long getMin() { if (perc0 == null) { final List<Long> durations = getSortedDuration(); perc0 = durations.isEmpty() ? 0 : durations.get(0); } return perc0; } public String getStaplerUri() { return staplerUri; } public String getUri() { return uri; } public String getShortUri() { if (uri.length() > 130) { return uri.substring(0, 129); } return uri; } public boolean isFailed() { return countErrors() != 0; } public int samplesCount() { synchronized (samples) { return samplesCount; } } public String encodeUriReport() throws UnsupportedEncodingException { StringBuilder sb = new StringBuilder(120); sb.append(performanceReport.getReportFileName()).append( GraphConfigurationDetail.SEPARATOR).append(getStaplerUri()).append( END_PERFORMANCE_PARAMETER); return URLEncoder.encode(sb.toString(), "UTF-8"); } public void addLastBuildUriReport(UriReport lastBuildUriReport) { this.lastBuildUriReport = lastBuildUriReport; } public long getAverageDiff() { if (lastBuildUriReport == null) { return 0; } return getAverage() - lastBuildUriReport.getAverage(); } public long getMedianDiff() { if (lastBuildUriReport == null) { return 0; } return getMedian() - lastBuildUriReport.getMedian(); } public double getErrorPercentDiff() { if (lastBuildUriReport == null) { return 0; } return Math.round((errorPercent() - lastBuildUriReport.errorPercent()) * 1000.0) / 1000.0; } public String getLastBuildHttpCodeIfChanged() { if (lastBuildUriReport == null) { return ""; } if (lastBuildUriReport.getHttpCode().equals(getHttpCode())) { return ""; } return lastBuildUriReport.getHttpCode(); } public int getSamplesCountDiff() { if (lastBuildUriReport == null) { return 0; } return samplesCount() - lastBuildUriReport.samplesCount(); } public float getSummarizerErrors() { return summarizerErrors / summarizerSize * 100; } public void doSummarizerTrendGraph(StaplerRequest request, StaplerResponse response) throws IOException { TimeSeries responseTimes = new TimeSeries("Response Time", FixedMillisecond.class); synchronized (samples) { for (Sample sample : samples) { responseTimes.addOrUpdate(new FixedMillisecond(sample.date), sample.duration); } } TimeSeriesCollection resp = new TimeSeriesCollection(); resp.addSeries(responseTimes); ArrayList<XYDataset> dataset = new ArrayList<XYDataset>(); dataset.add(resp); ChartUtil.generateGraph(request, response, PerformanceProjectAction.createSummarizerTrend(dataset, uri), 400, 200); } public Date getStart() { return start; } public Date getEnd() { return end; } public static class Sample implements Serializable, Comparable<Sample> { private static final long serialVersionUID = 4458431861223813407L; final Date date; final long duration; final String httpCode; public Sample(String httpCode, Date date, long duration) { this.httpCode = httpCode; this.date = date; this.duration = duration; } public String getHttpCode() { return httpCode; } public Date getDate() { return date; } public long getDuration() { return duration; } /** * Compare first based on duration, next on date. */ public int compareTo(Sample other) { if (this == other) return 0; if (this.duration < other.duration) return -1; if (this.duration > other.duration) return 1; if (this.date == null || other.date == null) return 0; if (this.date.before(other.date)) return -1; if (this.date.after(other.date)) return 1; return 0; } } public Long getThroughput() { return throughput; } }