11package com .blazemeter .jmeter .http2 .control ;
22
33import com .blazemeter .jmeter .http2 .core .HTTP2FutureResponseListener ;
4+ import com .blazemeter .jmeter .http2 .core .SampleClock ;
45import com .blazemeter .jmeter .http2 .sampler .HTTP2Sampler ;
56import com .blazemeter .jmeter .http2 .util .BzmHttpPluginProperties ;
67import java .io .Serializable ;
1314import org .apache .jmeter .control .NextIsNullException ;
1415import org .apache .jmeter .control .TransactionController ;
1516import org .apache .jmeter .control .TransactionSampler ;
17+ import org .apache .jmeter .samplers .SampleResult ;
1618import org .apache .jmeter .samplers .Sampler ;
1719import org .apache .jmeter .testelement .TestElement ;
1820import 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
0 commit comments