Skip to content

Commit 56496c3

Browse files
committed
Fix samplers timing
1 parent 1a610ae commit 56496c3

14 files changed

Lines changed: 1026 additions & 48 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ Add it with **Add → Logic Controller → bzm - HTTP Async Controller**. **`bzm
374374
| **Field** | **Description** | **Default** |
375375
|---|---|---|
376376
| Generate Parent Sample | Wraps child BlazeMeter HTTP results in one parent sample (sub-results in listeners/reports). | Off (`false`) |
377+
| Include duration of timer and pre-post processors in generated sample | Only applies with **Generate Parent Sample** on. When **off**, the parent sample measures the requests only: from the first one sent to the last response received, so think times and pre/post-processor time are reported as idle time instead of transaction time. When **on**, the parent sample spans the whole controller, pauses included (JMeter's Transaction Controller behaviour). | Off (`false`) |
377378
| Limit max number of parallel executions | When **off**, the cap is **`blazemeter.http.maxConcurrentAsyncInController`**. When **on**, cap is **Max parallel** (saved in the `.jmx` only while this is on; runtime still uses the global cap when off). | Off (`false`) |
378379
| Max parallel | Highest number of overlapping BlazeMeter HTTP samplers when limiting is **on** (integer ≥ **1**). Read-only when limiting is **off** (shows the effective global cap). | **100** (matches **`blazemeter.http.maxConcurrentAsyncInController`** unless you override; with limiting **on**, use the value you enter) |
379380

src/main/java/com/blazemeter/jmeter/http2/control/HTTP2Controller.java

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.blazemeter.jmeter.http2.control;
22

33
import com.blazemeter.jmeter.http2.core.HTTP2FutureResponseListener;
4+
import com.blazemeter.jmeter.http2.core.SampleClock;
45
import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler;
56
import com.blazemeter.jmeter.http2.util.BzmHttpPluginProperties;
67
import java.io.Serializable;
@@ -13,6 +14,7 @@
1314
import org.apache.jmeter.control.NextIsNullException;
1415
import org.apache.jmeter.control.TransactionController;
1516
import org.apache.jmeter.control.TransactionSampler;
17+
import org.apache.jmeter.samplers.SampleResult;
1618
import org.apache.jmeter.samplers.Sampler;
1719
import org.apache.jmeter.testelement.TestElement;
1820
import org.apache.jmeter.testelement.property.JMeterProperty;
@@ -56,6 +58,8 @@ public class HTTP2Controller extends TransactionController implements Serializab
5658
BzmHttpPluginProperties.CONTROLLER_PREFERRED_PREFIX + "maxConcurrentAsyncInController";
5759
private static final String MAX_CONCURRENT_LEGACY =
5860
BzmHttpPluginProperties.CONTROLLER_LEGACY_PREFIX + "maxConcurrentAsyncInController";
61+
/** JMeter's own property name, so a value set on a stock Transaction Controller still reads. */
62+
private static final String INCLUDE_TIMERS = "TransactionController.includeTimers";
5963

6064
private static final int DEFAULT_MAX_CONCURRENT_ASYNC_IN_CONTROLLER = 100;
6165
private static final long COMPLETION_POLL_INTERVAL_MILLIS = 10;
@@ -69,6 +73,12 @@ public class HTTP2Controller extends TransactionController implements Serializab
6973
private transient boolean handingOutPendingSampler;
7074
private transient boolean nestedSequentialRequestWarned;
7175
private boolean generateControllerSample;
76+
/**
77+
* The parent transaction currently open, so {@link #triggerEndOfLoop()} can still reach it: the
78+
* field {@link TransactionController} keeps it in is private and is already null by the time
79+
* {@code super.triggerEndOfLoop()} returns.
80+
*/
81+
private transient TransactionSampler openParentTransaction;
7282

7383
public HTTP2Controller() {
7484
super();
@@ -148,6 +158,7 @@ public void setGenerateParentSample(boolean generateParent) {
148158
public Sampler next() {
149159
if (isGenerateParentSample()) {
150160
Sampler next = super.next();
161+
measureParentSample(next);
151162
// Only after the controller has finished the parent transaction (next == null). Releasing
152163
// while still returning the done TransactionSampler is pointless: JMeterThread calls
153164
// configureTransactionSampler(done) next and puts that same instance back on the package.
@@ -161,6 +172,128 @@ public Sampler next() {
161172
return nextWithoutTransactionBookkeeping();
162173
}
163174

175+
/**
176+
* What the parent sample must report is the time this controller spent on requests, which for
177+
* overlapped requests is the span from the first one leaving to the last one arriving.
178+
*
179+
* <p>Neither of {@link TransactionSampler}'s two modes measures that. With "include timers" on it
180+
* leaves the sample stretching from the moment the transaction opened - before the timers and
181+
* pre-processors of the first request ran - to the last response, so a think time lands inside
182+
* the transaction time (issue #155). With it off, {@code setTransactionDone} reports the sum of
183+
* the children instead, which double counts requests that ran at the same time. So the span is
184+
* measured here, out of the children's own stamps, exactly as this controller did up to v3.0.1.
185+
*
186+
* <p>Also stamps an end time on a transaction that is still open. A transaction that never gets
187+
* one reports {@code 0 - startTime}: {@code setTransactionDone} only stamps it in the
188+
* "include timers" off branch, and {@code JMeterThread} does not call it at all when the run is
189+
* cut short - the scheduler expiring, a manual Stop - it just ends the open transaction where it
190+
* stands. An enclosing Transaction Controller then folds that result in through
191+
* {@code SampleResult.addSubResult}, whose {@code Math.max} keeps the zero, and the enclosing
192+
* sample comes out as a negative epoch that wrecks the Average and the Min of every aggregate
193+
* over the run.
194+
*/
195+
private void measureParentSample(Sampler next) {
196+
if (!(next instanceof TransactionSampler)) {
197+
return;
198+
}
199+
TransactionSampler transactionSampler = (TransactionSampler) next;
200+
if (transactionSampler.isTransactionDone()) {
201+
openParentTransaction = null;
202+
applyRequestSpan(transactionSampler);
203+
return;
204+
}
205+
openParentTransaction = transactionSampler;
206+
SampleResult parent = transactionSampler.getTransactionResult();
207+
if (parent != null && parent.getEndTime() == 0) {
208+
// Floor: a transaction ended from the outside now reports 0 ms instead of -startTime. Every
209+
// child that arrives afterwards pushes it forward again through addSubResult's Math.max.
210+
parent.setEndTime(parent.getStartTime());
211+
}
212+
}
213+
214+
/**
215+
* Rewrites the finished parent sample as {@code lastResponse - firstRequestSent}, leaving what
216+
* came before the first request (timers, pre-processors) in {@code idleTime}, which is the field
217+
* JMeter itself uses for it. Keeps the wall clock reading when the user asked for the timers to
218+
* be included.
219+
*
220+
* @see #isIncludeTimers()
221+
*/
222+
private void applyRequestSpan(TransactionSampler transactionSampler) {
223+
if (isIncludeTimers()) {
224+
return;
225+
}
226+
SampleResult parent = transactionSampler.getTransactionResult();
227+
if (parent == null) {
228+
return;
229+
}
230+
long firstStart = Long.MAX_VALUE;
231+
long lastEnd = 0;
232+
for (SampleResult child : parent.getSubResults()) {
233+
// Each child's stamps are on its own clock, put on the transaction's the same way
234+
// SampleResult.addSubResult does when it extends a parent's end time (Bug 51855).
235+
if (child.getStartTime() > 0) {
236+
firstStart = Math.min(firstStart,
237+
SampleClock.fromResultClock(parent, child, child.getStartTime()));
238+
}
239+
if (child.getEndTime() > 0) {
240+
lastEnd = Math.max(lastEnd,
241+
SampleClock.fromResultClock(parent, child, child.getEndTime()));
242+
}
243+
}
244+
if (firstStart == Long.MAX_VALUE || lastEnd < firstStart) {
245+
// Nothing was measured: an iteration that was cut short, or children that never got stamps.
246+
parent.setIdleTime(0);
247+
parent.setEndTime(parent.getStartTime());
248+
return;
249+
}
250+
// elapsed = endTime - startTime - idleTime, so this reads exactly lastEnd - firstStart. Going
251+
// through idleTime rather than a synthetic end time leaves the sample ending when its last
252+
// request did, and holding what came before the first one in the field JMeter's own Transaction
253+
// Controller keeps a pause in (Bug 50080).
254+
parent.setIdleTime(firstStart - parent.getStartTime());
255+
parent.setEndTime(lastEnd);
256+
}
257+
258+
/**
259+
* The children of an aborted iteration - Start Next Loop, Stop Thread - are attached by
260+
* {@code super.triggerEndOfLoop()}, which also closes the transaction, so the span can only be
261+
* measured after it.
262+
*/
263+
@Override
264+
public void triggerEndOfLoop() {
265+
TransactionSampler ending = openParentTransaction;
266+
openParentTransaction = null;
267+
super.triggerEndOfLoop();
268+
if (ending != null) {
269+
applyRequestSpan(ending);
270+
}
271+
}
272+
273+
/**
274+
* Same question a stock Transaction Controller asks, with the opposite default: a think time is a
275+
* pause, not request time, and this controller has never counted it. Answering {@code true} by
276+
* inheritance - which is what JMeter's own default does, for compatibility with test plans older
277+
* than its checkbox - is what put the think time inside the transaction time in v3.1.0. An
278+
* explicit value, from this element's GUI or from a JMX written against a stock Transaction
279+
* Controller, is honoured.
280+
*/
281+
@Override
282+
public boolean isIncludeTimers() {
283+
return containsElementPropertyNamed(INCLUDE_TIMERS)
284+
&& getPropertyAsBoolean(INCLUDE_TIMERS, false);
285+
}
286+
287+
/**
288+
* Always writes the property. {@code TransactionController.setIncludeTimers} drops it when it
289+
* matches JMeter's default of {@code true}, which would leave this controller reading its own
290+
* default of {@code false} and silently discard the user's choice.
291+
*/
292+
@Override
293+
public void setIncludeTimers(boolean includeTimers) {
294+
setProperty(INCLUDE_TIMERS, includeTimers);
295+
}
296+
164297
/**
165298
* Same workaround as JMeter PR #6386: when the parent transaction has finished, replace the
166299
* completed {@link TransactionSampler} in this controller's {@link SamplePackage} with a fresh

src/main/java/com/blazemeter/jmeter/http2/control/gui/HTTP2ControllerGUI.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
public class HTTP2ControllerGUI extends AbstractControllerGui implements Scrollable {
1919
private static final long serialVersionUID = 240L;
2020
private final JCheckBox generateControllerSample;
21+
private final JCheckBox includeTimers;
2122
private final JCheckBox limitMaxParallel;
2223
private final JTextField maxParallelField;
2324
private int defaultMaxParallel;
2425

2526
public HTTP2ControllerGUI() {
2627
generateControllerSample = new JCheckBox("Generate Parent Sample");
28+
includeTimers = new JCheckBox(
29+
"Include duration of timer and pre-post processors in generated sample");
2730
limitMaxParallel = new JCheckBox("Limit max number of parallel executions");
2831
maxParallelField = new JTextField(8);
2932
init();
@@ -47,6 +50,7 @@ public void modifyTestElement(TestElement el) {
4750
if (el instanceof HTTP2Controller) {
4851
HTTP2Controller controller = (HTTP2Controller) el;
4952
controller.setGenerateControllerSample(generateControllerSample.isSelected());
53+
controller.setIncludeTimers(includeTimers.isSelected());
5054
controller.setLimitMaxParallel(limitMaxParallel.isSelected());
5155
if (limitMaxParallel.isSelected()) {
5256
controller.setMaxConcurrentAsyncInController(parseMaxParallelValue());
@@ -60,6 +64,8 @@ public void configure(TestElement element) {
6064
if (element instanceof HTTP2Controller) {
6165
HTTP2Controller controller = (HTTP2Controller) element;
6266
generateControllerSample.setSelected(controller.isGenerateControllerSample());
67+
includeTimers.setSelected(controller.isIncludeTimers());
68+
updateIncludeTimersState(controller.isGenerateControllerSample());
6369
limitMaxParallel.setSelected(controller.isLimitMaxParallel());
6470
maxParallelField.setText(String.valueOf(controller.getMaxConcurrentAsyncInController()));
6571
updateMaxParallelFieldState(controller);
@@ -78,6 +84,11 @@ private void init() {
7884
add(makeTitlePanel(), BorderLayout.NORTH);
7985
add(buildOptionsPanel(), BorderLayout.CENTER);
8086

87+
// The timer question only exists for the sample this controller generates itself: with no
88+
// parent sample there is nothing whose duration could include them.
89+
generateControllerSample.addItemListener(
90+
event -> updateIncludeTimersState(event.getStateChange() == ItemEvent.SELECTED));
91+
8192
limitMaxParallel.addItemListener(event -> {
8293
boolean enabled = event.getStateChange() == ItemEvent.SELECTED;
8394
updateMaxParallelFieldState(enabled);
@@ -99,6 +110,9 @@ private JPanel buildOptionsPanel() {
99110
generateControllerSample.setAlignmentX(JCheckBox.LEFT_ALIGNMENT);
100111
panel.add(generateControllerSample);
101112

113+
includeTimers.setAlignmentX(JCheckBox.LEFT_ALIGNMENT);
114+
panel.add(includeTimers);
115+
102116
limitMaxParallel.setAlignmentX(JCheckBox.LEFT_ALIGNMENT);
103117
panel.add(limitMaxParallel);
104118

@@ -110,6 +124,10 @@ private JPanel buildOptionsPanel() {
110124
return panel;
111125
}
112126

127+
private void updateIncludeTimersState(boolean generatesParentSample) {
128+
includeTimers.setEnabled(generatesParentSample);
129+
}
130+
113131
private void updateMaxParallelFieldState(HTTP2Controller controller) {
114132
defaultMaxParallel = controller.getDefaultMaxConcurrentAsyncInController();
115133
updateMaxParallelFieldState(controller.isLimitMaxParallel());

0 commit comments

Comments
 (0)