// 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 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.Timer; import org.apache.jmeter.util.JMeterUtils; import org.apache.jorphan.logging.LoggingManager; import org.apache.log.Logger; import java.util.ArrayList; 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<Object> curProp = (ArrayList<Object>) 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<Object> prop, int colID) throws NumberFormatException { JMeterProperty val = (JMeterProperty) prop.get(colID); return val.getDoubleValue(); } private int getIntValue(ArrayList<Object> 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(); } }