blob: 448e0cc2683355082b29a5ba401bd91754dd4fb0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.jmeter.timers;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.jmeter.gui.GUIMenuSortOrder;
import org.apache.jmeter.gui.TestElementMetadata;
import org.apache.jmeter.testbeans.TestBean;
import org.apache.jmeter.testbeans.gui.GenericTestBeanCustomizer;
import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.testelement.property.DoubleProperty;
import org.apache.jmeter.testelement.property.IntegerProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.StringProperty;
import org.apache.jmeter.threads.AbstractThreadGroup;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements a constant throughput timer. A Constant Throughput
* Timer paces the samplers under its influence so that the total number of
* samples per unit of time approaches a given constant as much as possible.
*
* There are two different ways of pacing the requests:
* - delay each thread according to when it last ran
* - delay each thread according to when any thread last ran
*/
@GUIMenuSortOrder(4)
@TestElementMetadata(labelResource = "displayName")
public class ConstantThroughputTimer extends AbstractTestElement implements Timer, TestBean {
private static final long serialVersionUID = 4;
private static class ThroughputInfo{
final Object MUTEX = new Object();
long lastScheduledTime = 0;
}
private static final Logger log = LoggerFactory.getLogger(ConstantThroughputTimer.class);
private static final AtomicLong PREV_TEST_STARTED = new AtomicLong(0L);
private static final double MILLISEC_PER_MIN = 60000.0;
private static final Mode DEFAULT_CALC_MODE = Mode.ThisThreadOnly;
// TODO: most props use class simpleName as prefix but that would break backward compatiblity here
public static final String THROUGHPUT = "throughput";
public static final String CALC_MODE = "calcMode";
/**
* This enum defines the calculation modes used by the ConstantThroughputTimer.
*/
public enum Mode {
ThisThreadOnly("calcMode.1"), // NOSONAR Keep naming for compatibility
AllActiveThreads("calcMode.2"), // NOSONAR Keep naming for compatibility
AllActiveThreadsInCurrentThreadGroup("calcMode.3"), // NOSONAR Keep naming for compatibility
AllActiveThreads_Shared("calcMode.4"), // NOSONAR Keep naming for compatibility
AllActiveThreadsInCurrentThreadGroup_Shared("calcMode.5"), // NOSONAR Keep naming for compatibility
;
private final String propertyName; // The property name to be used to look up the display string
// Enum#values() clones the array, and we don't want to pay that cost as we know we don't modify the array
private static final Mode[] CACHED_VALUES = values();
Mode(String name) {
this.propertyName = name;
}
@Override
public String toString() {
return propertyName;
}
}
/**
* Target time for the start of the next request. The delay provided by the
* timer will be calculated so that the next request happens at this time.
*/
private long previousTime = 0;
//For calculating throughput across all threads
private static final ThroughputInfo allThreadsInfo = new ThroughputInfo();
//For holding the ThroughputInfo objects for all ThreadGroups. Keyed by AbstractThreadGroup objects
//TestElements can't be used as keys in HashMap.
private static final ConcurrentMap<IdentityKey<AbstractThreadGroup>, ThroughputInfo> threadGroupsInfoMap =
new ConcurrentHashMap<>();
/**
* Constructor for a non-configured ConstantThroughputTimer.
*/
public ConstantThroughputTimer() {
}
/**
* Sets the desired throughput.
*
* @param throughput
* Desired sampling rate, in samples per minute.
*/
public void setThroughput(double throughput) {
setProperty(new DoubleProperty(THROUGHPUT, throughput));
}
/**
* Gets the configured desired throughput.
*
* @return the rate at which samples should occur, in samples per minute.
*/
public double getThroughput() {
return getPropertyAsDouble(THROUGHPUT);
}
public int getCalcMode() {
return getPropertyAsInt(CALC_MODE, DEFAULT_CALC_MODE.ordinal());
}
public void setCalcMode(int mode) {
Mode resolved = Mode.CACHED_VALUES[mode];
setProperty(new IntegerProperty(CALC_MODE, resolved.ordinal()));
}
/**
* Retrieve the delay to use during test execution.
*
* @see org.apache.jmeter.timers.Timer#delay()
*/
@Override
public long delay() {
long currentTime = System.currentTimeMillis();
/*
* If previous time is zero, then target will be in the past.
* This is what we want, so first sample is run without a delay.
*/
long currentTarget = previousTime + calculateDelay();
if (currentTime > currentTarget) {
// We're behind schedule -- try to catch up:
previousTime = currentTime; // assume the sample will run immediately
return 0;
}
previousTime = currentTarget; // assume the sample will run as soon as the delay has expired
return currentTarget - currentTime;
}
/**
* Calculate the target time by adding the result of private method
* <code>calculateDelay()</code> to the given <code>currentTime</code>
*
* @param currentTime
* time in ms
* @return new Target time
*/
// TODO - is this used? (apart from test code)
protected long calculateCurrentTarget(long currentTime) {
return currentTime + calculateDelay();
}
// Calculate the delay based on the mode
private long calculateDelay() {
long testStarted = JMeterContextService.getTestStartTime();
long prevStarted = PREV_TEST_STARTED.get();
if (prevStarted != testStarted && PREV_TEST_STARTED.compareAndSet(prevStarted, testStarted)) {
// Reset counters if we are calculating throughput for a new test, see https://github.com/apache/jmeter/issues/6165
reset();
}
long delay;
// N.B. we fetch the throughput each time, as it may vary during a test
double msPerRequest = MILLISEC_PER_MIN / getThroughput();
switch (getMode()) {
case AllActiveThreads: // Total number of threads
delay = Math.round(JMeterContextService.getNumberOfThreads() * msPerRequest);
break;
case AllActiveThreadsInCurrentThreadGroup: // Active threads in this group
delay = Math.round(JMeterContextService.getContext().getThreadGroup().getNumberOfThreads() * msPerRequest);
break;
case AllActiveThreads_Shared: // All threads - alternate calculation
delay = calculateSharedDelay(allThreadsInfo,Math.round(msPerRequest));
break;
case AllActiveThreadsInCurrentThreadGroup_Shared: //All threads in this group - alternate calculation
final org.apache.jmeter.threads.AbstractThreadGroup group =
JMeterContextService.getContext().getThreadGroup();
IdentityKey<AbstractThreadGroup> key = new IdentityKey<>(group);
ThroughputInfo groupInfo = threadGroupsInfoMap.get(key);
if (groupInfo == null) {
groupInfo = threadGroupsInfoMap.computeIfAbsent(key, (k) -> new ThroughputInfo());
}
delay = calculateSharedDelay(groupInfo,Math.round(msPerRequest));
break;
case ThisThreadOnly:
default: // e.g. 0
delay = Math.round(msPerRequest); // i.e. * 1
break;
}
return delay;
}
private static long calculateSharedDelay(ThroughputInfo info, long milliSecPerRequest) {
final long now = System.currentTimeMillis();
final long calculatedDelay;
//Synchronize on the info object's MUTEX to ensure
//Multiple threads don't update the scheduled time simultaneously
synchronized (info.MUTEX) {
final long nextRequestTime = info.lastScheduledTime + milliSecPerRequest;
info.lastScheduledTime = Math.max(now, nextRequestTime);
calculatedDelay = info.lastScheduledTime - now;
}
return Math.max(calculatedDelay, 0);
}
private void reset() {
synchronized (allThreadsInfo.MUTEX) {
allThreadsInfo.lastScheduledTime = 0;
}
threadGroupsInfoMap.clear();
// no need to sync as one per instance
previousTime = 0;
}
/**
* Provide a description of this timer class.
*
* TODO: Is this ever used? I can't remember where. Remove if it isn't --
* TODO: or obtain text from bean's displayName or shortDescription.
*
* @return the description of this timer class.
*/
@Override
public String toString() {
return JMeterUtils.getResString("constant_throughput_timer_memo"); //$NON-NLS-1$
}
/**
* Override the setProperty method in order to convert
* the original String calcMode property.
* This used the locale-dependent display value, so caused
* problems when the language was changed.
* Note that the calcMode StringProperty is replaced with an IntegerProperty
* so the conversion only needs to happen once.
*/
@Override
public void setProperty(JMeterProperty property) {
if (property instanceof StringProperty) {
final String pn = property.getName();
if (pn.equals("calcMode")) {
final Object objectValue = property.getObjectValue();
try {
final BeanInfo beanInfo = Introspector.getBeanInfo(this.getClass());
final ResourceBundle rb = (ResourceBundle) beanInfo.getBeanDescriptor().getValue(GenericTestBeanCustomizer.RESOURCE_BUNDLE);
for(Enum<Mode> e : Mode.CACHED_VALUES) {
final String propName = e.toString();
if (objectValue.equals(rb.getObject(propName))) {
final int tmpMode = e.ordinal();
log.debug("Converted {}={} to mode={} using Locale: {}", pn, objectValue, tmpMode,
rb.getLocale());
super.setProperty(pn, tmpMode);
return;
}
}
log.warn("Could not convert {}={} using Locale: {}", pn, objectValue, rb.getLocale());
} catch (IntrospectionException e) {
log.error("Could not find BeanInfo", e);
}
}
}
super.setProperty(property);
}
// For access from test code
Mode getMode() {
int mode = getCalcMode();
return Mode.CACHED_VALUES[mode];
}
// For access from test code
void setMode(Mode newMode) {
setCalcMode(newMode.ordinal());
}
}