context) {
+ super(endPoint instanceof KeepAliveParityEndPoint ? endPoint
+ : new KeepAliveParityEndPoint(endPoint), context);
+ }
+
+ @Override
+ protected HttpChannelOverHTTP newHttpChannel() {
+ return new CustomHttpChannelOverHTTP(this);
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/jetty/custom/http1/CustomHttpSenderOverHTTP.java b/src/main/java/com/blazemeter/jmeter/http2/core/jetty/custom/http1/CustomHttpSenderOverHTTP.java
new file mode 100644
index 0000000..3f328e0
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/http2/core/jetty/custom/http1/CustomHttpSenderOverHTTP.java
@@ -0,0 +1,108 @@
+package com.blazemeter.jmeter.http2.core.jetty.custom.http1;
+
+import com.blazemeter.jmeter.http2.core.JmeterHttpClientAttributes;
+import java.nio.ByteBuffer;
+import java.util.Locale;
+import org.eclipse.jetty.client.transport.HttpExchange;
+import org.eclipse.jetty.client.transport.HttpRequest;
+import org.eclipse.jetty.client.transport.internal.HttpChannelOverHTTP;
+import org.eclipse.jetty.client.transport.internal.HttpConnectionOverHTTP;
+import org.eclipse.jetty.client.transport.internal.HttpSenderOverHTTP;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * HTTP/1 sender that preserves explicit {@code Connection: keep-alive} like Apache HttpClient4.
+ */
+public class CustomHttpSenderOverHTTP extends HttpSenderOverHTTP {
+
+ public CustomHttpSenderOverHTTP(HttpChannelOverHTTP channel) {
+ super(channel);
+ }
+
+ @Override
+ protected void sendHeaders(HttpExchange exchange, ByteBuffer contentBuffer, boolean lastContent,
+ Callback callback) {
+ HttpRequest request = exchange.getRequest();
+ if (!shouldEmitExplicitKeepAlive(request)) {
+ super.sendHeaders(exchange, contentBuffer, lastContent, callback);
+ return;
+ }
+
+ HttpVersion savedVersion = request.getVersion();
+ KeepAliveParityEndPoint parityEndPoint = resolveParityEndPoint();
+ if (parityEndPoint != null) {
+ parityEndPoint.enableRequestLinePatch();
+ }
+ request.version(HttpVersion.HTTP_1_0);
+ super.sendHeaders(exchange, contentBuffer, lastContent, new Callback() {
+ @Override
+ public void succeeded() {
+ try {
+ callback.succeeded();
+ } finally {
+ request.version(savedVersion);
+ if (parityEndPoint != null) {
+ parityEndPoint.disableRequestLinePatch();
+ }
+ }
+ }
+
+ @Override
+ public void failed(Throwable x) {
+ try {
+ callback.failed(x);
+ } finally {
+ request.version(savedVersion);
+ if (parityEndPoint != null) {
+ parityEndPoint.disableRequestLinePatch();
+ }
+ }
+ }
+ });
+ }
+
+ private KeepAliveParityEndPoint resolveParityEndPoint() {
+ HttpConnectionOverHTTP connection = getHttpChannel().getHttpConnection();
+ EndPoint endPoint = connection.getEndPoint();
+ if (endPoint instanceof KeepAliveParityEndPoint) {
+ return (KeepAliveParityEndPoint) endPoint;
+ }
+ if (endPoint instanceof EndPoint.Wrapper) {
+ EndPoint unwrapped = ((EndPoint.Wrapper) endPoint).unwrap();
+ if (unwrapped instanceof KeepAliveParityEndPoint) {
+ return (KeepAliveParityEndPoint) unwrapped;
+ }
+ }
+ return null;
+ }
+
+ private boolean shouldEmitExplicitKeepAlive(HttpRequest request) {
+ if (hasBlockingConnectionToken(request)) {
+ return false;
+ }
+ Object useKeepAlive = request.getAttributes().get(JmeterHttpClientAttributes.USE_KEEPALIVE);
+ if (Boolean.TRUE.equals(useKeepAlive)) {
+ return request.getVersion() == HttpVersion.HTTP_1_1;
+ }
+ String connection = request.getHeaders().get(HttpHeader.CONNECTION);
+ return connection != null
+ && connection.toLowerCase(Locale.ROOT).contains("keep-alive");
+ }
+
+ private boolean hasBlockingConnectionToken(HttpRequest request) {
+ String connection = request.getHeaders().get(HttpHeader.CONNECTION);
+ if (connection == null) {
+ return false;
+ }
+ for (String token : connection.split(",")) {
+ String trimmed = token.trim();
+ if ("Upgrade".equalsIgnoreCase(trimmed) || "close".equalsIgnoreCase(trimmed)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/jetty/custom/http1/KeepAliveParityEndPoint.java b/src/main/java/com/blazemeter/jmeter/http2/core/jetty/custom/http1/KeepAliveParityEndPoint.java
new file mode 100644
index 0000000..3e93bbd
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/http2/core/jetty/custom/http1/KeepAliveParityEndPoint.java
@@ -0,0 +1,226 @@
+package com.blazemeter.jmeter.http2.core.jetty.custom.http1;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadPendingException;
+import java.nio.channels.WritePendingException;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Patches the HTTP request line on the wire from {@code HTTP/1.0} to {@code HTTP/1.1}.
+ *
+ * Jetty's {@code HttpGenerator} strips explicit {@code Connection: keep-alive} for HTTP/1.1
+ * but keeps it for HTTP/1.0. The custom HTTP/1 sender temporarily uses HTTP/1.0 metadata and
+ * this endpoint rewrites only the request line so JMeter mirror assertions still see HTTP/1.1.
+ */
+public class KeepAliveParityEndPoint implements EndPoint, EndPoint.Wrapper {
+
+ private final EndPoint delegate;
+ private volatile boolean patchRequestLineToHttp11;
+
+ public KeepAliveParityEndPoint(EndPoint delegate) {
+ this.delegate = delegate;
+ }
+
+ public void enableRequestLinePatch() {
+ patchRequestLineToHttp11 = true;
+ }
+
+ public void disableRequestLinePatch() {
+ patchRequestLineToHttp11 = false;
+ }
+
+ @Override
+ public EndPoint unwrap() {
+ return delegate;
+ }
+
+ @Override
+ public SocketAddress getLocalSocketAddress() {
+ return delegate.getLocalSocketAddress();
+ }
+
+ @Override
+ public SocketAddress getRemoteSocketAddress() {
+ return delegate.getRemoteSocketAddress();
+ }
+
+ @Override
+ public int fill(ByteBuffer buffer) throws IOException {
+ return delegate.fill(buffer);
+ }
+
+ @Override
+ public SocketAddress receive(ByteBuffer buffer) throws IOException {
+ return delegate.receive(buffer);
+ }
+
+ @Override
+ public boolean flush(ByteBuffer... buffers) throws IOException {
+ return delegate.flush(buffers);
+ }
+
+ @Override
+ public boolean send(SocketAddress address, ByteBuffer... buffers) throws IOException {
+ return delegate.send(address, buffers);
+ }
+
+ @Override
+ public boolean isSecure() {
+ return delegate.isSecure();
+ }
+
+ @Override
+ public SslSessionData getSslSessionData() {
+ return delegate.getSslSessionData();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return delegate.isOpen();
+ }
+
+ @Override
+ public long getCreatedTimeStamp() {
+ return delegate.getCreatedTimeStamp();
+ }
+
+ @Override
+ public void shutdownOutput() {
+ delegate.shutdownOutput();
+ }
+
+ @Override
+ public boolean isOutputShutdown() {
+ return delegate.isOutputShutdown();
+ }
+
+ @Override
+ public boolean isInputShutdown() {
+ return delegate.isInputShutdown();
+ }
+
+ @Override
+ public void close(Throwable cause) {
+ delegate.close(cause);
+ }
+
+ @Override
+ public Object getTransport() {
+ return delegate.getTransport();
+ }
+
+ @Override
+ public long getIdleTimeout() {
+ return delegate.getIdleTimeout();
+ }
+
+ @Override
+ public void setIdleTimeout(long idleTimeout) {
+ delegate.setIdleTimeout(idleTimeout);
+ }
+
+ @Override
+ public void fillInterested(Callback callback) throws ReadPendingException {
+ delegate.fillInterested(callback);
+ }
+
+ @Override
+ public boolean tryFillInterested(Callback callback) {
+ return delegate.tryFillInterested(callback);
+ }
+
+ @Override
+ public boolean isFillInterested() {
+ return delegate.isFillInterested();
+ }
+
+ @Override
+ public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException {
+ delegate.write(callback, maybePatch(buffers));
+ }
+
+ @Override
+ public void write(Callback callback, SocketAddress address, ByteBuffer... buffers)
+ throws WritePendingException {
+ delegate.write(callback, address, maybePatch(buffers));
+ }
+
+ @Override
+ public void write(boolean last, ByteBuffer buffer, Callback callback) {
+ delegate.write(last, maybePatch(buffer), callback);
+ }
+
+ @Override
+ public Callback cancelWrite(Throwable cause) {
+ return delegate.cancelWrite(cause);
+ }
+
+ @Override
+ public Connection getConnection() {
+ return delegate.getConnection();
+ }
+
+ @Override
+ public void setConnection(Connection connection) {
+ delegate.setConnection(connection);
+ }
+
+ @Override
+ public void onOpen() {
+ delegate.onOpen();
+ }
+
+ @Override
+ public void onClose(Throwable cause) {
+ delegate.onClose(cause);
+ }
+
+ @Override
+ public void upgrade(Connection newConnection) {
+ delegate.upgrade(newConnection);
+ }
+
+ private ByteBuffer[] maybePatch(ByteBuffer... buffers) {
+ if (!patchRequestLineToHttp11 || buffers == null) {
+ return buffers;
+ }
+ ByteBuffer[] patched = new ByteBuffer[buffers.length];
+ for (int i = 0; i < buffers.length; i++) {
+ patched[i] = maybePatch(buffers[i]);
+ }
+ return patched;
+ }
+
+ private ByteBuffer maybePatch(ByteBuffer buffer) {
+ if (!patchRequestLineToHttp11 || buffer == null) {
+ return buffer;
+ }
+ patchRequestLineToHttp11(buffer);
+ return buffer;
+ }
+
+ static void patchRequestLineToHttp11(ByteBuffer buffer) {
+ int start = buffer.position();
+ int end = buffer.limit();
+ for (int i = start; i <= end - 8; i++) {
+ if (buffer.get(i) == 'H'
+ && buffer.get(i + 1) == 'T'
+ && buffer.get(i + 2) == 'T'
+ && buffer.get(i + 3) == 'P'
+ && buffer.get(i + 4) == '/'
+ && buffer.get(i + 5) == '1'
+ && buffer.get(i + 6) == '.'
+ && buffer.get(i + 7) == '0') {
+ buffer.put(i + 7, (byte) '1');
+ return;
+ }
+ if (buffer.get(i) == '\n') {
+ return;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/AsyncCompletionSamplePipeline.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/AsyncCompletionSamplePipeline.java
index a0c7950..0c59319 100644
--- a/src/main/java/com/blazemeter/jmeter/http2/sampler/AsyncCompletionSamplePipeline.java
+++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/AsyncCompletionSamplePipeline.java
@@ -28,7 +28,8 @@ final class AsyncCompletionSamplePipeline {
private static final Logger LOG = LoggerFactory.getLogger(AsyncCompletionSamplePipeline.class);
- private AsyncCompletionSamplePipeline() {}
+ private AsyncCompletionSamplePipeline() {
+ }
/**
* Mirrors {@code JMeterThread.executeSamplePackage} for the fragment after a successful sample,
diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java
index 529c270..cbc9127 100644
--- a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java
+++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java
@@ -6,6 +6,7 @@
import com.blazemeter.jmeter.http2.core.HTTP2ClientProfileConfig;
import com.blazemeter.jmeter.http2.core.HTTP2FutureResponseListener;
import com.blazemeter.jmeter.http2.core.HTTP2JettyClient;
+import com.blazemeter.jmeter.http2.core.JmeterHttpClientExceptionMapper;
import com.blazemeter.jmeter.http2.core.ProtocolErrorException;
import com.blazemeter.jmeter.http2.util.BzmHttpPluginProperties;
import com.github.benmanes.caffeine.cache.Caffeine;
@@ -119,28 +120,7 @@ protected Map childValue(
JMeterUtils.getProperty("HTTPResponse.parsers"); //$NON-NLS-1$
static {
- String[] parsers =
- JOrphanUtils.split(RESPONSE_PARSERS, " ", true); // returns empty array for null
- for (final String parser : parsers) {
- String classname = JMeterUtils.getProperty(parser + ".className"); //$NON-NLS-1$
- if (classname == null) {
- LOG.error("Cannot find .className property for {}, ensure you set property: '{}.className'",
- parser, parser);
- continue;
- }
- String typeList = JMeterUtils.getProperty(parser + ".types"); //$NON-NLS-1$
- if (typeList != null) {
- String[] types = JOrphanUtils.split(typeList, " ", true);
- for (final String type : types) {
- registerParser(type, classname);
- }
- } else {
- LOG.warn(
- "Cannot find .types property for {}, as a consequence parser " +
- "will not be used, to make it usable, define property:'{}.types'",
- parser, parser);
- }
- }
+ loadResponseParsersFromProperties();
}
private final transient Callable clientFactory;
@@ -626,7 +606,9 @@ private HTTPSampleResult buildErrorResult(Exception e, HTTPSampleResult result)
result.sampleEnd();
}
}
- return errorResult(e, result);
+ return errorResult(
+ JmeterHttpClientExceptionMapper.forSampleResult(e, getAutoRedirects(), result.getURL()),
+ result);
}
/**
@@ -636,6 +618,47 @@ private HTTPSampleResult buildErrorResult(Exception e, HTTPSampleResult result)
* the global HTTP/1.1-only origin cache ({@link HTTP2JettyClient}) even when the parent has
* HTTP/1.1 explicitly disabled (e.g. HTTP/2-only mode).
*/
+ private HTTP2Sampler newFileEmbeddedSampler(URL url) {
+ HTTP2Sampler fileSampler = new HTTP2Sampler();
+ copyJettyProtocolSettingsToEmbeddedSampler(fileSampler);
+ String path = url.getPath();
+ boolean htmlResource = path != null
+ && (path.endsWith(".html") || path.endsWith(".htm"));
+ fileSampler.setImageParser(htmlResource);
+ fileSampler.setMethod(HTTPConstants.GET);
+ fileSampler.setProtocol(url.getProtocol());
+ fileSampler.setDomain(url.getHost());
+ fileSampler.setPort(url.getPort());
+ if (url.getQuery() == null) {
+ fileSampler.setPath(url.getPath());
+ } else {
+ fileSampler.setPath(url.getPath() + url.getQuery());
+ }
+ fileSampler.setHeaderManager(getHeaderManager());
+ fileSampler.setCookieManager(getCookieManager());
+ return fileSampler;
+ }
+
+ private static String formatFileEmbeddedLabel(URL url, int index) {
+ String path = url.getPath();
+ if (path != null && path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ return url.getProtocol() + ":" + path + "-" + index;
+ }
+
+ private static void relabelFileEmbeddedChildren(HTTPSampleResult parent) {
+ int childIndex = 0;
+ for (SampleResult child : parent.getSubResults()) {
+ if (child instanceof HTTPSampleResult httpChild && httpChild.getURL() != null
+ && "file".equalsIgnoreCase(httpChild.getURL().getProtocol())) {
+ httpChild.setSampleLabel(
+ formatFileEmbeddedLabel(httpChild.getURL(), childIndex++));
+ relabelFileEmbeddedChildren(httpChild);
+ }
+ }
+ }
+
private void copyJettyProtocolSettingsToEmbeddedSampler(HTTP2Sampler embedded) {
embedded.setProfile(getProfile());
embedded.setEnableHttp3(getEnableHttp3());
@@ -736,8 +759,49 @@ static void registerParser(String contentType, String className) {
PARSERS_FOR_CONTENT_TYPE.put(contentType, className);
}
+ /**
+ * JMeter batch plans configure HTML parsers via {@code -q jmeter-batch.properties}. The plugin
+ * class may load before those properties exist, so register parsers lazily on first use.
+ */
+ private static void ensureResponseParsersLoaded() {
+ if (!PARSERS_FOR_CONTENT_TYPE.isEmpty()) {
+ return;
+ }
+ synchronized (PARSERS_FOR_CONTENT_TYPE) {
+ if (PARSERS_FOR_CONTENT_TYPE.isEmpty()) {
+ loadResponseParsersFromProperties();
+ }
+ }
+ }
+
+ private static void loadResponseParsersFromProperties() {
+ String responseParsers = JMeterUtils.getProperty("HTTPResponse.parsers");
+ String[] parsers = JOrphanUtils.split(responseParsers, " ", true);
+ for (final String parser : parsers) {
+ String classname = JMeterUtils.getProperty(parser + ".className");
+ if (classname == null) {
+ LOG.error("Cannot find .className property for {}, ensure you set property: '{}.className'",
+ parser, parser);
+ continue;
+ }
+ String typeList = JMeterUtils.getProperty(parser + ".types");
+ if (typeList != null) {
+ String[] types = JOrphanUtils.split(typeList, " ", true);
+ for (final String type : types) {
+ registerParser(type, classname);
+ }
+ } else {
+ LOG.warn(
+ "Cannot find .types property for {}, as a consequence parser "
+ + "will not be used, to make it usable, define property:'{}.types'",
+ parser, parser);
+ }
+ }
+ }
+
private LinkExtractorParser getParser(HTTPSampleResult res)
throws LinkExtractorParseException {
+ ensureResponseParsersLoaded();
String parserClassName =
PARSERS_FOR_CONTENT_TYPE.get(res.getMediaType());
if (!StringUtils.isEmpty(parserClassName)) {
@@ -755,13 +819,12 @@ private String getUserAgent(HTTPSampleResult sampleResult) {
// see HTTPJavaImpl#getConnectionHeaders
//': ' is used by JMeter to fill-in requestHeaders, see getConnectionHeaders
final String userAgentPrefix = USER_AGENT + ": ";
- String userAgentHdr = res.substring(
- index + userAgentPrefix.length(),
- res.indexOf(
- '\n',
- // '\n' is used by JMeter to fill-in requestHeaders, see getConnectionHeaders
- index + userAgentPrefix.length() + 1));
- return userAgentHdr.trim();
+ int valueStart = index + userAgentPrefix.length();
+ int lineEnd = res.indexOf('\n', valueStart);
+ if (lineEnd < 0) {
+ lineEnd = res.length();
+ }
+ return res.substring(valueStart, lineEnd).trim();
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("No user agent extracted from requestHeaders:{}", res);
@@ -928,6 +991,7 @@ protected HTTPSampleResult downloadPageResources(final HTTPSampleResult pRes,
setSyncRequest(!isConcurrentDwn); // Change default from main request based on sub request
+ int fileEmbeddedIndex = 0;
while (urls.hasNext()) {
Object binURL = urls.next(); // See catch clause below
try {
@@ -960,6 +1024,20 @@ protected HTTPSampleResult downloadPageResources(final HTTPSampleResult pRes,
continue;
}
+ if ("file".equalsIgnoreCase(url.getProtocol())) {
+ HTTP2Sampler fileSampler = newFileEmbeddedSampler(url);
+ HTTPSampleResult binRes =
+ fileSampler.sample(url, HTTPConstants.GET, false, frameDepth + 1);
+ if (binRes != null) {
+ binRes.setSampleLabel(formatFileEmbeddedLabel(url, fileEmbeddedIndex++));
+ relabelFileEmbeddedChildren(binRes);
+ }
+ subres.addSubResult(binRes);
+ setParentSampleSuccess(subres,
+ subres.isSuccessful() && (binRes == null || binRes.isSuccessful()));
+ continue;
+ }
+
HTTP2Sampler h2s = new HTTP2Sampler();
copyJettyProtocolSettingsToEmbeddedSampler(h2s);
h2s.setMethod("GET");
diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/JmxBlazeMeterHttpMigrator.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/JmxBlazeMeterHttpMigrator.java
new file mode 100644
index 0000000..5ba09f2
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/JmxBlazeMeterHttpMigrator.java
@@ -0,0 +1,151 @@
+package com.blazemeter.jmeter.http2.sampler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
+import org.apache.jmeter.save.SaveService;
+import org.apache.jmeter.testelement.TestElement;
+import org.apache.jorphan.collections.HashTree;
+import org.apache.jorphan.collections.ListedHashTree;
+
+/**
+ * Headless migration of JMeter test plans: replaces stock HTTP Request samplers with
+ * {@link HTTP2Sampler} while preserving child elements (assertions, timers, etc.).
+ */
+public final class JmxBlazeMeterHttpMigrator {
+
+ private JmxBlazeMeterHttpMigrator() {
+ }
+
+ public static HashTree loadTree(File jmxFile) throws IOException {
+ return SaveService.loadTree(jmxFile);
+ }
+
+ public static void saveTree(HashTree tree, File jmxFile) throws IOException {
+ try (OutputStream out = java.nio.file.Files.newOutputStream(jmxFile.toPath())) {
+ SaveService.saveTree(tree, out);
+ }
+ }
+
+ /**
+ * @return number of HTTP Request samplers replaced
+ */
+ public static int migrateTree(HashTree tree) {
+ return migrateTreeWithDetails(tree).getReplacedCount();
+ }
+
+ public static MigrationResult migrateTreeWithDetails(HashTree tree) {
+ MigrationResult result = new MigrationResult();
+ migrateInPlace(tree, result);
+ return result;
+ }
+
+ public static HashTree migrateCopy(HashTree source) {
+ return migrateCopy(source, new MigrationResult());
+ }
+
+ public static HashTree migrateCopy(HashTree source, MigrationResult result) {
+ ListedHashTree copy = new ListedHashTree();
+ for (Object key : source.list()) {
+ Object newKey = maybeReplaceSampler(key, result);
+ HashTree sub = source.getTree(key);
+ if (sub != null && !sub.isEmpty()) {
+ copy.add(newKey, migrateCopy(sub, result));
+ } else {
+ copy.add(newKey);
+ }
+ }
+ return copy;
+ }
+
+ public static File migrateFile(File sourceJmx, File targetJmx) throws IOException {
+ HashTree tree = loadTree(sourceJmx);
+ migrateTree(tree);
+ saveTree(tree, targetJmx);
+ return targetJmx;
+ }
+
+ public static int countMigratableSamplers(HashTree tree) {
+ int count = 0;
+ for (Object key : tree.list()) {
+ if (key instanceof TestElement
+ && HttpSamplerToBlazeMeterHttpMigrator.isMigratableApacheHttpSampler(
+ (TestElement) key)) {
+ count++;
+ }
+ HashTree sub = tree.getTree(key);
+ if (sub != null && !sub.isEmpty()) {
+ count += countMigratableSamplers(sub);
+ }
+ }
+ return count;
+ }
+
+ public static int countHttp2Samplers(HashTree tree) {
+ int count = 0;
+ for (Object key : tree.list()) {
+ if (key instanceof HTTP2Sampler) {
+ count++;
+ }
+ HashTree sub = tree.getTree(key);
+ if (sub != null && !sub.isEmpty()) {
+ count += countHttp2Samplers(sub);
+ }
+ }
+ return count;
+ }
+
+ private static void migrateInPlace(HashTree tree, MigrationResult result) {
+ List