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

kg.apc.jmeter.timers.VariableThroughputTimer Maven / Gradle / Ivy

There is a newer version: 1.4.0
Show newest version
// TODO: fight with lagging on start
// TODO: create a thread which will wake up at least one sampler to provide rps
package kg.apc.jmeter.timers;

import java.util.ArrayList;

import kg.apc.jmeter.JMeterPluginsUtils;
import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.engine.util.NoThreadClone;
import org.apache.jmeter.gui.util.PowerTableModel;
import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.NullProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.timers.ConstantThroughputTimer;
import org.apache.jmeter.timers.Timer;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

/**
 * @see ConstantThroughputTimer
 */
public class VariableThroughputTimer
        extends AbstractTestElement
        implements Timer, NoThreadClone, TestStateListener {

    public static final String[] columnIdentifiers = new String[]{
            "Start RPS", "End RPS", "Duration, sec"
    };
    public static final Class[] columnClasses = new Class[]{
            String.class, String.class, String.class
    };
    // TODO: eliminate magic property
    public static final String DATA_PROPERTY = "load_profile";
    public static final int DURATION_FIELD_NO = 2;
    public static final int FROM_FIELD_NO = 0;
    public static final int TO_FIELD_NO = 1;
    private static final Logger log = LoggingManager.getLoggerForClass();
    /* put this in fields because we don't want create variables in tight loops */
    private long cntDelayed;
    private double time = 0;
    private double msecPerReq;
    private long cntSent;
    private double rps;
    private double startSec = 0;
    private CollectionProperty overrideProp;
    private int stopTries;
    private double lastStopTry;
    private boolean stopping;

    public VariableThroughputTimer() {
        super();
        trySettingLoadFromProperty();
    }

    public long delay() {
        synchronized (this) {

            while (true) {
                int delay;
                long curTime = System.currentTimeMillis();
                long msecs = curTime % 1000;
                long secs = curTime - msecs;
                checkNextSecond(secs);
                delay = getDelay(msecs);

                if (stopping) {
                    delay = delay > 0 ? 10 : 0;
                    notify();
                }

                if (delay < 1) {
                    notify();
                    break;
                }
                cntDelayed++;
                try {
                    wait(delay);
                } catch (InterruptedException ex) {
                    log.error("Waiting thread was interrupted", ex);
                }
                cntDelayed--;
            }
            cntSent++;
        }
        return 0;
    }

    private synchronized void checkNextSecond(double secs) {
        // next second
        if (time == secs) {
            return;
        }

        if (startSec == 0) {
            startSec = secs;
        }
        time = secs;

        double nextRps = getRPSForSecond((secs - startSec) / 1000);
        if (nextRps < 0) {
            stopping = true;
            rps = rps > 0 ? rps * (stopTries > 10 ? 2 : 1) : 1;
            stopTest();
            notifyAll();
        } else {
            rps = nextRps;
        }

        if (log.isDebugEnabled()) {
            log.debug("Second changed " + ((secs - startSec) / 1000) + ", sleeping: " + cntDelayed + ", sent " + cntSent + ", RPS: " + rps);
        }

        if (cntDelayed < 1) {
            log.warn("No free threads left in worker pool, made  " + cntSent + '/' + rps + " samples");
        }

        cntSent = 0;
        msecPerReq = 1000d / rps;
    }

    private int getDelay(long msecs) {
        //log.info("Calculating "+msecs + " " + cntSent * msecPerReq+" "+cntSent);
        if (msecs < (cntSent * msecPerReq)) {
            return (int) (1 + 1000.0 * (cntDelayed + 1) / rps);
        }
        return 0;
    }

    void setData(CollectionProperty rows) {
        setProperty(rows);
    }

    JMeterProperty getData() {
        if (overrideProp != null) {
            return overrideProp;
        }
        return getProperty(DATA_PROPERTY);
    }

    public double getRPSForSecond(double sec) {
        JMeterProperty data = getData();
        if (data instanceof NullProperty) return -1;
        CollectionProperty rows = (CollectionProperty) data;
        PropertyIterator scheduleIT = rows.iterator();

        while (scheduleIT.hasNext()) {
            ArrayList curProp = (ArrayList) scheduleIT.next().getObjectValue();

            int duration = getIntValue(curProp, DURATION_FIELD_NO);
            double from = getDoubleValue(curProp, FROM_FIELD_NO);
            double to = getDoubleValue(curProp, TO_FIELD_NO);
            if (sec - duration <= 0) {
                return from + sec * (to - from) / (double) duration;
            } else {
                sec -= duration;
            }
        }
        return -1;
    }

    private double getDoubleValue(ArrayList prop, int colID) throws NumberFormatException {
        JMeterProperty val = (JMeterProperty) prop.get(colID);
        return val.getDoubleValue();
    }

    private int getIntValue(ArrayList prop, int colID) throws NumberFormatException {
        JMeterProperty val = (JMeterProperty) prop.get(colID);
        return val.getIntValue();
    }

    private void trySettingLoadFromProperty() {
        String loadProp = JMeterUtils.getProperty(DATA_PROPERTY);
        log.debug("Load prop: " + loadProp);
        if (loadProp != null && loadProp.length() > 0) {
            log.info("GUI load profile will be ignored");
            PowerTableModel dataModel = new PowerTableModel(VariableThroughputTimer.columnIdentifiers, VariableThroughputTimer.columnClasses);

            String[] chunks = loadProp.split("\\)");

            for (String chunk : chunks) {
                try {
                    parseChunk(chunk, dataModel);
                } catch (RuntimeException e) {
                    log.warn("Wrong load chunk ignored: " + chunk, e);
                }
            }

            log.info("Setting load profile from property " + DATA_PROPERTY + ": " + loadProp);
            overrideProp = JMeterPluginsUtils.tableModelRowsToCollectionProperty(dataModel, VariableThroughputTimer.DATA_PROPERTY);
        }
    }

    private static void parseChunk(String chunk, PowerTableModel model) {
        log.debug("Parsing chunk: " + chunk);
        String[] parts = chunk.split("[(,]");
        String loadVar = parts[0].trim();

        if (loadVar.equalsIgnoreCase("const")) {
            int const_load = Integer.parseInt(parts[1].trim());
            Integer[] row = new Integer[3];
            row[FROM_FIELD_NO] = const_load;
            row[TO_FIELD_NO] = const_load;
            row[DURATION_FIELD_NO] = JMeterPluginsUtils.getSecondsForShortString(parts[2]);
            model.addRow(row);

        } else if (loadVar.equalsIgnoreCase("line")) {
            Integer[] row = new Integer[3];
            row[FROM_FIELD_NO] = Integer.parseInt(parts[1].trim());
            row[TO_FIELD_NO] = Integer.parseInt(parts[2].trim());
            row[DURATION_FIELD_NO] = JMeterPluginsUtils.getSecondsForShortString(parts[3]);
            model.addRow(row);

        } else if (loadVar.equalsIgnoreCase("step")) {
            // FIXME: float (from-to)/inc will be stepped wrong
            int from = Integer.parseInt(parts[1].trim());
            int to = Integer.parseInt(parts[2].trim());
            int inc = Integer.parseInt(parts[3].trim()) * (from > to ? -1 : 1);
            //log.info(from + " " + to + " " + inc);
            for (int n = from; (inc > 0 ? n <= to : n > to); n += inc) {
                //log.info(" " + n);
                Integer[] row = new Integer[3];
                row[FROM_FIELD_NO] = n;
                row[TO_FIELD_NO] = n;
                row[DURATION_FIELD_NO] = JMeterPluginsUtils.getSecondsForShortString(parts[4]);
                model.addRow(row);
            }

        } else {
            throw new RuntimeException("Unknown load type: " + parts[0]);
        }
    }

    // TODO: resolve shutdown problems. Patch JMeter if needed
    // TODO: make something with test stopping in JMeter. Write custom plugin that tries to kill all threads? Guillotine Stopper! 
    protected void stopTest() {
        if (stopTries > 30) {
            throw new RuntimeException("More than 30 seconds - stopping by exception");
        }

        if (lastStopTry == time) {
            return;
        }
        log.info("No further RPS schedule, asking threads to stop...");
        lastStopTry = time;
        stopTries++;
        if (stopTries > 10) {
            log.info("Tries more than 10, stop it NOW!");
            StandardJMeterEngine.stopEngineNow();
        } else if (stopTries > 5) {
            log.info("Tries more than 5, stop it!");
            StandardJMeterEngine.stopEngine();
        } else {
            JMeterContextService.getContext().getEngine().askThreadsToStop();
        }
    }

    @Override
    public void testStarted() {
        stopping = false;
        stopTries = 0;
    }

    @Override
    public void testStarted(String string) {
        testStarted();
    }

    @Override
    public void testEnded() {
    }

    @Override
    public void testEnded(String string) {
        testEnded();
    }


}