com.artnaseef.jmeter.report.ResultCodesStackedReport.java Source code

Java tutorial

Introduction

Here is the source code for com.artnaseef.jmeter.report.ResultCodesStackedReport.java

Source

/*
 * 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.artnaseef.jmeter.report;

import com.artnaseef.jmeter.report.cli.ReportLauncher;
import com.artnaseef.jmeter.report.jtl.model.Sample;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRenderer;
import org.jfree.chart.util.ExportUtils;
import org.jfree.data.category.DefaultCategoryDataset;

import java.awt.*;
import java.io.File;
import java.io.PrintStream;
import java.util.*;
import java.util.List;

/**
 * Generate a Stacked Bar report of requests per second broken down by result code.
 *
 * Created by art on 4/7/15.
 */
public class ResultCodesStackedReport implements FeedableReport {

    private String outputFile = "resultCodesStacked.png";
    private String detailOutputFile;

    private int reportWidth = 1000;
    private int reportHeight = 750;

    private DefaultCategoryDataset dataset;
    private JFreeChart chart;
    private Map<Integer, Map<Long, Long>> samplesByReportCode;

    private double secPerSample;
    private String yAxisLabel = "Average Samples Per Second";

    private long timeSlotSize = 1000; // In milliseconds
    private int maxSlots = 25;

    private long startTimestampSlot = -1;
    private long endTimestampSlot = -1;

    private PrintStream detailFileWriter;

    private String feedUri;

    public static void main(String[] args) {
        ResultCodesStackedReport mainObj = new ResultCodesStackedReport();

        try {
            ReportLauncher launcher = new ReportLauncher();
            launcher.launchReport(mainObj, args);
        } catch (Exception exc) {
            exc.printStackTrace();
        }
    }

    @Override
    public void onSample(Sample topLevelSample) throws Exception {
        List<Sample> subSamples = topLevelSample.getSubSamples();
        if ((subSamples != null) && (!subSamples.isEmpty())) {
            for (Sample oneSub : subSamples) {
                this.onSample(oneSub);
            }
        } else {
            this.addConcreteSample(topLevelSample);
        }
    }

    @Override
    public void onFeedStart(String uri, Properties reportProperties) throws Exception {
        this.feedUri = uri;

        this.extractReportProperties(reportProperties);

        this.samplesByReportCode = new TreeMap<>();
        this.dataset = new DefaultCategoryDataset();

        if (this.detailOutputFile != null) {
            this.detailFileWriter = new PrintStream(this.detailOutputFile);
        }
    }

    @Override
    public void onFeedComplete() throws Exception {
        this.adjustSlots();

        this.calculateTimeCustomizations();

        this.populateSeries(this.feedUri);

        this.createChart();

        ExportUtils.writeAsPNG(this.chart, this.reportWidth, this.reportHeight, new File(this.outputFile));
    }

    /**
     * Extract configuration from the given report properties.
     *
     * @param prop
     */
    protected void extractReportProperties(Properties prop) {
        this.detailOutputFile = prop.getProperty(ReportLauncher.PROPERTY_DETAIL_FILE_NAME);

        String out = prop.getProperty(ReportLauncher.PROPERTY_OUTPUT_FILENAME);
        if (out != null) {
            this.outputFile = out;
        }

        Integer size;
        size = (Integer) prop.get(ReportLauncher.PROPERTY_CHART_HEIGHT);
        if (size != null) {
            this.reportHeight = size;
        }
        size = (Integer) prop.get(ReportLauncher.PROPERTY_CHART_WIDTH);
        if (size != null) {
            this.reportWidth = size;
        }

        Long slotSize = (Long) prop.get(ReportLauncher.PROPERTY_TIME_SLOT_SIZE);
        if (slotSize != null) {
            this.timeSlotSize = slotSize;
        }

        Integer maxSlotsProperty = (Integer) prop.get(ReportLauncher.PROPERTY_MAX_SLOTS);
        if (maxSlotsProperty != null) {
            this.maxSlots = maxSlotsProperty;
        }
    }

    /**
     * Calculate adjustments to the report based on time settings.
     */
    protected void calculateTimeCustomizations() {
        this.secPerSample = (double) this.timeSlotSize / 1000.0;

        if (Math.abs(this.secPerSample - 1.0) < 0.1) {
            this.yAxisLabel = "Second";
        } else {
            this.yAxisLabel = String.format("%01.1f Second", secPerSample);
        }
    }

    /**
     * Populate the chart data feed from the aggregated sample data.
     *
     * @param sourceUri URI from which the sample data was collected for reporting purposes.
     */
    protected void populateSeries(String sourceUri) {
        // Initialize the dataset to force the order; the chart is drawn in order the data is added to the dataset.
        int cur = 0;
        while (cur < (this.endTimestampSlot - this.startTimestampSlot) + 1) {
            this.dataset.addValue(0.0, Integer.valueOf(-1), Long.valueOf(cur));
            cur++;
        }

        // Iterate over all the result codes from the input.
        for (Map.Entry<Integer, Map<Long, Long>> entry : this.samplesByReportCode.entrySet()) {
            Integer resultCode = entry.getKey();

            // Iterate over every time slot sampled for this result code and add the total samples for this result
            //  code to the chart data feed.
            for (Map.Entry<Long, Long> hitCountSeconds : entry.getValue().entrySet()) {
                long timestamp = hitCountSeconds.getKey();
                long hits = hitCountSeconds.getValue();

                Long xPoint = this.calculateXAxisOffset(timestamp); // Timestamp offset
                double yPoint = (double) hits / this.secPerSample; // Average per second

                // Add the data point to the chart data feed.
                this.dataset.addValue(yPoint, Integer.valueOf(resultCode), xPoint);

                if (this.detailFileWriter != null) {
                    this.detailFileWriter.println(String.format("%s|%d|%d|%d|%d|%f", sourceUri, resultCode,
                            hitCountSeconds.getKey(), hitCountSeconds.getValue(), xPoint, yPoint));
                }
            }
        }
    }

    //
    // Generate the chart from the data feed.
    //
    protected void createChart() {
        // create the chart...
        this.chart = ChartFactory.createStackedBarChart("Average Result Codes per Second", // chart title
                this.yAxisLabel, // x axis label
                "Samples", // y axis label
                dataset, // data
                PlotOrientation.VERTICAL, true, // include legend
                true, // tooltips
                false // urls
        );

        //
        // Adjust colors for the chart.
        //
        CategoryPlot categoryPlot;
        categoryPlot = (CategoryPlot) this.chart.getPlot();
        categoryPlot.setBackgroundPaint(Color.WHITE);
        categoryPlot.setDomainGridlinePaint(Color.BLACK);
        categoryPlot.setRangeGridlinePaint(Color.BLACK);

        //
        // Customize the bar colors.
        //
        CategoryItemRenderer renderer = this.chart.getCategoryPlot().getRenderer();
        List rowKeys = this.dataset.getRowKeys();

        int cur = 0;
        Map<Integer, Integer> colorAdjustMap = new HashMap<>();
        while (cur < rowKeys.size()) {
            Integer resultCode = (Integer) rowKeys.get(cur);

            Color color;
            int group = resultCode / 100;
            switch (group) {
            case 2:
                color = Color.GREEN;
                break;

            case 3:
                color = Color.BLUE;
                break;

            case 4:
                color = Color.ORANGE;
                break;

            case 5:
                color = Color.RED;
                break;

            default:
                color = Color.GRAY;
                break;
            }

            color = this.adjustColor(colorAdjustMap, group, color);

            renderer.setSeriesPaint(cur, color);
            renderer.setSeriesOutlinePaint(cur, Color.BLACK);

            cur++;
        }
    }

    /**
     * Adjust one color for the chart given the map of color assignments already applied, the chart grouping
     * (i.e. category or row value), and the starting color for the group.
     *
     * @param adjustMap state of adjustments already made.
     * @param group the color grouping for which to assign a color.
     * @param startColor initial color to use in the group.
     * @return
     */
    protected Color adjustColor(Map<Integer, Integer> adjustMap, int group, Color startColor) {
        Color result = startColor;
        Integer count = adjustMap.get(group);

        if (count == null) {
            adjustMap.put(group, 1);
        } else {
            int cur = 0;
            while (cur < count) {
                result = result.darker();
                cur++;
            }

            adjustMap.put(group, count + 1);
        }

        return result;
    }

    /**
     * Add a single, concrete sample to the accumulated data.  Samples are aggregated into totals by slot using the
     * configured slot size.
     *
     * @param oneSample
     */
    protected void addConcreteSample(Sample oneSample) {
        Map<Long, Long> slotSamples = this.samplesByReportCode.get(oneSample.getResultCode());

        if (slotSamples == null) {
            slotSamples = new TreeMap<>();
            this.samplesByReportCode.put(oneSample.getResultCode(), slotSamples);
        }

        long newCount = 1;
        long timeStampSlot = calculateTimestampSlot(oneSample.getTimestamp());
        Long existingCount = slotSamples.get(timeStampSlot);

        if (existingCount != null) {
            newCount += existingCount;
        }

        slotSamples.put(timeStampSlot, newCount);

        if ((this.startTimestampSlot == -1) || (timeStampSlot < this.startTimestampSlot)) {
            this.startTimestampSlot = timeStampSlot;
        }

        if ((this.endTimestampSlot == -1) || (timeStampSlot > this.endTimestampSlot)) {
            this.endTimestampSlot = timeStampSlot;
        }
    }

