All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.lazerycode.jmeter.analyzer.parser.JMeterResultParser Maven / Gradle / Ivy

Go to download

Parses JMeter result files and computes performance indicators such as average request duration

There is a newer version: 1.0.6
Show newest version
package com.lazerycode.jmeter.analyzer.parser;

import com.lazerycode.jmeter.analyzer.statistics.Samples;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.AntPathMatcher;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import static com.lazerycode.jmeter.analyzer.config.Environment.ENVIRONMENT;
import static com.lazerycode.jmeter.analyzer.parser.StatusCodes.HTTPCODE_CONNECTIONERROR;
import static com.lazerycode.jmeter.analyzer.parser.StatusCodes.HTTPCODE_ERROR;

/**
 * Parses a JMeter xml result and provides {@link AggregatedResponses aggregated results}
 *
 * @author Dennis Homann, Arne Franken, Peter Kaul
 */
public class JMeterResultParser {

  /**
   * number of parsed items after which a log message is written
   */
  private static final int LOGMESSAGE_ITEMS = 10000;

  /**
   * Parses a JMeter Result XML file and provides a {@link AggregatedResponses} for every {@link Parser#getKey key}
   *
   * @param reader the JMeter xml file
   *
   * @return The AggregatedResponses for every thread group
   *
   * @throws IOException If reading fails
   * @throws SAXException  If parsing fails
   */
  public Map aggregate(Reader reader) throws IOException, SAXException {

    SAXParser parser;
    try {

      parser = SAXParserFactory.newInstance().newSAXParser();
    }
    catch (ParserConfigurationException e) {

      throw new IllegalStateException("Parser problems", e);
    }

    Parser p = new Parser();
    parser.parse(new InputSource(reader), p);

    return p.getResults();
  }



  // ==================

  /**
   * Parser does the heavy lifting.
   */
  private static class Parser extends DefaultHandler {

    /**
     * Example from JMeter results file:
     * 
     *
     * According to the documentation, the two possible node names are {@link #HTTPSAMPLE_ELEMENT} and {@link #SAMPLE_ELEMENT}:
     * http://jmeter.apache.org/usermanual/listeners.html
     */
    private static final String HTTPSAMPLE_ELEMENT = "httpSample";
    private static final String SAMPLE_ELEMENT = "sample";

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final AntPathMatcher matcher = new AntPathMatcher();

    private final int maxSamples;
    private final Map pathPatterns;
    private final boolean sizeByUris;
    private final boolean durationByUris;

    private long parsedCount = 0;

    private Map results = new LinkedHashMap();
    private Set nodeNames = new HashSet();

    /**
     * Constructor.
     * Fields configured from Environment
     */
    public Parser() {
      this(ENVIRONMENT.getMaxSamples(),
           ENVIRONMENT.getRequestGroups(),
           ENVIRONMENT.isGenerateCSVs(),
           ENVIRONMENT.isGenerateCSVs());
    }

    /**
     * Constructor.
     *
     * @param maxSamples The maximum number of samples that be stored internally for every metric
     * @param pathPatterns A number of ANT patterns. If set then the resulting {@link AggregatedResponses} will be
     *        grouped by uris matching these patterns. If not set then the threadgroup is used
     * @param sizeByUris true, if the response size shall be counted for each uri separately
     * @param durationByUris true, if the response duration shall be counted for each uri separately
     */
    public Parser(int maxSamples, Map pathPatterns, boolean sizeByUris, boolean durationByUris) {
      this.maxSamples = maxSamples;
      this.pathPatterns = pathPatterns;
      this.sizeByUris = sizeByUris;
      this.durationByUris = durationByUris;

      //add node names to set
      nodeNames.add(HTTPSAMPLE_ELEMENT);
      nodeNames.add(SAMPLE_ELEMENT);
    }

    /**
     * @return a mapping from identifier to aggregatedResult
     */
    public Map getResults() {
      return results;
    }

    @Override
    public void startElement(String u, String localName, String qName, Attributes atts) throws SAXException {

      if( nodeNames.contains(localName) || nodeNames.contains(qName) ) {

        String uri = atts.getValue("lb");
        String timestampString = atts.getValue("ts");
        long timestamp = Long.parseLong(timestampString);

        boolean success = Boolean.valueOf(atts.getValue("s"));

        String key = getKey(atts);

        // --- create / provide result container
        AggregatedResponses resultContainer = getResult(key);


        // --- parse bytes
        long bytes = -1;
        String byteString = atts.getValue("by");
        try {
          bytes = Long.parseLong(byteString);
        }
        catch (Exception e) {
          log.warn("Error parsing bytes: '{}'", byteString);
        }


        // --- parse duration
        long duration = -1;
        String durationString = atts.getValue("t");
        try {
          duration = Long.parseLong(durationString);
        }
        catch (Exception e) {
          log.warn("Error parsing duration '{}'", durationString);
        }

        // --- parse responseCode
        int responseCode = getResponseCode(atts);

        // ==== add data to the resultContainer
        addData(resultContainer, uri, timestamp, bytes, duration, responseCode, success);


        parsedCount++;
        
        // write a log message every 10000 entries
        if( parsedCount % LOGMESSAGE_ITEMS == 0 ) {
          log.info("Parsed {} entries ...", parsedCount);
        }
      }

      super.startElement(u, localName, qName, atts);
    }

