Skip to content

Commit

Permalink
fix: allow usage of variables in ConstantThroughputTimer.throughput a…
Browse files Browse the repository at this point in the history
…nd PreciseThroughputTimer

Previously, the timers might fail initialization in case variable was not present
when test started.

The current workaround is to avoid implementing TestStateListener in the timers
so JMeter does not attempt evaluating all the properties before test starts.

fixes #6165
  • Loading branch information
vlsi committed Dec 18, 2023
1 parent a45bcc9 commit 9b250b6
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
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.TestStateListener;
import org.apache.jmeter.testelement.property.DoubleProperty;
import org.apache.jmeter.testelement.property.IntegerProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
Expand All @@ -52,14 +52,15 @@
*/
@GUIMenuSortOrder(4)
@TestElementMetadata(labelResource = "displayName")
public class ConstantThroughputTimer extends AbstractTestElement implements Timer, TestStateListener, TestBean {
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;

Expand Down Expand Up @@ -180,6 +181,13 @@ protected long calculateCurrentTarget(long currentTime) {

// 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();
Expand Down Expand Up @@ -252,18 +260,6 @@ public String toString() {
return JMeterUtils.getResString("constant_throughput_timer_memo"); //$NON-NLS-1$
}

/**
* Get the timer ready to compute delays for a new test.
* <p>
* {@inheritDoc}
*/
@Override
public void testStarted()
{
log.debug("Test started - reset throughput calculation.");
reset();
}

/**
* Override the setProperty method in order to convert
* the original String calcMode property.
Expand Down Expand Up @@ -300,30 +296,6 @@ public void setProperty(JMeterProperty property) {
super.setProperty(property);
}

/**
* {@inheritDoc}
*/
@Override
public void testEnded() {
//NOOP
}

/**
* {@inheritDoc}
*/
@Override
public void testStarted(String host) {
testStarted();
}

/**
* {@inheritDoc}
*/
@Override
public void testEnded(String host) {
//NOOP
}

// For access from test code
Mode getMode() {
int mode = getCalcMode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
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.testelement.AbstractTestElement;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.threads.AbstractThreadGroup;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.timers.Timer;
import org.apache.jorphan.collections.IdentityKey;
import org.apache.jorphan.util.JMeterStopThreadException;
Expand All @@ -41,7 +42,7 @@
*/
@GUIMenuSortOrder(3)
@TestElementMetadata(labelResource = "displayName")
public class PreciseThroughputTimer extends AbstractTestElement implements Cloneable, Timer, TestStateListener, TestBean, ThroughputProvider, DurationProvider {
public class PreciseThroughputTimer extends AbstractTestElement implements Cloneable, Timer, TestBean, ThroughputProvider, DurationProvider {
private static final Logger log = LoggerFactory.getLogger(PreciseThroughputTimer.class);

private static final long serialVersionUID = 4;
Expand All @@ -50,6 +51,8 @@ public class PreciseThroughputTimer extends AbstractTestElement implements Clone
private static final ConcurrentMap<IdentityKey<AbstractThreadGroup>, EventProducer> groupEvents =
new ConcurrentHashMap<>();

private static final AtomicLong PREV_TEST_STARTED = new AtomicLong(0L);

/**
* Desired throughput configured as {@code throughput/throughputPeriod} per second.
*/
Expand All @@ -63,8 +66,6 @@ public class PreciseThroughputTimer extends AbstractTestElement implements Clone
*/
private long duration;

private long testStarted;

/**
* When number of required samples exceeds {@code exactLimit}, random generator would resort to approximate match of
* number of generated samples.
Expand All @@ -87,31 +88,9 @@ public class PreciseThroughputTimer extends AbstractTestElement implements Clone
@Override
public Object clone() {
final PreciseThroughputTimer newTimer = (PreciseThroughputTimer) super.clone();
newTimer.testStarted = testStarted; // JMeter cloning does not clone fields
return newTimer;
}

@Override
public void testStarted() {
testStarted(null);
}

@Override
public void testStarted(String host) {
groupEvents.clear();
testStarted = System.currentTimeMillis();
}

@Override
public void testEnded() {
// NOOP
}

@Override
public void testEnded(String s) {
// NOOP
}

@Override
public long delay() {
double nextEvent;
Expand All @@ -120,6 +99,7 @@ public long delay() {
nextEvent = events.next();
}
long now = System.currentTimeMillis();
long testStarted = JMeterContextService.getTestStartTime();
long delay = (long) (nextEvent * TimeUnit.SECONDS.toMillis(1) + testStarted - now);
if (log.isDebugEnabled()) {
log.debug("Calculated delay is {}", delay);
Expand All @@ -137,6 +117,13 @@ public long delay() {
}

private EventProducer getEventProducer() {
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
groupEvents.clear();
}

AbstractThreadGroup tg = getThreadContext().getThreadGroup();
IdentityKey<AbstractThreadGroup> key = new IdentityKey<>(tg);
EventProducer eventProducer = groupEvents.get(key);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 org.apache.jmeter.control.LoopController
import org.apache.jmeter.junit.JMeterTestCase
import org.apache.jmeter.sampler.DebugSampler
import org.apache.jmeter.test.assertions.executePlanAndCollectEvents
import org.apache.jmeter.threads.ThreadGroup
import org.apache.jmeter.treebuilder.TreeBuilder
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.seconds

class ConstantThroughputTimerKtTest : JMeterTestCase() {
fun TreeBuilder.oneRequest(body: ThreadGroup.() -> Unit) {
ThreadGroup::class {
numThreads = 1
rampUp = 0
setSamplerController(
LoopController().apply {
loops = 1
}
)
body()
}
}

@Test
fun `throughput as variable`() {
val events = executePlanAndCollectEvents(5.seconds) {
oneRequest {
DebugSampler::class {
// This initializes the variable during the test execution
name = "\${__groovy( vars.put(\"throughput\"\\, \"10000\") )}"
}
DebugSampler::class {
ConstantThroughputTimer::class {
setProperty(
"throughput",
"\${__groovy( vars.get(\"throughput\").toDouble() )}"
)
setProperty("calcMode", 0)
}
}
}
}
assertEquals(
2,
events.size,
"Test should complete within reasonable time, and the test has 2 debug samplers, so we expect 2 events"
)
}
}

0 comments on commit 9b250b6

Please sign in to comment.