    /**
     * Adjust the slot size, if needed, to keep the number of slots at or below the maximum.
     */
    protected void adjustSlots() {
        long range = (this.endTimestampSlot - this.startTimestampSlot) + 1;

        if (range > maxSlots) {
            long newSlotSize = (range * this.timeSlotSize) / maxSlots;

            this.resample(this.timeSlotSize, newSlotSize);
            this.timeSlotSize = newSlotSize;
        }
    }

    /**
     * Re-sample the averages from the original slot size given to the new slot size given.  Since the source data
     * has already been aggregated into slots, the results will not be as accurate as it would be to re-run the
     * report with the ideal slot size.  However, an effort is made to apply anti-aliasing so the resulting graph
     * should have a nearly identical overall shape to the original.
     *
     * @param origSlotSize
     * @param newSlotSize
     */
    protected void resample(long origSlotSize, long newSlotSize) {
        Map<Integer, Map<Long, Long>> updatedSamplesByReportCode = new TreeMap<>();
        long updatedStartTimeSlot = Integer.MAX_VALUE;
        long updatedEndTimeSlot = 0;

        double slotRatio = (double) origSlotSize / (double) newSlotSize;

        for (Integer oneRc : this.samplesByReportCode.keySet()) {
            Map<Long, Long> origSamplesOneRc = this.samplesByReportCode.get(oneRc);
            Map<Long, Long> newSamplesOneRc;

            newSamplesOneRc = new TreeMap<>();
            updatedSamplesByReportCode.put(oneRc, newSamplesOneRc);

            for (Long origSlot : origSamplesOneRc.keySet()) {
                // Calculate the left-side position for the re-sample, and the percentage that the old samples "cover"
                //  the left-side.  The right-side will get any remainder after populating the left side.  Remember
                //  that there is integer arithmetic here and automatic truncation of decimals.
                double newSlotTgtPt = origSlot * slotRatio;
                long newLeftSlot = (long) newSlotTgtPt;
                double leftPct = 1 - (newSlotTgtPt - newLeftSlot);

                // Update the count on the left size, if any.
                long leftCount = (long) (origSamplesOneRc.get(origSlot) * leftPct);
                if (leftCount > 0) {
                    Long orig = newSamplesOneRc.get(newLeftSlot);
                    if (orig == null) {
                        newSamplesOneRc.put(newLeftSlot, leftCount);
                    } else {
                        newSamplesOneRc.put(newLeftSlot, leftCount + orig);
                    }

                    // Adjust the updated start and end slots
                    if (newLeftSlot < updatedStartTimeSlot) {
                        updatedStartTimeSlot = newLeftSlot;
                    }
                    if (newLeftSlot > updatedEndTimeSlot) {
                        updatedEndTimeSlot = newLeftSlot;
                    }
                }

                // Update the count on the right side, if anything remains.
                long rightCount = origSamplesOneRc.get(origSlot) - leftCount;
                if (rightCount > 0) {
                    Long orig = newSamplesOneRc.get(newLeftSlot + 1);
                    if (orig == null) {
                        newSamplesOneRc.put(newLeftSlot + 1, leftCount);
                    } else {
                        newSamplesOneRc.put(newLeftSlot + 1, leftCount + orig);
                    }

                    // Adjust the updated start and end slots
                    if (newLeftSlot < updatedStartTimeSlot) {
                        updatedStartTimeSlot = newLeftSlot;
                    }
                    if (newLeftSlot > updatedEndTimeSlot) {
                        updatedEndTimeSlot = newLeftSlot;
                    }
                }
            }
        }

        // Replace the originals with the updates.
        this.samplesByReportCode = updatedSamplesByReportCode;
        this.startTimestampSlot = updatedStartTimeSlot;
        this.endTimestampSlot = updatedEndTimeSlot;
    }

    protected long calculateTimestampSlot(long timestamp) {
        return timestamp / this.timeSlotSize;
    }

    protected long calculateXAxisOffset(long timestampSlot) {
        long result = timestampSlot - this.startTimestampSlot;

        return result;
    }
}