    @Override
    public void endDocument() throws SAXException {
      super.endDocument();
      //finish collection of responses/samples
      for( AggregatedResponses r : results.values() ) {
        r.finish();
      }
    }

    //==================================================================================================================

    /**
     * Add data from httpSample to {@link AggregatedResponses the resultContainer}
     *
     * @param resultContainer container to add data to
     * @param uri uri identifying the resultContainer
     * @param timestamp httpSample timestamp
     * @param bytes httpSample bytes
     * @param duration httpSample duration
     * @param responseCode httpSample responseCode
     * @param success httpSample success
     */
    private void addData(AggregatedResponses resultContainer, String uri,
                         long timestamp, long bytes, long duration, int responseCode, boolean success) {


      StatusCodes statusCodes = resultContainer.getStatusCodes();
      statusCodes.increment(responseCode);

      // -- register data
      if( !success || bytes == -1 || duration == -1 ||
              responseCode >= HTTPCODE_ERROR || responseCode == HTTPCODE_CONNECTIONERROR ) {

        // httpSample is not okay
        // 4xx (client error) or 5xx (server error)
        Samples requestResult = resultContainer.getDuration();
        requestResult.addError(timestamp);
        Samples bytesResult = resultContainer.getSize();
        bytesResult.addError(timestamp);
      }
      else {

        // httpSample is okay
        Samples bytesResult = resultContainer.getSize();
        bytesResult.addSample(timestamp, bytes);
        Samples requestResult = resultContainer.getDuration();
        requestResult.addSample(timestamp, duration);

        Map sizeByUriMapping = resultContainer.getSizeByUri();
        Map durationByUriMapping = resultContainer.getDurationByUri();

        add(sizeByUriMapping, uri, timestamp, bytes);
        add(durationByUriMapping, uri, timestamp, duration);
      }

      //set start and end time
      if( resultContainer.getStart() == 0 ) {
        resultContainer.setStart(timestamp);
      }
      resultContainer.setEnd(timestamp);

    }

    /**
     * Create / provide {@link AggregatedResponses result container}
     *
     * @param key identifier
     *
     * @return the aggregated response matching the key
     */
    private AggregatedResponses getResult(String key) {

      AggregatedResponses resultContainer = results.get(key);
      if(resultContainer == null) {

        //initialize new AggregatedResponses
        resultContainer = new AggregatedResponses();
        resultContainer.setDuration(new Samples(maxSamples, true));
        resultContainer.setSize(new Samples(maxSamples, false));
        resultContainer.setStatusCodes(new StatusCodes());
        if( sizeByUris ) {
          resultContainer.setSizeByUri(new HashMap());
        }
        if( durationByUris ) {
          resultContainer.setDurationByUri(new HashMap());
        }

        results.put(key,resultContainer);
      }

      return resultContainer;
    }

    /**
     * Get the reponse code from the {@link Attributes}
     * Response code in  element may not be an Integer, this is a safeguard against that.
     *
     * @param atts attributes to extract the response code from
     * @return a valid response code
     */
    private int getResponseCode(Attributes atts) {

      int responseCode;
      String responseCodeString = atts.getValue("rc");
      try {

        responseCode = Integer.valueOf(responseCodeString);
      }
      catch (Exception e) {
        log.warn("Error parsing response code '{}'", responseCodeString);
        responseCode = HTTPCODE_CONNECTIONERROR;
      }

      return responseCode;
    }

    /**
     * Returns a key for the attributes.
     * If a path pattern is configured, either a matching path or "default" will be returned.
     * If no path pattern is configured, the name of the threadgroup will be used.
     *
     * @param attributes attributes to extract the key from
     * @return the key
     */
    private String getKey(Attributes attributes) {

      String key = null;
      if( pathPatterns != null ) {

        // try to find a pattern key
        String uri = attributes.getValue("lb");
        for( Map.Entry entry : pathPatterns.entrySet() ) {

          if( matcher.match(entry.getValue(), uri) ) {
            // found a pattern
            key = entry.getKey();
            break;
          }
        }

        if( key == null ) {
          // no pattern found. use default
          key = "default";
        }
      }
      else {

        // use threadgroup name as a key
        key = attributes.getValue("tn");

        //key is now "threadgroupname int-int"
        int threadGroupSeparator = key.indexOf(' ');
        if( threadGroupSeparator > -1) {
          // cut off trailing threadno
          key = key.substring(0, threadGroupSeparator);
        }
      }

      return key;
    }

    /**
     * Add #timestamp and matching #value to (new) Samples object matching the given #uri to given Map #uriSamples
     *
     * @param uriSamples map to add the Samples to
     * @param uri the uri identifying the Samples object
     * @param timestamp the timestamp
     * @param value the value
     */
    private void add(Map uriSamples, String uri, long timestamp, long value) {

      if( uriSamples != null ) {

        Samples s = uriSamples.get(uri);

        if( s == null ) {
          // no Sample was previously stored for the uri.
          s = new Samples(0, false); // 0 = don't collect samples. This is important, otherwise a OOM may occur if the result set is big

          uriSamples.put(uri, s);
        }

        s.addSample(timestamp, value);
      }
    }

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy