From f8b045bd2d95ec28c70c4d64c85400b28d8baef5 Mon Sep 17 00:00:00 2001 From: franz1981 Date: Thu, 16 Sep 2021 13:49:40 +0200 Subject: [PATCH] ARTEMIS-3522 Implement performance tool for RUL benchmarks Co-authored-by: gtully --- artemis-cli/pom.xml | 4 + .../apache/activemq/artemis/cli/Artemis.java | 7 + .../commands/messages/ConnectionAbstract.java | 29 +- .../cli/commands/messages/DestAbstract.java | 6 +- .../perf/AsyncJms2ProducerFacade.java | 274 ++++++++++++ .../messages/perf/BenchmarkService.java | 30 ++ .../messages/perf/LiveStatistics.java | 408 ++++++++++++++++++ .../perf/MessageListenerBenchmark.java | 305 +++++++++++++ .../perf/MessageListenerBenchmarkBuilder.java | 96 +++++ .../commands/messages/perf/MicrosClock.java | 65 +++ .../messages/perf/MicrosTimeProvider.java | 23 + .../messages/perf/PaddingDecimalFormat.java | 74 ++++ .../messages/perf/PerfClientCommand.java | 202 +++++++++ .../commands/messages/perf/PerfCommand.java | 154 +++++++ .../messages/perf/PerfConsumerCommand.java | 111 +++++ .../messages/perf/PerfProducerCommand.java | 157 +++++++ .../messages/perf/ProducerBenchmark.java | 341 +++++++++++++++ .../perf/ProducerBenchmarkBuilder.java | 128 ++++++ .../messages/perf/ProducerLoadGenerator.java | 42 ++ .../perf/ProducerMaxLoadGenerator.java | 52 +++ .../perf/ProducerTargetRateLoadGenerator.java | 69 +++ .../commands/messages/perf/RateSampler.java | 59 +++ .../perf/RecordingMessageListener.java | 101 +++++ .../perf/SkeletalProducerLoadGenerator.java | 241 +++++++++++ .../src/main/resources/bin/artemis | 4 +- .../src/main/resources/bin/artemis.cmd | 2 +- .../src/main/resources/licenses/bin/LICENSE | 6 + .../bin/licenses/LICENSE-hdrhistogram.txt | 41 ++ .../src/main/resources/features.xml | 1 + docs/user-manual/en/SUMMARY.md | 1 + docs/user-manual/en/images/30K.png | Bin 0 -> 48908 bytes docs/user-manual/en/images/90K.png | Bin 0 -> 38510 bytes docs/user-manual/en/images/hot_test.png | Bin 0 -> 42725 bytes docs/user-manual/en/images/test.png | Bin 0 -> 56536 bytes docs/user-manual/en/perf-tools.md | 379 ++++++++++++++++ pom.xml | 14 +- 36 files changed, 3415 insertions(+), 11 deletions(-) create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/AsyncJms2ProducerFacade.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/BenchmarkService.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/LiveStatistics.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmark.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmarkBuilder.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosClock.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosTimeProvider.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PaddingDecimalFormat.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfClientCommand.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfCommand.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfConsumerCommand.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfProducerCommand.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmark.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmarkBuilder.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerLoadGenerator.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerMaxLoadGenerator.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerTargetRateLoadGenerator.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RateSampler.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RecordingMessageListener.java create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/SkeletalProducerLoadGenerator.java create mode 100644 artemis-distribution/src/main/resources/licenses/bin/licenses/LICENSE-hdrhistogram.txt create mode 100644 docs/user-manual/en/images/30K.png create mode 100644 docs/user-manual/en/images/90K.png create mode 100644 docs/user-manual/en/images/hot_test.png create mode 100644 docs/user-manual/en/images/test.png create mode 100644 docs/user-manual/en/perf-tools.md diff --git a/artemis-cli/pom.xml b/artemis-cli/pom.xml index e92c2d4004..a5ca840215 100644 --- a/artemis-cli/pom.xml +++ b/artemis-cli/pom.xml @@ -112,6 +112,10 @@ org.apache.commons commons-lang3 + + org.hdrhistogram + HdrHistogram + org.apache.commons commons-configuration2 diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java index 398bbd3e4b..91cbeead5d 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/Artemis.java @@ -37,6 +37,9 @@ import org.apache.activemq.artemis.cli.commands.check.HelpCheck; import org.apache.activemq.artemis.cli.commands.check.NodeCheck; import org.apache.activemq.artemis.cli.commands.check.QueueCheck; import org.apache.activemq.artemis.cli.commands.messages.Transfer; +import org.apache.activemq.artemis.cli.commands.messages.perf.PerfClientCommand; +import org.apache.activemq.artemis.cli.commands.messages.perf.PerfConsumerCommand; +import org.apache.activemq.artemis.cli.commands.messages.perf.PerfProducerCommand; import org.apache.activemq.artemis.cli.commands.queue.StatQueue; import org.apache.activemq.artemis.cli.commands.Run; import org.apache.activemq.artemis.cli.commands.Stop; @@ -163,6 +166,10 @@ public class Artemis { withCommand(HelpAction.class).withCommand(Producer.class).withCommand(Transfer.class).withCommand(Consumer.class). withCommand(Browse.class).withCommand(Mask.class).withCommand(PrintVersion.class).withDefaultCommand(HelpAction.class); + builder.withGroup("perf").withDescription("Perf tools group (example ./artemis perf client)") + .withDefaultCommand(PerfClientCommand.class) + .withCommands(PerfProducerCommand.class, PerfConsumerCommand.class, PerfClientCommand.class); + builder.withGroup("check").withDescription("Check tools group (node|queue) (example ./artemis check node)"). withDefaultCommand(HelpCheck.class).withCommands(NodeCheck.class, QueueCheck.class); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/ConnectionAbstract.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/ConnectionAbstract.java index 8dbe97f8d1..d463566657 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/ConnectionAbstract.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/ConnectionAbstract.java @@ -43,10 +43,10 @@ public class ConnectionAbstract extends InputAbstract { protected String password; @Option(name = "--clientID", description = "ClientID to be associated with connection") - String clientID; + protected String clientID; @Option(name = "--protocol", description = "Protocol used. Valid values are amqp or core. Default=core.") - String protocol = "core"; + protected String protocol = "core"; public String getBrokerURL() { return brokerURL; @@ -126,16 +126,27 @@ public class ConnectionAbstract extends InputAbstract { } protected ConnectionFactory createConnectionFactory() throws Exception { + return createConnectionFactory(brokerURL, user, password, clientID, protocol); + } + + protected ConnectionFactory createConnectionFactory(String brokerURL, + String user, + String password, + String clientID, + String protocol) throws Exception { if (protocol.equals("core")) { - return createCoreConnectionFactory(); + return createCoreConnectionFactory(brokerURL, user, password, clientID); } else if (protocol.equals("amqp")) { - return createAMQPConnectionFactory(); + return createAMQPConnectionFactory(brokerURL, user, password, clientID); } else { throw new IllegalStateException("protocol " + protocol + " not supported"); } } - private ConnectionFactory createAMQPConnectionFactory() { + private ConnectionFactory createAMQPConnectionFactory(String brokerURL, + String user, + String password, + String clientID) { if (brokerURL.startsWith("tcp://")) { // replacing tcp:// by amqp:// brokerURL = "amqp" + brokerURL.substring(3); @@ -172,8 +183,14 @@ public class ConnectionAbstract extends InputAbstract { } protected ActiveMQConnectionFactory createCoreConnectionFactory() { - ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerURL, user, password); + return createCoreConnectionFactory(brokerURL, user, password, clientID); + } + protected ActiveMQConnectionFactory createCoreConnectionFactory(String brokerURL, + String user, + String password, + String clientID) { + ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerURL, user, password); if (clientID != null) { System.out.println("Consumer:: clientID = " + clientID); cf.setClientID(clientID); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/DestAbstract.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/DestAbstract.java index 360bac6dd2..d3dc25ad9b 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/DestAbstract.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/DestAbstract.java @@ -65,13 +65,17 @@ public class DestAbstract extends ConnectionAbstract { } protected Destination getDestination(Session session) throws JMSException { + return getDestination(session, destination); + } + + public static Destination getDestination(Session session, String destination) throws JMSException { if (destination.startsWith(ActiveMQDestination.TOPIC_QUALIFIED_PREFIX)) { return session.createTopic(stripPrefix(destination)); } return session.createQueue(stripPrefix(destination)); } - private String stripPrefix(String destination) { + public static String stripPrefix(String destination) { int index = destination.indexOf("://"); if (index != -1) { return destination.substring(index + 3); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/AsyncJms2ProducerFacade.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/AsyncJms2ProducerFacade.java new file mode 100644 index 0000000000..49f5d4c29f --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/AsyncJms2ProducerFacade.java @@ -0,0 +1,274 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.BytesMessage; +import javax.jms.CompletionListener; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import javax.jms.Session; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import static java.util.Objects.requireNonNull; + +public final class AsyncJms2ProducerFacade { + + private final long id; + protected final Session session; + private final MessageProducer producer; + + /* + * maxPending limits the number of in-flight sent messages + * in a way that if the limit is reached and a single completion arrive, + * a subsequent send attempt will succeed. + */ + private long pending; + private final long maxPending; + + /* + * Tracking sent messages in transaction requires using 2 separate counters + * ie pendingMsgInTransaction, completedMsgInTransaction + * because, using just one won't allow tracking completions of previously sent messages in order to commit + * the transaction while there are no more in-flight ones. + */ + private final long transactionCapacity; + private long pendingMsgInTransaction; + private long completedMsgInTransaction; + + private final List availableObservers; + private final List closedObservers; + + private static final AtomicLongFieldUpdater MESSAGE_SENT_UPDATER = AtomicLongFieldUpdater.newUpdater(AsyncJms2ProducerFacade.class, "messageSent"); + private static final AtomicLongFieldUpdater MESSAGE_COMPLETED_UPDATER = AtomicLongFieldUpdater.newUpdater(AsyncJms2ProducerFacade.class, "messageCompleted"); + private static final AtomicLongFieldUpdater NOT_AVAILABLE_UPDATER = AtomicLongFieldUpdater.newUpdater(AsyncJms2ProducerFacade.class, "notAvailable"); + + private volatile long messageSent; + private volatile long messageCompleted; + private volatile long notAvailable; + + private boolean closing; + private boolean closed; + private final Destination destination; + + public AsyncJms2ProducerFacade(final long id, + final Session session, + final MessageProducer producer, + final Destination destination, + final long maxPending, + final long transactionCapacity) { + this.id = id; + this.session = requireNonNull(session); + this.producer = requireNonNull(producer); + this.destination = destination; + this.pending = 0; + this.maxPending = transactionCapacity > 0 && maxPending > 0 ? Math.max(maxPending, transactionCapacity) : maxPending; + this.availableObservers = new ArrayList<>(1); + this.closedObservers = new ArrayList<>(1); + this.messageSent = 0; + this.messageCompleted = 0; + this.notAvailable = 0; + try { + if (transactionCapacity < 0) { + throw new IllegalStateException("transactionCapacity must be >= 0"); + } + if (transactionCapacity > 0) { + if (!session.getTransacted()) { + throw new IllegalStateException("session must be transacted with transactionCapacity != 0"); + } + } else { + if (session.getTransacted()) { + throw new IllegalStateException("session cannot be transacted with transactionCapacity = 0"); + } + } + } catch (final JMSException ex) { + throw new IllegalStateException(ex); + } + this.transactionCapacity = transactionCapacity; + this.pendingMsgInTransaction = 0; + this.completedMsgInTransaction = 0; + this.closing = false; + this.closed = false; + } + + public long getId() { + return id; + } + + public Destination getDestination() { + return destination; + } + + BytesMessage createBytesMessage() throws JMSException { + return session.createBytesMessage(); + } + + private void addedPendingSend() { + if (transactionCapacity > 0 && pendingMsgInTransaction == transactionCapacity) { + throw new IllegalStateException("reached max in-flight transacted sent messages"); + } + if (maxPending > 0 && pending == maxPending) { + throw new IllegalStateException("reached max in-flight sent messages"); + } + pending++; + pendingMsgInTransaction++; + } + + /** + * if {@code true}, a subsequent {@link #trySend} would return {@link SendAttemptResult#Success}.
+ * Otherwise, a subsequent {@link #trySend} would return {@link SendAttemptResult#NotAvailable}. + */ + private boolean isAvailable() { + if (maxPending > 0 && pending == maxPending) { + return false; + } + return transactionCapacity == 0 || pendingMsgInTransaction != transactionCapacity; + } + + public enum SendAttemptResult { + Closing, Closed, NotAvailable, Success + } + + public SendAttemptResult trySend(final Message message, + final CompletionListener completionListener, + final Runnable availableObserver) throws JMSException { + if (closing) { + return SendAttemptResult.Closing; + } + if (closed) { + return SendAttemptResult.Closed; + } + if (!isAvailable()) { + availableObservers.add(availableObserver); + orderedIncrementNotAvailable(); + return SendAttemptResult.NotAvailable; + } + producer.send(message, completionListener); + orderedIncrementSent(); + addedPendingSend(); + return SendAttemptResult.Success; + } + + public void onSendErrored() { + if (closed) { + return; + } + availableObservers.clear(); + closedObservers.forEach(Runnable::run); + closedObservers.clear(); + closed = true; + } + + public JMSException onSendCompleted() { + if (closed) { + return null; + } + JMSException completionError = null; + orderedIncrementCompleted(); + if (transactionCapacity > 0 && completedMsgInTransaction == transactionCapacity) { + throw new IllegalStateException("cannot complete more send"); + } + if (pending == 0) { + throw new IllegalStateException("cannot complete more send"); + } + pending--; + completedMsgInTransaction++; + if (transactionCapacity > 0) { + if (completedMsgInTransaction == transactionCapacity || (closing && pending == 0)) { + completedMsgInTransaction = 0; + pendingMsgInTransaction = 0; + try { + session.commit(); + } catch (final JMSException fatal) { + completionError = fatal; + closing = true; + } + if (closing) { + closing = false; + closed = true; + closedObservers.forEach(Runnable::run); + closedObservers.clear(); + } else if (isAvailable()) { + availableObservers.forEach(Runnable::run); + availableObservers.clear(); + } + } + } else { + if (closing && pending == 0) { + closing = false; + closed = true; + closedObservers.forEach(Runnable::run); + closedObservers.clear(); + } else if (isAvailable()) { + availableObservers.forEach(Runnable::run); + availableObservers.clear(); + } + } + return completionError; + } + + public long getMessageSent() { + return messageSent; + } + + private void orderedIncrementSent() { + MESSAGE_SENT_UPDATER.lazySet(this, messageSent + 1); + } + + public long getMessageCompleted() { + return messageCompleted; + } + + private void orderedIncrementCompleted() { + MESSAGE_COMPLETED_UPDATER.lazySet(this, messageCompleted + 1); + } + + public long getNotAvailable() { + return notAvailable; + } + + private void orderedIncrementNotAvailable() { + NOT_AVAILABLE_UPDATER.lazySet(this, notAvailable + 1); + } + + public void requestClose() { + requestClose(() -> { + }); + } + + public void requestClose(final Runnable onClosed) { + if (closed) { + onClosed.run(); + return; + } + if (closing) { + closedObservers.add(onClosed); + return; + } + availableObservers.clear(); + if (pending > 0) { + closing = true; + closedObservers.add(onClosed); + } else { + closed = true; + onClosed.run(); + } + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/BenchmarkService.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/BenchmarkService.java new file mode 100644 index 0000000000..f91363690a --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/BenchmarkService.java @@ -0,0 +1,30 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +public interface BenchmarkService extends AutoCloseable { + + BenchmarkService start(); + + boolean anyError(); + + boolean isRunning(); + + @Override + void close(); + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/LiveStatistics.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/LiveStatistics.java new file mode 100644 index 0000000000..da0b3765ef --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/LiveStatistics.java @@ -0,0 +1,408 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import java.io.FileWriter; +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.concurrent.TimeUnit; + +import org.HdrHistogram.Histogram; +import org.HdrHistogram.HistogramLogWriter; +import org.apache.activemq.artemis.json.JsonObjectBuilder; +import org.apache.activemq.artemis.utils.JsonLoader; + +public final class LiveStatistics { + + public enum ReportInterval { + ns(1), + us(TimeUnit.MICROSECONDS.toNanos(1)), + ms(TimeUnit.MILLISECONDS.toNanos(1)), + sec(TimeUnit.SECONDS.toNanos(1)); + + public final long nanoseconds; + + ReportInterval(long value) { + this.nanoseconds = value; + } + } + + private final ProducerLoadGenerator[] producers; + private final Histogram[] waitLatencies; + private final Histogram intervalWaitLatencies; + private final Histogram[] sentLatencies; + private final Histogram intervalSentLatencies; + private final RecordingMessageListener[] listeners; + private final Histogram[] endToEndLatencies; + private final Histogram intervalEndToEndLatencies; + private final RateSampler sentMsg; + private final RateSampler blockedMsg; + private final RateSampler completedMsg; + private final RateSampler receivedMsg; + private final Histogram accumulatedWaitLatencies; + private final Histogram accumulatedSentLatencies; + private final Histogram accumulatedEndToEndLatencies; + private final DecimalFormat jsonLatencyFormat; + private final PaddingDecimalFormat latencyFormat; + private final PaddingDecimalFormat rateFormat; + private final PaddingDecimalFormat countFormat; + private long sampleTime; + private final FileWriter jsonWriter; + private long jsonSamples; + private final HistogramLogWriter latenciesLogWriter; + + public LiveStatistics(final String jsonOutput, final String hdrOutput, final ProducerLoadGenerator[] producers, final RecordingMessageListener[] listeners) throws IOException { + if (producers != null && producers.length > 0) { + this.producers = producers; + sentLatencies = new Histogram[producers.length]; + intervalSentLatencies = new Histogram(2); + accumulatedSentLatencies = new Histogram(2); + if (producers[0] instanceof ProducerTargetRateLoadGenerator) { + waitLatencies = new Histogram[producers.length]; + intervalWaitLatencies = new Histogram(2); + accumulatedWaitLatencies = new Histogram(2); + } else { + waitLatencies = null; + intervalWaitLatencies = null; + accumulatedWaitLatencies = null; + } + sentMsg = RateSampler.of(() -> { + long sum = 0; + for (ProducerLoadGenerator producer : producers) { + sum += producer.getProducer().getMessageSent(); + } + return sum; + }); + blockedMsg = RateSampler.of(() -> { + long sum = 0; + for (ProducerLoadGenerator producer : producers) { + sum += producer.getProducer().getNotAvailable(); + } + return sum; + }); + completedMsg = RateSampler.of(() -> { + long sum = 0; + for (ProducerLoadGenerator producer : producers) { + sum += producer.getProducer().getMessageCompleted(); + } + return sum; + }); + } else { + this.producers = null; + sentLatencies = null; + intervalSentLatencies = null; + accumulatedSentLatencies = null; + waitLatencies = null; + intervalWaitLatencies = null; + accumulatedWaitLatencies = null; + sentMsg = null; + blockedMsg = null; + completedMsg = null; + } + if (listeners != null) { + this.listeners = listeners; + endToEndLatencies = new Histogram[listeners.length]; + intervalEndToEndLatencies = new Histogram(2); + accumulatedEndToEndLatencies = new Histogram(2); + receivedMsg = RateSampler.of(() -> { + long sum = 0; + for (RecordingMessageListener listener : listeners) { + sum += listener.getReceivedMessages(); + } + return sum; + }); + } else { + this.listeners = null; + endToEndLatencies = null; + intervalEndToEndLatencies = null; + accumulatedEndToEndLatencies = null; + receivedMsg = null; + } + this.sampleTime = System.currentTimeMillis(); + this.jsonLatencyFormat = new DecimalFormat("0.00"); + this.latencyFormat = new PaddingDecimalFormat("0.00", 9); + this.rateFormat = new PaddingDecimalFormat("0", 8); + this.countFormat = new PaddingDecimalFormat("0", 12); + this.jsonSamples = 0; + if (jsonOutput != null) { + this.jsonWriter = new FileWriter(jsonOutput); + this.jsonWriter.write("[\n"); + } else { + this.jsonWriter = null; + } + if (hdrOutput != null) { + this.latenciesLogWriter = new HistogramLogWriter(hdrOutput); + this.latenciesLogWriter.outputLogFormatVersion(); + this.latenciesLogWriter.outputLegend(); + } else { + this.latenciesLogWriter = null; + } + } + + private boolean anyFatalError() { + if (producers != null) { + for (ProducerLoadGenerator producer : producers) { + if (producer.getFatalException() != null) { + return true; + } + } + } + if (listeners != null) { + for (RecordingMessageListener listener : listeners) { + if (listener.anyFatalException()) { + return true; + } + } + } + return false; + } + + public void sampleMetrics(final boolean warmup) { + final long lastSampleTime = this.sampleTime; + sampleTime = System.currentTimeMillis(); + if (receivedMsg != null) { + receivedMsg.run(); + } + if (completedMsg != null) { + completedMsg.run(); + } + if (blockedMsg != null) { + blockedMsg.run(); + } + if (sentMsg != null) { + sentMsg.run(); + } + if (endToEndLatencies != null) { + for (int i = 0, size = listeners.length; i < size; i++) { + endToEndLatencies[i] = listeners[i].getReceiveLatencyRecorder().getIntervalHistogram(endToEndLatencies[i]); + } + } + if (sentLatencies != null) { + for (int i = 0, size = producers.length; i < size; i++) { + sentLatencies[i] = producers[i].getSendCompletedLatencies().getIntervalHistogram(sentLatencies[i]); + } + } + if (waitLatencies != null) { + for (int i = 0, size = producers.length; i < size; i++) { + waitLatencies[i] = producers[i].getWaitLatencies().getIntervalHistogram(waitLatencies[i]); + } + } + aggregateLatencies(warmup, lastSampleTime, sampleTime, intervalEndToEndLatencies, endToEndLatencies, accumulatedEndToEndLatencies); + aggregateLatencies(warmup, lastSampleTime, sampleTime, intervalSentLatencies, sentLatencies, accumulatedSentLatencies); + aggregateLatencies(warmup, lastSampleTime, sampleTime, intervalWaitLatencies, waitLatencies, accumulatedWaitLatencies); + } + + private static void aggregateLatencies(final boolean warmup, + final long lastSampleTime, + final long sampleTime, + final Histogram intervalSentLatencies, + final Histogram[] sentLatencies, + final Histogram accumulatedSentLatencies) { + if (intervalSentLatencies != null) { + intervalSentLatencies.reset(); + intervalSentLatencies.setStartTimeStamp(lastSampleTime); + intervalSentLatencies.setEndTimeStamp(sampleTime); + for (Histogram histogram : sentLatencies) { + intervalSentLatencies.add(histogram); + } + if (!warmup) { + accumulatedSentLatencies.add(intervalSentLatencies); + } + } + } + + public void outAtInterval(final boolean warmup, final StringBuilder out, final ReportInterval interval, final boolean includeLatencies) throws IOException { + // space after "true" is to ensure length to be the same as "false": don't remove it! + out.append("\n--- warmup ").append(warmup ? "true " : "false"); + appendRateOf(out, "\n--- sent: ", sentMsg, rateFormat, interval, "msg"); + appendRateOf(out, "\n--- blocked: ", blockedMsg, rateFormat, interval, "msg"); + appendRateOf(out, "\n--- completed:", completedMsg, rateFormat, interval, "msg"); + appendRateOf(out, "\n--- received: ", receivedMsg, rateFormat, interval, "msg"); + if (includeLatencies) { + outPercentiles(out, "\n--- send delay time:", intervalWaitLatencies, latencyFormat); + outPercentiles(out, "\n--- send ack time: ", intervalSentLatencies, latencyFormat); + outPercentiles(out, "\n--- transfer time: ", intervalEndToEndLatencies, latencyFormat); + } + appendJsonIntervalSampleOnFile(warmup, interval); + appendTaggedHdrHistograms(warmup); + } + + private void appendJsonIntervalSampleOnFile(final boolean warmup, final ReportInterval interval) throws IOException { + if (jsonWriter == null) { + return; + } + final JsonObjectBuilder jsonBuilder = JsonLoader.createObjectBuilder(); + jsonBuilder.add("sampleType", "interval"); + jsonBuilder.add("warmup", warmup); + jsonBuilder.add("time", sampleTime); + addRate(jsonBuilder,"sent", sentMsg, interval); + addRate(jsonBuilder, "delayed", blockedMsg, interval); + addRate(jsonBuilder, "completed", completedMsg, interval); + addRate(jsonBuilder, "received", receivedMsg, interval); + addPercentiles(jsonBuilder, "delaySendTime", intervalWaitLatencies); + addPercentiles(jsonBuilder, "sendTime", intervalSentLatencies); + addPercentiles(jsonBuilder, "transferTime", intervalEndToEndLatencies); + final String jsonSample = jsonBuilder.build().toString(); + if (jsonSamples > 0) { + jsonWriter.write(",\n"); + } + jsonSamples++; + jsonWriter.write(jsonSample); + jsonWriter.flush(); + } + + private void appendJsonSummarySampleOnFile(final boolean failedBenchmark) throws IOException { + if (jsonWriter == null) { + return; + } + final JsonObjectBuilder jsonBuilder = JsonLoader.createObjectBuilder(); + jsonBuilder.add("sampleType", "summary"); + jsonBuilder.add("time", sampleTime); + jsonBuilder.add("result", failedBenchmark ? "fail" : "success"); + if (sentMsg != null) { + jsonBuilder.add("totalSent", sentMsg.getLastSample()); + } + if (blockedMsg != null) { + jsonBuilder.add("totalBlocked", blockedMsg.getLastSample()); + } + if (completedMsg != null) { + jsonBuilder.add("totalCompleted", completedMsg.getLastSample()); + } + if (receivedMsg != null) { + jsonBuilder.add("totalReceived", receivedMsg.getLastSample()); + } + addPercentiles(jsonBuilder, "totalDelaySendTime", accumulatedWaitLatencies); + addPercentiles(jsonBuilder, "totalSendTime", accumulatedSentLatencies); + addPercentiles(jsonBuilder, "totalTransferTime", accumulatedEndToEndLatencies); + final String jsonSample = jsonBuilder.build().toString(); + if (jsonSamples > 0) { + jsonWriter.write(",\n"); + } + jsonSamples++; + jsonWriter.write(jsonSample); + jsonWriter.flush(); + } + + private void appendTaggedHdrHistograms(final boolean warmup) { + if (latenciesLogWriter == null) { + return; + } + if (intervalWaitLatencies != null) { + intervalWaitLatencies.setTag(warmup ? "warmup delay send" : "delay send"); + latenciesLogWriter.outputIntervalHistogram(intervalWaitLatencies); + } + if (intervalSentLatencies != null) { + intervalSentLatencies.setTag(warmup ? "warmup send" : "send"); + latenciesLogWriter.outputIntervalHistogram(intervalSentLatencies); + } + if (intervalEndToEndLatencies != null) { + intervalEndToEndLatencies.setTag(warmup ? "warmup transfer" : "transfer"); + latenciesLogWriter.outputIntervalHistogram(intervalEndToEndLatencies); + } + } + + private static JsonObjectBuilder addRate(final JsonObjectBuilder obj, + final String metric, + final RateSampler rate, + final ReportInterval interval) { + if (rate == null) { + return obj; + } + return obj.add(metric, rate.reportRate(interval.nanoseconds)); + } + + private JsonObjectBuilder addPercentiles(final JsonObjectBuilder obj, + final String metric, + final Histogram distribution) { + if (distribution == null) { + return obj; + } + return obj + .add(metric, JsonLoader.createObjectBuilder() + .add("mean", jsonLatencyFormat.format(distribution.getMean())) + .add("50", distribution.getValueAtPercentile(50.0d)) + .add("90", distribution.getValueAtPercentile(90.0d)) + .add("99", distribution.getValueAtPercentile(99.0d)) + .add("99.9", distribution.getValueAtPercentile(99.9d)) + .add("99.99", distribution.getValueAtPercentile(99.99d)) + .add("max", distribution.getMaxValue()) + .add("count", distribution.getTotalCount())); + } + + public void outSummary(final StringBuilder out) throws IOException { + out.append("\n--- SUMMARY"); + final boolean failedBenchmark = anyFatalError(); + out.append("\n--- result: ").append(failedBenchmark ? " fail" : " success"); + if (sentMsg != null) { + out.append("\n--- total sent: ").append(countFormat.format(sentMsg.getLastSample())); + } + if (blockedMsg != null) { + out.append("\n--- total blocked: ").append(countFormat.format(blockedMsg.getLastSample())); + } + if (completedMsg != null) { + out.append("\n--- total completed:").append(countFormat.format(completedMsg.getLastSample())); + } + if (receivedMsg != null) { + out.append("\n--- total received: ").append(countFormat.format(receivedMsg.getLastSample())); + } + outPercentiles(out, "\n--- aggregated delay send time:", accumulatedWaitLatencies, latencyFormat); + outPercentiles(out, "\n--- aggregated send time: ", accumulatedSentLatencies, latencyFormat); + outPercentiles(out, "\n--- aggregated transfer time: ", accumulatedEndToEndLatencies, latencyFormat); + appendJsonSummarySampleOnFile(failedBenchmark); + } + + public void close() throws IOException { + if (jsonWriter != null) { + jsonWriter.write("\n]"); + jsonWriter.close(); + } + if (latenciesLogWriter != null) { + latenciesLogWriter.close(); + } + } + + private static void outPercentiles(final StringBuilder out, + final String metric, + final Histogram histogram, + final DecimalFormat latencyFormat) { + if (histogram == null) { + return; + } + out.append(' ').append(metric); + out.append(' ').append("mean: ").append(latencyFormat.format(histogram.getMean())).append(" us"); + out.append(" - ").append("50.00%: ").append(latencyFormat.format(histogram.getValueAtPercentile(50.0d))).append(" us"); + out.append(" - ").append("90.00%: ").append(latencyFormat.format(histogram.getValueAtPercentile(90.0d))).append(" us"); + out.append(" - ").append("99.00%: ").append(latencyFormat.format(histogram.getValueAtPercentile(99.0d))).append(" us"); + out.append(" - ").append("99.90%: ").append(latencyFormat.format(histogram.getValueAtPercentile(99.9d))).append(" us"); + out.append(" - ").append("99.99%: ").append(latencyFormat.format(histogram.getValueAtPercentile(99.99d))).append(" us"); + out.append(" - ").append("max: ").append(latencyFormat.format(histogram.getMaxValue())).append(" us"); + } + + private static StringBuilder appendRateOf(final StringBuilder out, + final String metric, + final RateSampler sampler, + final DecimalFormat rateFormat, + final ReportInterval outInterval, + final String unit) { + if (sampler == null) { + return out; + } + return out.append(' ').append(metric) + .append(' ').append(rateFormat.format(sampler.reportRate(outInterval.nanoseconds))) + .append(' ').append(unit).append('/').append(outInterval.name()); + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmark.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmark.java new file mode 100644 index 0000000000..4743a7bb0d --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmark.java @@ -0,0 +1,305 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.Session; +import javax.jms.Topic; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +import org.HdrHistogram.SingleWriterRecorder; + +public final class MessageListenerBenchmark implements BenchmarkService { + + private final ConnectionFactory factory; + private final MicrosTimeProvider timeProvider; + private final int consumers; + private final boolean canDelaySetMessageCount; + private final int connections; + private final String clientID; + private final Destination[] destinations; + private final int sharedSubscription; + private final boolean durableSubscription; + private final long messageCount; + private final boolean transaction; + private Set jmsConnections; + private boolean started; + private boolean closed; + private MessageCountLimiter msgCountLimiter; + private List listeners; + private AtomicBoolean fatalException; + private List silentUnsubscribe; + + public static final class MessageCountLimiter { + + private volatile long messageLimit = Long.MAX_VALUE; + private final LongAdder totalMessagesReceived; + + MessageCountLimiter() { + totalMessagesReceived = new LongAdder(); + } + + public MessageCountLimiter setMessageLimit(final long messageLimit) { + this.messageLimit = messageLimit; + return this; + } + + public boolean isLimitReached() { + return totalMessagesReceived.sum() >= messageLimit; + } + + public void onMessageReceived() { + totalMessagesReceived.increment(); + } + } + + public MessageListenerBenchmark(final ConnectionFactory factory, + final MicrosTimeProvider timeProvider, + final int consumers, + final long messageCount, + final int connections, + final String clientID, + final Destination[] destinations, + final boolean transaction, + final int sharedSubscription, + final boolean durableSubscription, + final boolean canDelayMessageCount) { + this.factory = factory; + this.timeProvider = timeProvider; + this.consumers = consumers; + this.messageCount = messageCount; + this.connections = connections; + this.clientID = clientID; + this.destinations = destinations; + this.transaction = transaction; + this.sharedSubscription = sharedSubscription; + this.durableSubscription = durableSubscription; + this.started = false; + this.closed = false; + this.jmsConnections = new HashSet<>(connections); + this.canDelaySetMessageCount = canDelayMessageCount; + this.listeners = null; + this.fatalException = null; + this.silentUnsubscribe = null; + } + + public synchronized RecordingMessageListener[] getListeners() { + return listeners == null ? null : listeners.toArray(new RecordingMessageListener[listeners.size()]); + } + + @Override + public synchronized boolean anyError() { + if (fatalException == null) { + return false; + } + return fatalException.get(); + } + + @Override + public synchronized boolean isRunning() { + if (!started || closed) { + return false; + } + if (fatalException.get()) { + return false; + } + if (msgCountLimiter == null) { + return true; + } + return !msgCountLimiter.isLimitReached(); + } + + public synchronized void setMessageCount(final long messageCount) { + if (!started || closed) { + return; + } + msgCountLimiter.setMessageLimit(messageCount); + } + + @Override + public synchronized MessageListenerBenchmark start() { + if (started) { + return this; + } + started = true; + closed = false; + final AtomicLong consumerId = new AtomicLong(1); + // setup connection failure listeners + final AtomicBoolean signalBrokenConnection = new AtomicBoolean(false); + fatalException = signalBrokenConnection; + // create connections upfront and register failure listener + final Connection[] jmsConnections = new Connection[connections]; + for (int i = 0; i < connections; i++) { + final Connection connection; + try { + connection = factory.createConnection(); + if (clientID != null) { + if (connections > 1) { + connection.setClientID(clientID + i); + } else { + connection.setClientID(clientID); + } + } + connection.setExceptionListener(ignore -> { + signalBrokenConnection.set(true); + }); + jmsConnections[i] = connection; + } catch (JMSException e) { + throw new RuntimeException(e); + } + } + this.jmsConnections.addAll(Arrays.asList(jmsConnections)); + // start connections + this.jmsConnections.forEach(connection -> { + try { + connection.start(); + } catch (JMSException e) { + throw new RuntimeException(e); + } + }); + int connectionSequence = 0; + final int totalListeners = consumers * destinations.length * Math.max(sharedSubscription, 1); + this.listeners = new ArrayList<>(totalListeners); + if (messageCount > 0) { + msgCountLimiter = new MessageCountLimiter().setMessageLimit(messageCount); + } else if (canDelaySetMessageCount) { + msgCountLimiter = new MessageCountLimiter(); + } + // create consumers per destination + if (durableSubscription) { + silentUnsubscribe = new ArrayList<>(); + } + for (int i = 0; i < destinations.length; i++) { + final Destination destination = destinations[i]; + if (sharedSubscription == 0) { + final Queue destinationListeners = new ArrayDeque<>(consumers); + createListeners(destinationListeners, consumerId, destination, consumers); + listeners.addAll(destinationListeners); + try { + for (int consumerIndex = 0; consumerIndex < consumers; consumerIndex++) { + final Connection connection = jmsConnections[connectionSequence % connections]; + connectionSequence++; + final Session session = connection.createSession(transaction ? Session.SESSION_TRANSACTED : Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer; + if (durableSubscription) { + final Topic topic = (Topic) destination; + consumer = session.createDurableConsumer((Topic) destination, topic.getTopicName() + consumerIndex); + } else { + consumer = session.createConsumer(destination); + } + consumer.setMessageListener(destinationListeners.remove()); + } + } catch (JMSException e) { + throw new RuntimeException(e); + } + } else { + final int listenersPerDestination = sharedSubscription * consumers; + final Queue destinationListeners = new ArrayDeque<>(listenersPerDestination); + createListeners(destinationListeners, consumerId, destination, listenersPerDestination); + listeners.addAll(destinationListeners); + try { + final String topicName = ((Topic) destination).getTopicName(); + for (int subscriptionIndex = 0; subscriptionIndex < sharedSubscription; subscriptionIndex++) { + Connection connection = null; + if (clientID != null) { + connection = jmsConnections[connectionSequence % connections]; + assert connection.getClientID() != null; + connectionSequence++; + } + for (int consumerIndex = 0; consumerIndex < consumers; consumerIndex++) { + if (clientID == null) { + assert connection == null; + connection = jmsConnections[connectionSequence % connections]; + connectionSequence++; + } + final Session session = connection.createSession(transaction ? Session.SESSION_TRANSACTED : Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer; + if (durableSubscription) { + final String subscriptionName = topicName + subscriptionIndex; + consumer = session.createSharedDurableConsumer((Topic) destination, subscriptionName); + silentUnsubscribe.add(() -> { + try { + session.unsubscribe(subscriptionName); + } catch (JMSException e) { + throw new RuntimeException(e); + } + }); + } else { + consumer = session.createSharedConsumer((Topic) destination, topicName + subscriptionIndex); + } + consumer.setMessageListener(destinationListeners.remove()); + } + } + } catch (JMSException fatal) { + throw new RuntimeException(fatal); + } + } + } + return this; + } + + private void createListeners(final Collection listeners, + final AtomicLong consumerId, + final Destination destination, + final int count) { + for (int c = 0; c < count; c++) { + listeners.add(new RecordingMessageListener(consumerId.getAndIncrement(), destination, transaction, + new AtomicLong(0), + msgCountLimiter == null ? null : msgCountLimiter::onMessageReceived, + timeProvider, new SingleWriterRecorder(2), + fatalException)); + } + } + + @Override + public synchronized void close() { + if (!started || closed) { + return; + } + listeners = null; + started = false; + closed = true; + msgCountLimiter = null; + fatalException = null; + if (silentUnsubscribe != null) { + silentUnsubscribe.forEach(Runnable::run); + silentUnsubscribe = null; + } + jmsConnections.forEach(connection -> { + try { + connection.close(); + } catch (JMSException ignore) { + + } + }); + jmsConnections.clear(); + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmarkBuilder.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmarkBuilder.java new file mode 100644 index 0000000000..540e5db62a --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MessageListenerBenchmarkBuilder.java @@ -0,0 +1,96 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.ConnectionFactory; +import javax.jms.Destination; + +public class MessageListenerBenchmarkBuilder { + + private ConnectionFactory factory; + private MicrosTimeProvider timeProvider; + private int consumers; + private long messageCount; + private int connections; + private String clientID; + private Destination[] destinations; + private boolean transaction; + private int sharedSubscription; + private boolean durableSubscription; + private boolean canDelayMessageCount; + + public MessageListenerBenchmarkBuilder setFactory(final ConnectionFactory factory) { + this.factory = factory; + return this; + } + + public MessageListenerBenchmarkBuilder setTimeProvider(final MicrosTimeProvider timeProvider) { + this.timeProvider = timeProvider; + return this; + } + + public MessageListenerBenchmarkBuilder setConsumers(final int consumers) { + this.consumers = consumers; + return this; + } + + public MessageListenerBenchmarkBuilder setMessageCount(final long messageCount) { + this.messageCount = messageCount; + return this; + } + + public MessageListenerBenchmarkBuilder setConnections(final int connections) { + this.connections = connections; + return this; + } + + public MessageListenerBenchmarkBuilder setClientID(final String clientID) { + this.clientID = clientID; + return this; + } + + public MessageListenerBenchmarkBuilder setDestinations(final Destination[] destinations) { + this.destinations = destinations; + return this; + } + + public MessageListenerBenchmarkBuilder setTransacted(final boolean transacted) { + this.transaction = transacted; + return this; + } + + public MessageListenerBenchmarkBuilder setSharedSubscription(final int sharedSubscription) { + this.sharedSubscription = sharedSubscription; + return this; + } + + public MessageListenerBenchmarkBuilder setDurableSubscription(final boolean durableSubscription) { + this.durableSubscription = durableSubscription; + return this; + } + + public MessageListenerBenchmarkBuilder setCanDelayMessageCount(final boolean canDelayMessageCount) { + this.canDelayMessageCount = canDelayMessageCount; + return this; + } + + public MessageListenerBenchmark createMessageListenerBenchmark() { + return new MessageListenerBenchmark(factory, timeProvider, consumers, messageCount, connections, clientID, + destinations, transaction, sharedSubscription, durableSubscription, + canDelayMessageCount); + } +} \ No newline at end of file diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosClock.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosClock.java new file mode 100644 index 0000000000..29875b6e40 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosClock.java @@ -0,0 +1,65 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import java.util.concurrent.TimeUnit; + +public class MicrosClock { + + // no need for volatile here + private static long offset = -1; + private static long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + + private static final boolean AVAILABLE = checkAvailable(); + + private static boolean checkAvailable() { + try { + final long now = now(); + if (now < 0) { + return false; + } + return true; + } catch (Throwable t) { + return false; + } + } + + public static boolean isAvailable() { + return AVAILABLE; + } + + public static long now() { + long epochSecond = offset; + long nanoAdjustment = jdk.internal.misc.VM.getNanoTimeAdjustment(epochSecond); + + if (nanoAdjustment == -1) { + epochSecond = System.currentTimeMillis() / 1000 - 1024; + nanoAdjustment = jdk.internal.misc.VM.getNanoTimeAdjustment(epochSecond); + if (nanoAdjustment == -1) { + throw new InternalError("Offset " + epochSecond + " is not in range"); + } else { + offset = epochSecond; + } + } + final long secs = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, NANOS_PER_SECOND)); + final long secsInUs = TimeUnit.SECONDS.toMicros(secs); + final long nsOffset = (int) Math.floorMod(nanoAdjustment, NANOS_PER_SECOND); + final long usOffset = TimeUnit.NANOSECONDS.toMicros(nsOffset); + return secsInUs + usOffset; + } + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosTimeProvider.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosTimeProvider.java new file mode 100644 index 0000000000..5ed71665f0 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/MicrosTimeProvider.java @@ -0,0 +1,23 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +@FunctionalInterface +public interface MicrosTimeProvider { + + long now(); +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PaddingDecimalFormat.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PaddingDecimalFormat.java new file mode 100644 index 0000000000..da96323200 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PaddingDecimalFormat.java @@ -0,0 +1,74 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; + +public class PaddingDecimalFormat extends DecimalFormat { + + private int minimumLength; + private final StringBuilder pad; + + /** + * Creates a PaddingDecimalFormat using the given pattern and minimum {@code minLength} and the symbols for the default + * locale. + */ + public PaddingDecimalFormat(String pattern, int minLength) { + super(pattern); + minimumLength = minLength; + pad = new StringBuilder(); + } + + /** + * Creates a PaddingDecimalFormat using the given pattern, symbols and minimum minimumLength. + */ + public PaddingDecimalFormat(String pattern, DecimalFormatSymbols symbols, int minLength) { + super(pattern, symbols); + minimumLength = minLength; + pad = new StringBuilder(); + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + int initLength = toAppendTo.length(); + super.format(number, toAppendTo, pos); + return pad(toAppendTo, initLength); + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + int initLength = toAppendTo.length(); + super.format(number, toAppendTo, pos); + return pad(toAppendTo, initLength); + } + + private StringBuffer pad(StringBuffer toAppendTo, int initLength) { + int numLength = toAppendTo.length() - initLength; + int padLength = minimumLength - numLength; + if (padLength > 0) { + final int initialPadLength = pad.length(); + for (int i = initialPadLength; i < padLength; i++) { + pad.append(' '); + } + pad.setLength(padLength); + toAppendTo.insert(initLength, pad); + } + return toAppendTo; + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfClientCommand.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfClientCommand.java new file mode 100644 index 0000000000..2e2985688d --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfClientCommand.java @@ -0,0 +1,202 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import io.netty.channel.DefaultEventLoop; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import org.apache.activemq.artemis.cli.commands.ActionContext; + +@Command(name = "client", description = "It will produce and consume messages to a broker instance") +public class PerfClientCommand extends PerfCommand { + + @Option(name = "--tx", description = "Perform Message::acknowledge per each message received (Default: disabled)") + protected boolean transaction; + + @Option(name = "--shared", description = "Create shared subscription (Default: 0)") + protected int sharedSubscription = 0; + + @Option(name = "--durable", description = "Enabled durable subscription (Default: disabled)") + protected boolean durableSubscription = false; + + @Option(name = "--consumer-connections", description = "Number of consumer connections to be used. Default is same as the total number of consumers") + protected int consumerConnections = 0; + + @Option(name = "--consumers", description = "Number of consumer to use for each generated destination (Default: 1)") + protected int consumersPerDestination = 1; + + @Option(name = "--persistent", description = "It will send messages persistently. Default is non persistent") + protected boolean persistent = false; + + @Option(name = "--message-size", description = "Size of each byteMessage (Default is 1024)") + protected int messageSize = 1024; + + @Option(name = "--rate", description = "Expected total message rate. (Default is unbounded)") + protected Long rate = null; + + @Option(name = "--ttl", description = "TTL for each message") + protected long ttl = 0L; + + @Option(name = "--group", description = "Message Group to be used") + protected String msgGroupID = null; + + @Option(name = "--shared-connections", description = "It create --threads shared connections among producers (Default: not shared)") + protected boolean sharedConnections = false; + + @Option(name = "--tx-size", description = "TX Size") + protected long txSize; + + @Option(name = "--producers", description = "Number of producers to use for each generated destination (Default: 1)") + protected int producersPerDestination = 1; + + @Option(name = "--threads", description = "Number of worker threads to schedule producer load tasks (Default: 1)") + protected int threads = 1; + + @Option(name = "--max-pending", description = "How many not yet completed messages can exists (Default is 1)") + protected long maxPending = 1; + + @Option(name = "--consumer-url", description = "Setup the url used for MessageListener(s) connections. Default is same as --url") + protected String consumerUrl = null; + + @Option(name = "--consumer-protocol", description = "Setup the protocol used for MessageListener(s) connections. Default is same as --protocol") + protected String consumerProtocol = null; + + @Option(name = "--enable-msg-id", description = "Enable setting JMS messageID per-message (Default: disabled)") + protected boolean enableMessageID; + + @Option(name = "--enable-timestamp", description = "Enable setting JMS timestamp per-message (Default: disabled)") + protected boolean enableTimestamp; + + private volatile BenchmarkService producerBenchmark; + + @Override + protected void onExecuteBenchmark(final ConnectionFactory producerConnectionFactory, final Destination[] jmsDestinations, final ActionContext context) throws Exception { + final String listenerProtocol = this.consumerProtocol != null ? this.consumerProtocol : getProtocol(); + final String listenerUrl = this.consumerUrl != null ? this.consumerUrl : brokerURL; + final ConnectionFactory consumerConnectionFactory = createConnectionFactory(listenerUrl, user, password, null, listenerProtocol); + if (consumerConnections == 0) { + if (sharedSubscription > 0) { + if (getClientID() == null) { + consumerConnections = sharedSubscription * consumersPerDestination * jmsDestinations.length; + } else { + consumerConnections = sharedSubscription * jmsDestinations.length; + } + } else { + consumerConnections = consumersPerDestination * jmsDestinations.length; + } + } + final int totalProducers = producersPerDestination * jmsDestinations.length; + + if (threads >= totalProducers) { + if (threads > totalProducers) { + context.err.println("Doesn't make sense to set workers > producers: auto-adjusting it to be the same as the producer count"); + threads = totalProducers; + } + } + + boolean warmingUp = warmup != 0; + final LiveStatistics statistics; + final StringBuilder skratchBuffer = new StringBuilder(); + try (MessageListenerBenchmark consumerBenchmark = new MessageListenerBenchmarkBuilder() + .setClientID(getClientID()) + .setDestinations(consumerProtocol != null ? lookupDestinations(consumerConnectionFactory) : jmsDestinations) + .setFactory(consumerConnectionFactory) + .setTransacted(transaction) + .setConsumers(consumersPerDestination) + .setConnections(consumerConnections) + .setTimeProvider(() -> TimeUnit.NANOSECONDS.toMicros(System.nanoTime())) + .setCanDelayMessageCount(true) + .setSharedSubscription(sharedSubscription) + .setDurableSubscription(durableSubscription) + .createMessageListenerBenchmark()) { + + final DefaultEventLoopGroup eventLoopGroup = new DefaultEventLoopGroup(threads) { + @Override + protected EventLoop newChild(final Executor executor, final Object... args) { + return new DefaultEventLoop(this, executor) { + @Override + protected Queue newTaskQueue(final int maxPendingTasks) { + return new LinkedTransferQueue<>(); + } + }; + } + }; + try (ProducerBenchmark producerBenchmark = new ProducerBenchmarkBuilder() + .setPersistent(persistent) + .setDestinations(jmsDestinations) + .setFactory(producerConnectionFactory) + .setTtl(ttl) + .setTransactionCapacity(txSize) + .setGroup(msgGroupID) + .setProducers(producersPerDestination) + .setMessageRate(rate) + .setMessageCount(messageCount) + .setMessageSize(messageSize) + .setTimeProvider(() -> TimeUnit.NANOSECONDS.toMicros(System.nanoTime())) + .setLoopGroup(eventLoopGroup) + .setMaxPending(maxPending) + .setSharedConnections(sharedConnections) + .setEnableMessageID(enableMessageID) + .setEnableTimestamp(enableTimestamp) + .createProducerBenchmark()) { + this.producerBenchmark = producerBenchmark; + consumerBenchmark.start(); + producerBenchmark.start(); + final long now = System.currentTimeMillis(); + final long endWarmup = warmup > 0 ? now + TimeUnit.SECONDS.toMillis(warmup) : 0; + final long end = duration > 0 ? now + TimeUnit.SECONDS.toMillis(duration) : 0; + statistics = new LiveStatistics(reportFileName, hdrFileName, producerBenchmark.getGenerators(), consumerBenchmark.getListeners()); + LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); + warmingUp = collectAndReportStatisticsWhileRunning(warmingUp, statistics, skratchBuffer, endWarmup, end, producerBenchmark); + final boolean producerFatalError = producerBenchmark.anyError(); + producerBenchmark.asyncClose(); + if (!producerFatalError) { + consumerBenchmark.setMessageCount(producerBenchmark.expectedTotalMessageCountToReceive(sharedSubscription, consumersPerDestination)); + // we don't care about duration here, but just on the expected messages to receive + warmingUp = collectAndReportStatisticsWhileRunning(warmingUp, statistics, skratchBuffer, endWarmup, 0, consumerBenchmark); + } + } + // last sample must be collected while the whole benchmark is complete + statistics.sampleMetrics(warmingUp); + skratchBuffer.setLength(0); + statistics.outSummary(skratchBuffer); + if (!isSilentInput()) { + context.out.println(skratchBuffer); + } + eventLoopGroup.shutdownGracefully(); + statistics.close(); + } + } + + @Override + protected void onInterruptBenchmark() { + final BenchmarkService benchmark = this.producerBenchmark; + if (benchmark != null) { + benchmark.close(); + } + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfCommand.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfCommand.java new file mode 100644 index 0000000000..c30dc59899 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfCommand.java @@ -0,0 +1,154 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import javax.jms.Session; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import io.airlift.airline.Arguments; +import io.airlift.airline.Option; +import org.apache.activemq.artemis.cli.commands.ActionContext; +import org.apache.activemq.artemis.cli.commands.messages.ConnectionAbstract; +import org.apache.activemq.artemis.cli.commands.messages.DestAbstract; + +import static java.util.Collections.singletonList; + +public abstract class PerfCommand extends ConnectionAbstract { + + @Option(name = "--show-latency", description = "Show latencies at interval on output (Default is disabled)") + protected boolean showLatency = false; + + @Option(name = "--json", description = "Report file name (Default is none)") + protected String reportFileName = null; + + @Option(name = "--hdr", description = "HDR Histogram Report file name (Default is none)") + protected String hdrFileName = null; + + @Option(name = "--duration", description = "Test duration in seconds (Default: 0)") + protected int duration = 0; + + @Option(name = "--warmup", description = "Warmup in seconds (Default: 0)") + protected int warmup = 0; + + @Option(name = "--message-count", description = "Total number of messages (Default: 0)") + protected long messageCount = 0; + + @Option(name = "--num-destinations", description = "If present, generate --num-destinations for each destination name, using it as a prefix and adding a number [0,--num-destinations) as suffix. (Default: none)") + protected int numDestinations = 1; + + @Arguments(description = "List of destination names. Each name can be prefixed with queue:// or topic:// and can be an FQQN in the form of
::. (Default: queue://TEST)") + protected List destinations; + + private final CountDownLatch completed = new CountDownLatch(1); + + @Override + public Object execute(ActionContext context) throws Exception { + super.execute(context); + final ConnectionFactory factory = createConnectionFactory(brokerURL, user, password, null, getProtocol()); + final Destination[] jmsDestinations = lookupDestinations(factory, destinations, numDestinations); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + onInterruptBenchmark(); + try { + completed.await(); + } catch (InterruptedException ignored) { + + } + })); + try { + onExecuteBenchmark(factory, jmsDestinations, context); + } finally { + completed.countDown(); + } + return null; + } + + protected abstract void onExecuteBenchmark(ConnectionFactory factory, + Destination[] jmsDestinations, + ActionContext context) throws Exception; + + protected abstract void onInterruptBenchmark(); + + protected boolean collectAndReportStatisticsWhileRunning(boolean warmingUp, + final LiveStatistics statistics, + final StringBuilder skratchBuffer, + final long endWarmup, + final long end, + final BenchmarkService benchmark) throws IOException { + while (benchmark.isRunning()) { + if (end != 0) { + final long tick = System.currentTimeMillis(); + if (tick - end >= 0) { + break; + } + } + if (endWarmup != 0 && warmingUp) { + final long tick = System.currentTimeMillis(); + if (tick - endWarmup >= 0) { + warmingUp = false; + } + } + statistics.sampleMetrics(warmingUp); + skratchBuffer.setLength(0); + statistics.outAtInterval(warmingUp, skratchBuffer, LiveStatistics.ReportInterval.sec, showLatency); + if (!isSilentInput()) { + context.out.println(skratchBuffer); + } + LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); + } + return warmingUp; + } + + protected final Destination[] lookupDestinations(final ConnectionFactory factory) throws Exception { + return lookupDestinations(factory, destinations, numDestinations); + } + + private static Destination[] lookupDestinations(final ConnectionFactory factory, + final List destinations, + final int numDestinations) throws Exception { + final List destinationNames; + if (destinations == null || destinations.isEmpty()) { + destinationNames = singletonList("queue://TEST"); + } else { + destinationNames = destinations; + } + final Destination[] jmsDestinations = new Destination[destinationNames.size() * numDestinations]; + try (Connection connection = factory.createConnection(); + Session session = connection.createSession()) { + int i = 0; + for (String destinationName : destinationNames) { + if (numDestinations == 1) { + jmsDestinations[i] = DestAbstract.getDestination(session, destinationName); + i++; + } else { + for (int suffix = 0; suffix < numDestinations; suffix++) { + jmsDestinations[i] = DestAbstract.getDestination(session, destinationName + suffix); + i++; + } + } + } + } + return jmsDestinations; + } + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfConsumerCommand.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfConsumerCommand.java new file mode 100644 index 0000000000..e0be5651a4 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfConsumerCommand.java @@ -0,0 +1,111 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.activemq.artemis.cli.commands.ActionContext; + +@Command(name = "consumer", description = "It will consume messages from a broker instance") +public class PerfConsumerCommand extends PerfCommand { + + @Option(name = "--tx", description = "Perform Message::acknowledge per each message received (Default: disabled)") + protected boolean transaction; + + @Option(name = "--shared", description = "Create shared subscription (Default: 0)") + protected int sharedSubscription = 0; + + @Option(name = "--durable", description = "Enabled durable subscription (Default: disabled)") + protected boolean durableSubscription = false; + + @Option(name = "--num-connections", description = "Number of connections to be used. Default is same as the total number of consumers") + protected int connections = 0; + + @Option(name = "--consumers", description = "Number of consumer to use for each generated destination (Default: 1)") + protected int consumersPerDestination = 1; + + private BenchmarkService benchmark; + + @Override + protected void onExecuteBenchmark(final ConnectionFactory factory, + final Destination[] jmsDestinations, + final ActionContext context) throws Exception { + MicrosTimeProvider timeProvider = () -> TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); + if (MicrosClock.isAvailable()) { + timeProvider = MicrosClock::now; + } else { + context.err.println("Microseconds wall-clock time not available: using System::currentTimeMillis. Add --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED to the JVM parameters to enable it."); + } + if (connections == 0) { + if (sharedSubscription > 0) { + if (clientID == null) { + connections = sharedSubscription * consumersPerDestination * jmsDestinations.length; + } else { + connections = sharedSubscription * jmsDestinations.length; + } + } else { + connections = consumersPerDestination * jmsDestinations.length; + } + } + + boolean warmingUp = warmup != 0; + final StringBuilder skratchBuffer = new StringBuilder(); + final LiveStatistics statistics; + try (MessageListenerBenchmark benchmark = new MessageListenerBenchmarkBuilder() + .setClientID(getClientID()) + .setDestinations(jmsDestinations) + .setFactory(factory) + .setTransacted(transaction) + .setConsumers(consumersPerDestination) + .setMessageCount(messageCount) + .setConnections(connections) + .setTimeProvider(timeProvider) + .setSharedSubscription(sharedSubscription) + .setDurableSubscription(durableSubscription) + .createMessageListenerBenchmark()) { + this.benchmark = benchmark; + benchmark.start(); + final long now = System.currentTimeMillis(); + final long endWarmup = warmup > 0 ? now + TimeUnit.SECONDS.toMillis(warmup) : 0; + final long end = duration > 0 ? now + TimeUnit.SECONDS.toMillis(duration) : 0; + statistics = new LiveStatistics(reportFileName, hdrFileName, null, benchmark.getListeners()); + LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); + warmingUp = collectAndReportStatisticsWhileRunning(warmingUp, statistics, skratchBuffer, endWarmup, end, benchmark); + } + // last sample must be collected while the whole benchmark is complete + statistics.sampleMetrics(warmingUp); + skratchBuffer.setLength(0); + statistics.outSummary(skratchBuffer); + if (!isSilentInput()) { + context.out.println(skratchBuffer); + } + statistics.close(); + } + + @Override + protected void onInterruptBenchmark() { + final BenchmarkService benchmark = this.benchmark; + if (benchmark != null) { + benchmark.close(); + } + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfProducerCommand.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfProducerCommand.java new file mode 100644 index 0000000000..dc6882ed65 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/PerfProducerCommand.java @@ -0,0 +1,157 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import io.netty.channel.DefaultEventLoop; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import org.apache.activemq.artemis.cli.commands.ActionContext; + +@Command(name = "producer", description = "It will send messages to a broker instance") +public class PerfProducerCommand extends PerfCommand { + @Option(name = "--persistent", description = "It will send messages persistently. Default is non persistent") + protected boolean persistent = false; + + @Option(name = "--message-size", description = "Size of each byteMessage (Default is 1024)") + protected int messageSize = 1024; + + @Option(name = "--rate", description = "Expected total message rate. (Default is unbounded)") + protected Long rate = null; + + @Option(name = "--ttl", description = "TTL for each message") + protected long ttl = 0L; + + @Option(name = "--group", description = "Message Group to be used") + protected String msgGroupID = null; + + @Option(name = "--shared-connections", description = "It create --threads shared connections among producers (Default: not shared)") + protected boolean sharedConnections = false; + + @Option(name = "--tx-size", description = "TX Size") + protected long txSize; + + @Option(name = "--producers", description = "Number of producers to use for each generated destination (Default: 1)") + protected int producersPerDestination = 1; + + @Option(name = "--threads", description = "Number of worker threads to schedule producer load tasks (Default: 1)") + protected int threads = 1; + + @Option(name = "--max-pending", description = "How many not yet completed messages can exists (Default is 1)") + protected long maxPending = 1; + + @Option(name = "--enable-msg-id", description = "Enable setting JMS messageID per-message (Default: disabled)") + protected boolean enableMessageID; + + @Option(name = "--enable-timestamp", description = "Enable setting JMS timestamp per-message (Default: disabled)") + protected boolean enableTimestamp; + + protected volatile BenchmarkService benchmark; + + @Override + protected void onExecuteBenchmark(final ConnectionFactory factory, + final Destination[] jmsDestinations, + final ActionContext context) throws Exception { + if (getClientID() != null) { + context.err.println("ClientID configuration is not supported"); + } + MicrosTimeProvider timeProvider = () -> TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()); + if (MicrosClock.isAvailable()) { + timeProvider = MicrosClock::now; + } else { + context.err.println("Microseconds wall-clock time not available: using System::currentTimeMillis. Add --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED to the JVM parameters to enable it."); + } + + final int totalProducers = producersPerDestination * jmsDestinations.length; + + if (threads >= totalProducers) { + if (threads > totalProducers) { + context.err.println("Doesn't make sense to set workers > producers: auto-adjusting it to be the same as the producer count"); + threads = totalProducers; + } + } + + final DefaultEventLoopGroup eventLoopGroup = new DefaultEventLoopGroup(threads) { + @Override + protected EventLoop newChild(final Executor executor, final Object... args) { + return new DefaultEventLoop(this, executor) { + @Override + protected Queue newTaskQueue(final int maxPendingTasks) { + return new LinkedTransferQueue<>(); + } + }; + } + }; + boolean warmingUp = warmup != 0; + final LiveStatistics statistics; + final StringBuilder skratchBuffer = new StringBuilder(); + try (ProducerBenchmark benchmark = new ProducerBenchmarkBuilder() + .setPersistent(persistent) + .setDestinations(jmsDestinations) + .setFactory(factory) + .setTtl(ttl) + .setTransactionCapacity(txSize) + .setGroup(msgGroupID) + .setProducers(producersPerDestination) + .setMessageRate(rate) + .setMessageCount(messageCount) + .setMessageSize(messageSize) + .setTimeProvider(timeProvider) + .setLoopGroup(eventLoopGroup) + .setMaxPending(maxPending) + .setSharedConnections(sharedConnections) + .setEnableMessageID(enableMessageID) + .setEnableTimestamp(enableTimestamp) + .createProducerBenchmark()) { + this.benchmark = benchmark; + benchmark.start(); + final long now = System.currentTimeMillis(); + final long endWarmup = warmup > 0 ? now + TimeUnit.SECONDS.toMillis(warmup) : 0; + final long end = duration > 0 ? now + TimeUnit.SECONDS.toMillis(duration) : 0; + statistics = new LiveStatistics(reportFileName, hdrFileName, benchmark.getGenerators(), null); + LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); + warmingUp = collectAndReportStatisticsWhileRunning(warmingUp, statistics, skratchBuffer, endWarmup, end, benchmark); + } + // last sample must be collected while the whole benchmark is complete + statistics.sampleMetrics(warmingUp); + skratchBuffer.setLength(0); + statistics.outSummary(skratchBuffer); + if (!isSilentInput()) { + context.out.println(skratchBuffer); + } + eventLoopGroup.shutdownGracefully(); + statistics.close(); + } + + @Override + protected void onInterruptBenchmark() { + final BenchmarkService benchmark = this.benchmark; + if (benchmark != null) { + benchmark.close(); + } + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmark.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmark.java new file mode 100644 index 0000000000..be52268f5d --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmark.java @@ -0,0 +1,341 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.DeliveryMode; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BooleanSupplier; +import java.util.stream.Stream; + +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.EventExecutor; +import org.HdrHistogram.SingleWriterRecorder; +import org.apache.activemq.artemis.api.core.ObjLongPair; + +public final class ProducerBenchmark implements BenchmarkService { + + private final ConnectionFactory factory; + private final MicrosTimeProvider timeProvider; + private final EventLoopGroup eventLoopGroup; + private final int producers; + private final long messageCount; + private final String group; + private final long ttl; + private final int messageSize; + private final Destination[] destinations; + private final boolean persistent; + private final long maxPending; + private final long transactionCapacity; + private final Long messageRate; + private final boolean sharedConnections; + private final boolean enableTimestamp; + private final boolean enableMessageID; + private Set connections; + private ProducerLoadGenerator[] generators; + private boolean started; + private boolean closed; + private final Map> producersPerDestination; + private CompletableFuture allGeneratorClosed; + + public ProducerBenchmark(final ConnectionFactory factory, + final MicrosTimeProvider timeProvider, + final EventLoopGroup loopGroup, + final int producers, + final long messageCount, + final boolean sharedConnections, + final String group, + final long ttl, + final int messageSize, + final Destination[] destinations, + final boolean persistent, + final long maxPending, + final long transactionCapacity, + final Long messageRate, + final boolean enableMessageID, + final boolean enableTimestamp) { + this.factory = factory; + this.timeProvider = timeProvider; + this.eventLoopGroup = loopGroup; + this.producers = producers; + this.messageCount = messageCount; + this.sharedConnections = sharedConnections; + this.group = group; + this.ttl = ttl; + this.messageSize = messageSize; + this.destinations = destinations; + this.persistent = persistent; + this.maxPending = maxPending; + this.transactionCapacity = transactionCapacity; + this.messageRate = messageRate; + this.started = false; + this.closed = false; + this.connections = new HashSet<>(); + this.producersPerDestination = new HashMap<>(destinations.length); + this.enableMessageID = enableMessageID; + this.enableTimestamp = enableTimestamp; + } + + private synchronized Stream> messageSentPerDestination() { + return producersPerDestination.entrySet().stream() + .map(producers -> new ObjLongPair(producers.getKey(), + producers.getValue().stream() + .mapToLong(AsyncJms2ProducerFacade::getMessageSent).sum())); + } + + public synchronized long expectedTotalMessageCountToReceive(final int sharedSubscriptions, + final int consumersPerDestination) { + return expectedTotalMessageCountToReceive(messageSentPerDestination(), sharedSubscriptions, consumersPerDestination); + } + + public static long expectedTotalMessageCountToReceive(final Stream> messageSentPerDestination, + final int sharedSubscriptions, + final int consumersPerDestination) { + return messageSentPerDestination.mapToLong(messagesPerDestination -> { + if (messagesPerDestination.getA() instanceof Topic) { + final int subscribers = sharedSubscriptions > 0 ? sharedSubscriptions : consumersPerDestination; + return subscribers * messagesPerDestination.getB(); + } + assert messagesPerDestination.getA() instanceof Queue; + return messagesPerDestination.getB(); + }).sum(); + } + + public synchronized ProducerLoadGenerator[] getGenerators() { + return generators; + } + + @Override + public synchronized boolean anyError() { + if (!started || closed) { + return false; + } + for (ProducerLoadGenerator loadGenerator : generators) { + if (loadGenerator.getFatalException() != null) { + return true; + } + } + return false; + } + + @Override + public synchronized boolean isRunning() { + if (!started || closed) { + return false; + } + final ProducerLoadGenerator[] generators = this.generators; + if (generators == null) { + return false; + } + boolean running = false; + for (ProducerLoadGenerator loadGenerator : generators) { + if (!loadGenerator.isCompleted()) { + running = true; + } else if (loadGenerator.getFatalException() != null) { + running = false; + break; + } + } + return running; + } + + @Override + public synchronized ProducerBenchmark start() { + if (started) { + return this; + } + producersPerDestination.clear(); + started = true; + closed = false; + // create connections: if shared, one for each event loop, if !shared, one for each producer + final int totalProducers = destinations.length * producers; + final IdentityHashMap sharedConnections = this.sharedConnections ? new IdentityHashMap<>() : null; + final Connection[] exclusiveConnections = !this.sharedConnections ? new Connection[totalProducers] : null; + if (this.sharedConnections) { + eventLoopGroup.forEach(eventExecutor -> { + final Connection connection; + try { + connection = factory.createConnection(); + connections.add(connection); + sharedConnections.put(eventExecutor, connection); + } catch (JMSException e) { + throw new RuntimeException(e); + } + }); + } else { + for (int i = 0; i < totalProducers; i++) { + try { + final Connection connection = factory.createConnection(); + exclusiveConnections[i] = connection; + connections.add(connection); + } catch (JMSException e) { + throw new RuntimeException(e); + } + } + } + // start connections + connections.forEach(connection -> { + try { + connection.start(); + } catch (JMSException e) { + throw new RuntimeException(e); + } + }); + final AtomicLong producerId = new AtomicLong(1); + // create shared content + final byte[] messageContent = new byte[messageSize]; + Arrays.fill(messageContent, (byte) 1); + // create producers/sessions/senders/load generators + this.generators = new ProducerLoadGenerator[totalProducers]; + final ArrayList allProducers = new ArrayList<>(totalProducers); + int producerSequence = 0; + final int messageCountPerProducer = (int) (messageCount / totalProducers); + long remainingMessageCount = messageCount; + final Long messageRatePerProducer = messageRate == null ? null : (messageRate / totalProducers); + Long remainingMessageRate = messageRate; + for (int d = 0; d < destinations.length; d++) { + final Destination destination = destinations[d]; + final ArrayList producers = new ArrayList<>(this.producers); + producersPerDestination.put(destination, producers); + for (int i = 0; i < this.producers; i++) { + final EventLoop eventLoop = eventLoopGroup.next(); + final Connection connection; + if (this.sharedConnections) { + connection = sharedConnections.get(eventLoop); + } else { + connection = exclusiveConnections[producerSequence]; + } + final Session session; + final MessageProducer producer; + try { + session = connection.createSession(transactionCapacity > 0 ? Session.SESSION_TRANSACTED : Session.AUTO_ACKNOWLEDGE); + producer = session.createProducer(destination); + producer.setDisableMessageID(!enableMessageID); + producer.setDisableMessageTimestamp(!enableTimestamp); + producer.setDeliveryMode(persistent ? DeliveryMode.PERSISTENT : DeliveryMode.NON_PERSISTENT); + producer.setTimeToLive(ttl); + } catch (JMSException e) { + throw new RuntimeException(e); + } + final AsyncJms2ProducerFacade producerFacade = new AsyncJms2ProducerFacade(producerId.getAndIncrement(), + session, producer, destination, + maxPending, transactionCapacity); + allProducers.add(producerFacade); + producers.add(producerFacade); + final BooleanSupplier keepOnSendingStrategy; + if (messageCount == 0) { + keepOnSendingStrategy = () -> true; + } else { + final long count = Math.min(messageCountPerProducer, remainingMessageCount); + remainingMessageCount -= count; + keepOnSendingStrategy = () -> producerFacade.getMessageSent() < count; + } + final SingleWriterRecorder sendLatencyRecorder = new SingleWriterRecorder(2); + + final Long ratePeriodNanos; + if (messageRatePerProducer != null) { + final long rate = Math.min(messageRatePerProducer, remainingMessageRate); + ratePeriodNanos = TimeUnit.SECONDS.toNanos(1) / rate; + remainingMessageRate -= rate; + } else { + ratePeriodNanos = null; + } + + generators[producerSequence] = ratePeriodNanos != null ? + new ProducerTargetRateLoadGenerator(producerFacade, eventLoop, timeProvider, keepOnSendingStrategy, ratePeriodNanos, group, messageContent, sendLatencyRecorder, new SingleWriterRecorder(2)) : + new ProducerMaxLoadGenerator(producerFacade, eventLoop, timeProvider, keepOnSendingStrategy, group, messageContent, sendLatencyRecorder); + + producerSequence++; + } + } + + // deploy and start generators + for (int i = 0; i < totalProducers; i++) { + generators[i].getExecutor().execute(generators[i]); + } + return this; + } + + /** + * After this, now new messages are sent, but there still be some to be completed: the return value can be used + * to await completions to arrive. + */ + public synchronized CompletionStage asyncClose() { + if (!started || closed) { + return CompletableFuture.completedFuture(null); + } + if (allGeneratorClosed != null) { + return allGeneratorClosed; + } + final CompletableFuture[] closedGenerators = new CompletableFuture[generators.length]; + for (int i = 0; i < generators.length; i++) { + final CompletableFuture onClosed = new CompletableFuture(); + closedGenerators[i] = onClosed; + try { + generators[i].asyncClose(() -> onClosed.complete(null)).get(); + } catch (final Throwable ignore) { + closedGenerators[i].completeExceptionally(ignore); + } + } + CompletableFuture allGeneratorClosed = CompletableFuture.allOf(closedGenerators); + final Connection[] openedConnections = connections.toArray(new Connection[connections.size()]); + this.allGeneratorClosed = allGeneratorClosed.whenCompleteAsync((res, error) -> { + synchronized (this) { + generators = null; + started = false; + closed = true; + this.allGeneratorClosed = null; + } + for (Connection connection : openedConnections) { + // close connection: it should roll-back pending opened sessions and await completion to be called + try { + connection.close(); + } catch (JMSException ignore) { + + } + } + }, eventLoopGroup); + connections.clear(); + return allGeneratorClosed; + } + + @Override + public void close() { + asyncClose().toCompletableFuture().join(); + } + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmarkBuilder.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmarkBuilder.java new file mode 100644 index 0000000000..2c612e5a2f --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerBenchmarkBuilder.java @@ -0,0 +1,128 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.ConnectionFactory; +import javax.jms.Destination; + +import io.netty.channel.EventLoopGroup; + +public class ProducerBenchmarkBuilder { + + private ConnectionFactory factory; + private MicrosTimeProvider timeProvider; + private EventLoopGroup loopGroup; + private int producers; + private long messageCount; + private String group; + private long ttl; + private int messageSize; + private Destination[] destinations; + private boolean persistent; + private long maxPending; + private long transactionCapacity; + private Long messageRate; + private boolean sharedConnections; + private boolean enableMessageID; + private boolean enableTimestamp; + + public ProducerBenchmarkBuilder setFactory(final ConnectionFactory factory) { + this.factory = factory; + return this; + } + + public ProducerBenchmarkBuilder setTimeProvider(final MicrosTimeProvider timeProvider) { + this.timeProvider = timeProvider; + return this; + } + + public ProducerBenchmarkBuilder setLoopGroup(final EventLoopGroup loopGroup) { + this.loopGroup = loopGroup; + return this; + } + + public ProducerBenchmarkBuilder setProducers(final int producers) { + this.producers = producers; + return this; + } + + public ProducerBenchmarkBuilder setMessageCount(final long messageCount) { + this.messageCount = messageCount; + return this; + } + + public ProducerBenchmarkBuilder setSharedConnections(final boolean sharedConnections) { + this.sharedConnections = sharedConnections; + return this; + } + + public ProducerBenchmarkBuilder setGroup(final String group) { + this.group = group; + return this; + } + + public ProducerBenchmarkBuilder setTtl(final long ttl) { + this.ttl = ttl; + return this; + } + + public ProducerBenchmarkBuilder setMessageSize(final int messageSize) { + this.messageSize = messageSize; + return this; + } + + public ProducerBenchmarkBuilder setDestinations(final Destination[] destinations) { + this.destinations = destinations; + return this; + } + + public ProducerBenchmarkBuilder setPersistent(final boolean persistent) { + this.persistent = persistent; + return this; + } + + public ProducerBenchmarkBuilder setMaxPending(final long maxPending) { + this.maxPending = maxPending; + return this; + } + + public ProducerBenchmarkBuilder setTransactionCapacity(final long transactionCapacity) { + this.transactionCapacity = transactionCapacity; + return this; + } + + public ProducerBenchmarkBuilder setMessageRate(final Long messageRate) { + this.messageRate = messageRate; + return this; + } + + public ProducerBenchmarkBuilder setEnableTimestamp(final boolean enableTimestamp) { + this.enableTimestamp = enableTimestamp; + return this; + } + + public ProducerBenchmarkBuilder setEnableMessageID(final boolean enableMessageID) { + this.enableMessageID = enableMessageID; + return this; + } + + public ProducerBenchmark createProducerBenchmark() { + return new ProducerBenchmark(factory, timeProvider, loopGroup, producers, messageCount, sharedConnections, + group, ttl, messageSize, destinations, persistent, maxPending, + transactionCapacity, messageRate, enableMessageID, enableTimestamp); + } +} \ No newline at end of file diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerLoadGenerator.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerLoadGenerator.java new file mode 100644 index 0000000000..58e2dbd5c4 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerLoadGenerator.java @@ -0,0 +1,42 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import java.util.concurrent.Future; + +import io.netty.util.concurrent.OrderedEventExecutor; +import org.HdrHistogram.SingleWriterRecorder; + +public interface ProducerLoadGenerator extends Runnable { + + OrderedEventExecutor getExecutor(); + + @Override + void run(); + + Future asyncClose(Runnable onClosed); + + SingleWriterRecorder getWaitLatencies(); + + SingleWriterRecorder getSendCompletedLatencies(); + + AsyncJms2ProducerFacade getProducer(); + + boolean isCompleted(); + + Exception getFatalException(); +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerMaxLoadGenerator.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerMaxLoadGenerator.java new file mode 100644 index 0000000000..f7190bc4db --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerMaxLoadGenerator.java @@ -0,0 +1,52 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import java.util.function.BooleanSupplier; + +import io.netty.util.concurrent.OrderedEventExecutor; +import org.HdrHistogram.SingleWriterRecorder; + +public final class ProducerMaxLoadGenerator extends SkeletalProducerLoadGenerator { + + public ProducerMaxLoadGenerator(final AsyncJms2ProducerFacade producer, + final OrderedEventExecutor executor, + final MicrosTimeProvider timeProvider, + final BooleanSupplier keepOnSending, + final String group, + final byte[] msgContent, + final SingleWriterRecorder sendCompletedLatencies) { + super(producer, executor, timeProvider, keepOnSending, group, msgContent, sendCompletedLatencies, null); + } + + @Override + public void run() { + if (closed || stopLoad) { + return; + } + if (!trySend(timeProvider.now())) { + return; + } + if (!keepOnSending.getAsBoolean()) { + producer.requestClose(); + stopLoad = true; + return; + } + asyncContinue(); + } + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerTargetRateLoadGenerator.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerTargetRateLoadGenerator.java new file mode 100644 index 0000000000..3310a02491 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/ProducerTargetRateLoadGenerator.java @@ -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.activemq.artemis.cli.commands.messages.perf; + +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +import io.netty.util.concurrent.OrderedEventExecutor; +import org.HdrHistogram.SingleWriterRecorder; + +public final class ProducerTargetRateLoadGenerator extends SkeletalProducerLoadGenerator { + + private final long usPeriod; + private long fireTimeMicros; + private boolean started; + + public ProducerTargetRateLoadGenerator(final AsyncJms2ProducerFacade producer, + final OrderedEventExecutor executor, + final MicrosTimeProvider timeProvider, + final BooleanSupplier keepOnSending, + final long nsPeriod, + final String group, + final byte[] msgContent, + final SingleWriterRecorder sendCompletedLatencies, + final SingleWriterRecorder waitLatencies) { + super(producer, executor, timeProvider, keepOnSending, group, msgContent, sendCompletedLatencies, waitLatencies); + this.fireTimeMicros = 0; + this.usPeriod = TimeUnit.NANOSECONDS.toMicros(nsPeriod); + this.started = false; + } + + @Override + public void run() { + if (closed || stopLoad) { + return; + } + final long now = timeProvider.now(); + if (!started) { + started = true; + fireTimeMicros = now; + } + if (!trySend(fireTimeMicros, now)) { + return; + } + if (!keepOnSending.getAsBoolean()) { + producer.requestClose(); + stopLoad = true; + return; + } + fireTimeMicros += usPeriod; + final long delay = fireTimeMicros - timeProvider.now(); + final long usToNextFireTime = Math.max(0, delay); + asyncContinue(usToNextFireTime); + } +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RateSampler.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RateSampler.java new file mode 100644 index 0000000000..0e92168183 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RateSampler.java @@ -0,0 +1,59 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import java.util.function.LongSupplier; + +public final class RateSampler implements Runnable { + + private final LongSupplier sampling; + private long lastSample; + private long lastSampleTime; + private long rate; + private long timeSpanNs; + + private RateSampler(final LongSupplier sampling) { + this.sampling = sampling; + this.timeSpanNs = 0; + this.lastSampleTime = System.nanoTime(); + this.lastSample = sampling.getAsLong(); + this.rate = 0; + } + + @Override + public void run() { + final long now = System.nanoTime(); + final long newSample = sampling.getAsLong(); + rate = newSample - lastSample; + timeSpanNs = now - lastSampleTime; + lastSample = newSample; + lastSampleTime = now; + } + + public long getLastSample() { + return lastSample; + } + + public long reportRate(final long reportIntervalNs) { + return (rate * reportIntervalNs) / timeSpanNs; + } + + public static RateSampler of(final LongSupplier sampling) { + return new RateSampler(sampling); + } + +} diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RecordingMessageListener.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RecordingMessageListener.java new file mode 100644 index 0000000000..b650e79886 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/RecordingMessageListener.java @@ -0,0 +1,101 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.HdrHistogram.SingleWriterRecorder; + +public final class RecordingMessageListener implements MessageListener { + + private final long id; + private final Destination destination; + private final boolean transaction; + private final AtomicLong receivedMessages; + private final Runnable onMessageReceived; + private final MicrosTimeProvider timeProvider; + private final SingleWriterRecorder receiveLatencyRecorder; + private AtomicBoolean fatalException; + + RecordingMessageListener(final long id, + final Destination destination, + final boolean transaction, + final AtomicLong receivedMessages, + final Runnable onMessageReceived, + final MicrosTimeProvider timeProvider, + final SingleWriterRecorder receiveLatencyRecorder, + final AtomicBoolean fatalException) { + this.id = id; + this.destination = destination; + this.transaction = transaction; + this.receivedMessages = receivedMessages; + this.onMessageReceived = onMessageReceived; + this.timeProvider = timeProvider; + this.receiveLatencyRecorder = receiveLatencyRecorder; + this.fatalException = fatalException; + } + + public boolean anyFatalException() { + return fatalException.get(); + } + + public SingleWriterRecorder getReceiveLatencyRecorder() { + return receiveLatencyRecorder; + } + + public long getId() { + return id; + } + + public Destination getDestination() { + return destination; + } + + public long getReceivedMessages() { + return receivedMessages.get(); + } + + @Override + public void onMessage(final Message message) { + if (onMessageReceived != null) { + onMessageReceived.run(); + } + receivedMessages.lazySet(receivedMessages.get() + 1); + if (receiveLatencyRecorder != null) { + try { + final long start = message.getLongProperty("time"); + final long receivedOn = timeProvider.now(); + final long elapsedUs = receivedOn - start; + receiveLatencyRecorder.recordValue(elapsedUs); + } catch (JMSException fatal) { + fatalException.compareAndSet(false, true); + } + } + if (transaction) { + try { + message.acknowledge(); + } catch (JMSException fatal) { + fatalException.compareAndSet(false, true); + } + } + } +} \ No newline at end of file diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/SkeletalProducerLoadGenerator.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/SkeletalProducerLoadGenerator.java new file mode 100644 index 0000000000..5d186a02e1 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/messages/perf/SkeletalProducerLoadGenerator.java @@ -0,0 +1,241 @@ +/* + * 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.activemq.artemis.cli.commands.messages.perf; + +import javax.jms.BytesMessage; +import javax.jms.CompletionListener; +import javax.jms.JMSException; +import javax.jms.Message; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BooleanSupplier; + +import io.netty.util.concurrent.OrderedEventExecutor; +import org.HdrHistogram.SingleWriterRecorder; +import org.apache.activemq.artemis.cli.commands.messages.perf.AsyncJms2ProducerFacade.SendAttemptResult; + +public abstract class SkeletalProducerLoadGenerator implements CompletionListener, ProducerLoadGenerator { + + protected final AsyncJms2ProducerFacade producer; + private final OrderedEventExecutor executor; + protected final BooleanSupplier keepOnSending; + protected final MicrosTimeProvider timeProvider; + private final String group; + private final byte[] messageContent; + private BytesMessage messageToSend; + protected boolean closed; + protected volatile boolean stopLoad; + private final SingleWriterRecorder waitLatencies; + private final SingleWriterRecorder sendCompletedLatencies; + private final AtomicLong unprocessedCompletions; + private final AtomicBoolean scheduledProcessingCompletions; + private volatile Exception fatalException; + private boolean stopHandlingCompletions; + + public SkeletalProducerLoadGenerator(final AsyncJms2ProducerFacade producer, + final OrderedEventExecutor executor, + final MicrosTimeProvider timeProvider, + final BooleanSupplier keepOnSending, + final String group, + final byte[] msgContent, + final SingleWriterRecorder sendCompletedLatencies, + final SingleWriterRecorder waitLatencies) { + this.sendCompletedLatencies = sendCompletedLatencies; + this.waitLatencies = waitLatencies; + this.producer = producer; + this.executor = executor; + this.timeProvider = timeProvider; + this.keepOnSending = keepOnSending; + this.group = group; + this.messageContent = msgContent; + this.messageToSend = null; + this.closed = false; + this.stopLoad = false; + this.unprocessedCompletions = new AtomicLong(); + this.scheduledProcessingCompletions = new AtomicBoolean(); + this.fatalException = null; + this.stopHandlingCompletions = false; + } + + @Override + public Exception getFatalException() { + return fatalException; + } + + @Override + public SingleWriterRecorder getSendCompletedLatencies() { + return sendCompletedLatencies; + } + + @Override + public SingleWriterRecorder getWaitLatencies() { + return waitLatencies; + } + + @Override + public AsyncJms2ProducerFacade getProducer() { + return producer; + } + + @Override + public boolean isCompleted() { + if (stopLoad && fatalException != null) { + return true; + } + return stopLoad && producer.getMessageCompleted() == producer.getMessageSent(); + } + + @Override + public OrderedEventExecutor getExecutor() { + return executor; + } + + protected final void asyncContinue() { + asyncContinue(0); + } + + protected final void asyncContinue(final long usDelay) { + if (usDelay == 0) { + executor.execute(this); + } else { + executor.schedule(this, usDelay, TimeUnit.MICROSECONDS); + } + } + + protected final boolean trySend(final long sendTime) { + return trySend(sendTime, sendTime); + } + + protected final boolean trySend(final long expectedSendTime, final long sendTime) { + assert executor.inEventLoop(); + assert !closed; + try { + if (messageToSend == null) { + messageToSend = producer.createBytesMessage(); + messageToSend.writeBytes(this.messageContent); + } + messageToSend.setLongProperty("time", sendTime); + if (group != null) { + messageToSend.setStringProperty("JMSXGroupID", group); + } + final SendAttemptResult result = producer.trySend(messageToSend, this, this); + if (result != SendAttemptResult.NotAvailable) { + messageToSend = null; + if (result == SendAttemptResult.Success) { + if (waitLatencies != null) { + waitLatencies.recordValue(sendTime - expectedSendTime); + } + } + } + return result == SendAttemptResult.Success; + } catch (final JMSException e) { + onSendErrored(e); + return false; + } + } + + @Override + public void onCompletion(final Message message) { + asyncOnSendCompleted(message, null); + } + + @Override + public void onException(final Message message, final Exception exception) { + asyncOnSendCompleted(message, exception); + } + + private void asyncOnSendCompleted(final Message message, Exception completionError) { + if (stopHandlingCompletions) { + return; + } + if (completionError == null) { + try { + recordSendCompletionLatency(message); + unprocessedCompletions.incrementAndGet(); + scheduleProcessingCompletions(); + } catch (final JMSException jmsException) { + completionError = jmsException; + } + } + if (completionError != null) { + stopHandlingCompletions = true; + final Exception fatal = completionError; + executor.execute(() -> onSendErrored(fatal)); + } + } + + private void onSendErrored(final Exception fatal) { + assert executor.inEventLoop(); + if (fatalException != null) { + return; + } + producer.onSendErrored(); + fatalException = fatal; + stopLoad = true; + closed = true; + } + + private void scheduleProcessingCompletions() { + if (unprocessedCompletions.get() > 0 && scheduledProcessingCompletions.compareAndSet(false, true)) { + executor.execute(this::processCompletions); + } + } + + private void processCompletions() { + assert executor.inEventLoop(); + assert scheduledProcessingCompletions.get(); + if (fatalException != null) { + return; + } + final long completions = unprocessedCompletions.getAndSet(0); + for (long i = 0; i < completions; i++) { + final JMSException completionException = producer.onSendCompleted(); + if (completionException != null) { + fatalException = completionException; + return; + } + } + scheduledProcessingCompletions.set(false); + scheduleProcessingCompletions(); + } + + private void recordSendCompletionLatency(final Message message) throws JMSException { + final long time = message.getLongProperty("time"); + final long elapsedMicros = timeProvider.now() - time; + sendCompletedLatencies.recordValue(elapsedMicros); + } + + @Override + public Future asyncClose(final Runnable onClosed) { + return executor.submit(() -> onClose(onClosed)); + } + + private void onClose(final Runnable onClosed) { + assert executor.inEventLoop(); + if (closed) { + onClosed.run(); + return; + } + closed = true; + // no need for this anymore + messageToSend = null; + producer.requestClose(onClosed); + } +} diff --git a/artemis-distribution/src/main/resources/bin/artemis b/artemis-distribution/src/main/resources/bin/artemis index 4b5ebe5ad2..7bc8329c1b 100755 --- a/artemis-distribution/src/main/resources/bin/artemis +++ b/artemis-distribution/src/main/resources/bin/artemis @@ -44,8 +44,8 @@ if [ -z "$ARTEMIS_HOME" ] ; then ARTEMIS_HOME=`cd "$ARTEMIS_HOME/.." && pwd` fi -# Set Defaults Properties -JAVA_ARGS="-XX:+UseParallelGC -Xms512M -Xmx1024M" +# Set Defaults JAVA_ARGS Properties if not already set: this allow specific artemis commands to override default JVM configuration +if [ -z "${JAVA_ARGS}" ]; then JAVA_ARGS="-XX:+UseParallelGC -Xms512M -Xmx1024M"; fi CLASSPATH="$ARTEMIS_HOME/lib/artemis-boot.jar" # OS specific support. diff --git a/artemis-distribution/src/main/resources/bin/artemis.cmd b/artemis-distribution/src/main/resources/bin/artemis.cmd index 66f4cdbbb1..7d6452f122 100755 --- a/artemis-distribution/src/main/resources/bin/artemis.cmd +++ b/artemis-distribution/src/main/resources/bin/artemis.cmd @@ -45,7 +45,7 @@ echo. :RUN_JAVA rem "Set Defaults." -set JAVA_ARGS=-XX:+UseParallelGC -Xms512M -Xmx1024M +if "%JAVA_ARGS%" == "" set JAVA_ARGS=-XX:+UseParallelGC -Xms512M -Xmx1024M rem "Create full JVM Args" set JVM_ARGS=%JAVA_ARGS% diff --git a/artemis-distribution/src/main/resources/licenses/bin/LICENSE b/artemis-distribution/src/main/resources/licenses/bin/LICENSE index d1adb40b3e..2c4e096055 100644 --- a/artemis-distribution/src/main/resources/licenses/bin/LICENSE +++ b/artemis-distribution/src/main/resources/licenses/bin/LICENSE @@ -280,6 +280,12 @@ For jakarta.xml.bind-api: This product bundles jakarta.xml.bind-api, which is available under the Eclipse Distribution License (EDL) 1.0. For details, see licenses/EDL-1.0.txt +============================================================================== +For HdrHistogram: +============================================================================== +This product bundles HdrHistogram, which is available under the +"2-clause BSD" license. For details, see licences/LICENSE-hdrhistogram.txt. + ============================================================================== Apache ActiveMQ Artemis Subcomponents: diff --git a/artemis-distribution/src/main/resources/licenses/bin/licenses/LICENSE-hdrhistogram.txt b/artemis-distribution/src/main/resources/licenses/bin/licenses/LICENSE-hdrhistogram.txt new file mode 100644 index 0000000000..ce8e7c0ea1 --- /dev/null +++ b/artemis-distribution/src/main/resources/licenses/bin/licenses/LICENSE-hdrhistogram.txt @@ -0,0 +1,41 @@ +The code in this repository code was Written by Gil Tene, Michael Barker, +and Matt Warren, and released to the public domain, as explained at +http://creativecommons.org/publicdomain/zero/1.0/ + +For users of this code who wish to consume it under the "BSD" license +rather than under the public domain or CC0 contribution text mentioned +above, the code found under this directory is *also* provided under the +following license (commonly referred to as the BSD 2-Clause License). This +license does not detract from the above stated release of the code into +the public domain, and simply represents an additional license granted by +the Author. + +----------------------------------------------------------------------------- +** Beginning of "BSD 2-Clause License" text. ** + + Copyright (c) 2012, 2013, 2014, 2015, 2016 Gil Tene + Copyright (c) 2014 Michael Barker + Copyright (c) 2014 Matt Warren + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/artemis-features/src/main/resources/features.xml b/artemis-features/src/main/resources/features.xml index 9af4cc2b56..f26d8d1d2e 100644 --- a/artemis-features/src/main/resources/features.xml +++ b/artemis-features/src/main/resources/features.xml @@ -73,6 +73,7 @@ mvn:org.apache.commons/commons-text/${commons.text.version} mvn:org.apache.commons/commons-lang3/${commons.lang.version} mvn:org.jctools/jctools-core/${jctools.version} + mvn:org.hdrhistogram/HdrHistogram/${hdrhistogram.version} mvn:com.google.guava/failureaccess/1.0.1 mvn:com.google.guava/guava/${guava.version} mvn:org.apache.commons/commons-dbcp2/${commons.dbcp2.version} diff --git a/docs/user-manual/en/SUMMARY.md b/docs/user-manual/en/SUMMARY.md index 45607d6310..0d0b1b6a38 100644 --- a/docs/user-manual/en/SUMMARY.md +++ b/docs/user-manual/en/SUMMARY.md @@ -85,5 +85,6 @@ * [Maven Plugin](maven-plugin.md) * [Unit Testing](unit-testing.md) * [Troubleshooting and Performance Tuning](perf-tuning.md) +* [Performance Tools](perf-tools.md) * [Configuration Reference](configuration-index.md) * [Restart Sequence](restart-sequence.md) diff --git a/docs/user-manual/en/images/30K.png b/docs/user-manual/en/images/30K.png new file mode 100644 index 0000000000000000000000000000000000000000..aed763ec5543092192665b6ffe605b7cc04864e1 GIT binary patch literal 48908 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

Z{8DbJ?Lqku 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 literal 0 HcmV?d00001 diff --git a/docs/user-manual/en/images/test.png b/docs/user-manual/en/images/test.png new file mode 100644 index 0000000000000000000000000000000000000000..131a46c0a4d648ee5ec36ff75a9dcbd4e5f01ab9 GIT binary patch literal 56536 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 **Note:** +> The tools can run both from within the broker instance's folder or +> from the base artemis `bin` folder. +> In the former case it will use the same JVM parameter configured on the instance (on `artemis.profile`), +> while in the latter case the user should set `JAVA_ARGS` environment variable to override default heap and GC parameters +> +> ie `-XX:+UseParallelGC -Xms512M -Xmx1024M` + +## Case 1: Single producer Single consumer over a queue + +This is the simplest possible case: running a load test with 1 producer and 1 consumer on a non-durable queue `TEST_QUEUE`, +using [non-persistent](https://jakarta.ee/specifications/messaging/2.0/apidocs/javax/jms/deliverymode#NON_PERSISTENT) +1024 bytes long (by default) messages, using [auto-acknowledge](https://jakarta.ee/specifications/messaging/2.0/apidocs/javax/jms/session#AUTO_ACKNOWLEDGE). + +Let's see what happens after typing: +```bash +$ ./artemis perf client queue://TEST_QUEUE +Connection brokerURL = tcp://localhost:61616 +2022-01-18 10:30:54,535 WARN [org.apache.activemq.artemis.core.client] AMQ212053: CompletionListener/SendAcknowledgementHandler used with confirmationWindowSize=-1. Enable confirmationWindowSize to receive acks from server! +--- warmup false +--- sent: 7316 msg/sec +--- blocked: 6632 msg/sec +--- completed: 7320 msg/sec +--- received: 7317 msg/sec +# ... +``` +The test keeps on running, until `SIGTERM` or `SIGINT` signals are sent to the Java process (on Linux Console it translates into pressing **CTRL + C**). +Before looking what the metrics mean, there's an initial `WARN` log that shouldn't be ignored: +```bash +WARN [org.apache.activemq.artemis.core.client] AMQ212053: CompletionListener/SendAcknowledgementHandler used with confirmationWindowSize=-1. Enable confirmationWindowSize to receive acks from server! +``` +It shows two things: +1. the load generator uses [async message producers](https://jakarta.ee/specifications/messaging/2.0/apidocs/javax/jms/messageproducer#send-javax.jms.Destination-javax.jms.Message-javax.jms.CompletionListener-) +2. `confirmationWindowSize` is an Artemis CORE protocol specific setting; the `perf` commands uses CORE as the default JMS provider + +###Live Latency Console Reporting + +The `perf client` command can report on Console different latency percentiles metrics by adding `--show-latency` to the command arguments, but in order to obtain meaningful metrics, we need to address `WARN` by setting `confirmationWindowSize` on the producer `url`, +setting `--consumer-url` to save applying the same configuration for consumer(s). + +In short, the command is using these additional parameters: +```bash +--show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 +``` +####Running it +```bash +$ ./artemis perf client --show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 queue://TEST_QUEUE +--- warmup false +--- sent: 8114 msg/sec +--- blocked: 8114 msg/sec +--- completed: 8114 msg/sec +--- received: 8113 msg/sec +--- send ack time: mean: 113.01 us - 50.00%: 106.00 us - 90.00%: 142.00 us - 99.00%: 204.00 us - 99.90%: 371.00 us - 99.99%: 3455.00 us - max: 3455.00 us +--- transfer time: mean: 213.71 us - 50.00%: 126.00 us - 90.00%: 177.00 us - 99.00%: 3439.00 us - 99.90%: 7967.00 us - 99.99%: 8895.00 us - max: 8895.00 us +# CTRL + C pressed +--- SUMMARY +--- result: success +--- total sent: 70194 +--- total blocked: 70194 +--- total completed: 70194 +--- total received: 70194 +--- aggregated send time: mean: 101.53 us - 50.00%: 86.00 us - 90.00%: 140.00 us - 99.00%: 283.00 us - 99.90%: 591.00 us - 99.99%: 2007.00 us - max: 24959.00 us +--- aggregated transfer time: mean: 127.48 us - 50.00%: 97.00 us - 90.00%: 166.00 us - 99.00%: 449.00 us - 99.90%: 4671.00 us - 99.99%: 8255.00 us - max: 27263.00 us +``` +Some notes: +1. `WARN` message is now gone +2. `send ack time` and `transfer time` statistics are printed at second interval +3. `total` and `aggregated` metrics are printed on test completion (more on this later) + +The meaning of the live latency statistics are: +- `send ack time`: percentiles of latency to acknowledge sent messages +- `transfer time`: percentiles of latency to transfer messages from producer(s) to consumer(s) + +The `perf` commands uses [JMS 2 async message producers](https://jakarta.ee/specifications/messaging/2.0/apidocs/javax/jms/messageproducer#send-javax.jms.Destination-javax.jms.Message-javax.jms.CompletionListener-) +that allow the load generator to accumulate in-flight sent messages and depending on the protocol implementation, may block its producer thread due to producer flow control. +e.g: the Artemis CORE protocol can block producers threads to refill producers credits, while the [QPID-JMS](https://qpid.apache.org/components/jms/index.html) won't. + +The `perf` tool is implementing its own in-flight sent requests tracking and can be configured to limit the amount of pending sent messages, +while reporting the rate by which producers are "blocked" awaiting completions + +> **Producers threads are `blocked`?** +> Although the load back-pressure mechanism is non-blocking, given that the load generator cannot push further load while back-pressured +> by the protocol client, the load is semantically "blocked". +> This detail is relevant to explain the live rate [statistics](#running-it) on Console: +> + +By default, the `perf` tools (i.e: `client` and `producer`) **limits the number of in-flight request to 1**: to change the default setting +users should add `--max-pending` parameter configuration. + +> **Note:** +> Setting `--max-pending 0` will disable the load generator in-flight sent messages limiter, allowing the tool to accumulate +> an unbounded number of in-flight messages, risking `OutOfMemoryError`. +> This is **NOT RECOMMENDED!** + +More detail on the metrics: +- `warmup`: the generator phase while the statistics sample is collected; warmup duration can be set by setting `--warmup` +- `sent`: the message sent rate +- `blocked`: the rate of attempts to send a new message, "blocked" awaiting `--max-pending` refill +- `completed`: the rate of message send acknowledgements received by producer(s) +- `received`: the rate of messages received by consumer(s) + +###How to read the live statistics? +The huge amount of `blocked` vs `sent` means that the broker wasn't fast enough to refill the single `--max-pending` budget +before sending a new message. +It can be changed into: +```bash +--max-pending 100 +``` +#####to our previous command: +```bash +$ ./artemis perf client --warmup 20 --max-pending 100 --show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 queue://TEST_QUEUE +Connection brokerURL = tcp://localhost:61616?confirmationWindowSize=20000 +# first samples shows very BAD performance because client JVM is still warming up +--- warmup true +--- sent: 27366 msg/sec +--- blocked: 361 msg/sec +--- completed: 27305 msg/sec +--- received: 26195 msg/sec +--- send ack time: mean: 1743.39 us - 50.00%: 1551.00 us - 90.00%: 3119.00 us - 99.00%: 5215.00 us - 99.90%: 8575.00 us - 99.99%: 8703.00 us - max: 23679.00 us +--- transfer time: mean: 11860.32 us - 50.00%: 11583.00 us - 90.00%: 18559.00 us - 99.00%: 24319.00 us - 99.90%: 31359.00 us - 99.99%: 31615.00 us - max: 31615.00 us +# ... > 20 seconds later ... +# performance is now way better then during warmup +--- warmup false +--- sent: 86525 msg/sec +--- blocked: 5734 msg/sec +--- completed: 86525 msg/sec +--- received: 86556 msg/sec +--- send ack time: mean: 1109.13 us - 50.00%: 1103.00 us - 90.00%: 1447.00 us - 99.00%: 1687.00 us - 99.90%: 5791.00 us - 99.99%: 5983.00 us - max: 5983.00 us +--- transfer time: mean: 4662.94 us - 50.00%: 1679.00 us - 90.00%: 12159.00 us - 99.00%: 14079.00 us - 99.90%: 14527.00 us - 99.99%: 14783.00 us - max: 14783.00 us +# CTRL + C +--- SUMMARY +--- result: success +--- total sent: 3450389 +--- total blocked: 168863 +--- total completed: 3450389 +--- total received: 3450389 +--- aggregated send time: mean: 1056.09 us - 50.00%: 1003.00 us - 90.00%: 1423.00 us - 99.00%: 1639.00 us - 99.90%: 4287.00 us - 99.99%: 7103.00 us - max: 19583.00 us +--- aggregated transfer time: mean: 18647.51 us - 50.00%: 10751.00 us - 90.00%: 54271.00 us - 99.00%: 84991.00 us - 99.90%: 90111.00 us - 99.99%: 93183.00 us - max: 94207.00 us +``` +Some notes on the results: +- we now have a reasonable `blocked/sent` ratio (< ~10%) +- sent rate has improved **ten-fold** if compared to [previous results](#running-it) + +And on the `SUMMARY` statistics: +- `total` counters include measurements collected with `warmup true` +- `aggregated` latencies **don't** include measurements collected with `warmup true` + +###How to compare latencies across tests? + +The Console output format isn't designed for easy latency comparisons, however the +`perf` commands expose `--hdr ` parameter to produce a [HDR Histogram](http://hdrhistogram.org/) compatible report that can be opened with different visualizers +eg [Online HdrHistogram Log Analyzer](https://hdrhistogram.github.io/HdrHistogramJSDemo/logparser.html), [HdrHistogramVisualizer](https://github.com/ennerf/HdrHistogramVisualizer) or [HistogramLogAnalyzer](https://github.com/HdrHistogram/HistogramLogAnalyzer). + +> **Note:** +> Any latency collected trace on this guide is going to use [Online HdrHistogram Log Analyzer](https://hdrhistogram.github.io/HdrHistogramJSDemo/logparser.html) +> as HDR Histogram visualizer tool. + +Below is the visualization of the HDR histograms collected while adding to the previous benchmark +```bash +--hdr /tmp/non_durable_queue.hdr +``` +Whole test execution shows tagged latencies, to distinguish `warmup` ones: + +![test](images/test.png) + +Filtering out `warmup` latencies, it looks like + +![hot test](images/hot_test.png) + +Latency results shows that at higher percentiles `transfer` latency is way higher than the `sent` one +(reminder: `sent` it's the time to acknowledge sent messages), probably meaning that some queuing-up is happening on the broker. + +In order to test this theory we switch to **target rate tests**. + +## Case 2: Target Rate Single producer Single consumer over a queue + +`perf client` and `perf producer` tools allow specifying a target rate to schedule producer(s) requests: adding +```bash +--rate +``` +The previous example [last run](#to-our-previous-command) shows that `--max-pending 100` guarantees < 10% blocked/sent messages with +aggregated latencies +```bash +--- aggregated send time: mean: 1056.09 us - 50.00%: 1003.00 us - 90.00%: 1423.00 us - 99.00%: 1639.00 us - 99.90%: 4287.00 us - 99.99%: 7103.00 us - max: 19583.00 us +--- aggregated transfer time: mean: 18647.51 us - 50.00%: 10751.00 us - 90.00%: 54271.00 us - 99.00%: 84991.00 us - 99.90%: 90111.00 us - 99.99%: 93183.00 us - max: 94207.00 us +``` +We would like to lower `transfer time` sub-millisecond; let's try +by running a load test with ~30% of the max perceived sent rate, by setting: +```bash +--rate 30000 --hdr /tmp/30K.hdr +``` +The whole command is then: +```bash +$ ./artemis perf client --rate 30000 --hdr /tmp/30K.hdr --warmup 20 --max-pending 100 --show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 queue://TEST_QUEUE +# ... after 20 warmup seconds ... +--- warmup false +--- sent: 30302 msg/sec +--- blocked: 0 msg/sec +--- completed: 30302 msg/sec +--- received: 30303 msg/sec +--- send delay time: mean: 24.20 us - 50.00%: 21.00 us - 90.00%: 54.00 us - 99.00%: 72.00 us - 99.90%: 233.00 us - 99.99%: 659.00 us - max: 731.00 us +--- send ack time: mean: 150.48 us - 50.00%: 120.00 us - 90.00%: 172.00 us - 99.00%: 1223.00 us - 99.90%: 2543.00 us - 99.99%: 3183.00 us - max: 3247.00 us +--- transfer time: mean: 171.53 us - 50.00%: 135.00 us - 90.00%: 194.00 us - 99.00%: 1407.00 us - 99.90%: 2607.00 us - 99.99%: 3151.00 us - max: 3183.00 us +# CTRL + C +--- SUMMARY +--- result: success +--- total sent: 1216053 +--- total blocked: 845 +--- total completed: 1216053 +--- total received: 1216053 +--- aggregated delay send time: mean: 35.84 us - 50.00%: 20.00 us - 90.00%: 55.00 us - 99.00%: 116.00 us - 99.90%: 3359.00 us - 99.99%: 5503.00 us - max: 6495.00 us +--- aggregated send time: mean: 147.38 us - 50.00%: 117.00 us - 90.00%: 165.00 us - 99.00%: 991.00 us - 99.90%: 4191.00 us - 99.99%: 5695.00 us - max: 7103.00 us +--- aggregated transfer time: mean: 178.48 us - 50.00%: 134.00 us - 90.00%: 188.00 us - 99.00%: 1359.00 us - 99.90%: 5471.00 us - 99.99%: 8831.00 us - max: 12799.00 us +``` +We've now achieved sub-millisecond `transfer` latencies until `90.00 pencentile`. +Opening `/tmp/30K.hdr` makes easier to see it: + +![test](images/30K.png) + +Now `send` and `transfer` time looks quite similar and there's no sign of queueing, but... +### What `delay send time` means? + +This metric is borrowed from the [Coordinated Omission](http://highscalability.com/blog/2015/10/5/your-load-generator-is-probably-lying-to-you-take-the-red-pi.html) concept, +and it measures the delay of producer(s) while trying to send messages at the requested rate. + +The source of such delay could be: + +- slow responding broker: the load generator reached `--max-pending` and the expected rate cannot be honored +- client running out of resources (lack of CPU time, GC pauses, etc etc): load generator cannot keep-up with the expected rate because it is just "too fast" for it +- protocol-dependent blocking behaviours: CORE JMS 2 async send can block due to `producerWindowSize` exhaustion + +A sane run of a target rate test should keep `delay send time` under control or investigation actions must be taken +to understand what's the source of the delay. +Let's show it with an example: we've already checked the all-out rate of the broker ie ~90K msg/sec + +By running a `--rate 90000` test under the same conditions, latencies will look as + +![test](images/90K.png) + +It clearly shows that the load generator is getting delayed and cannot keep-up with the expected rate. + +Below is a more complex example involving destinations (auto)generation with "asymmetric" load i.e: the producer number is different from consumer number. + +## Case 3: Target Rate load on 10 durable topics, each with 3 producers and 2 unshared consumers + +The `perf` tool can auto generate destinations using +```bash +--num-destinations +``` +and naming them by using the destination name specified as the seed and an ordered sequence suffix. + +eg +```bash +--num-destinations 3 topic://TOPIC +``` +would generate 3 topics: `TOPIC0`, `TOPIC1`, `TOPIC2`. + +With the default configuration (without specifying `--num-destinations`) it would just create `TOPIC`, without any numerical suffix. + +In order to create a load generation on 10 topics, **each** with 3 producers and 2 unshared consumers: +```bash +--producers 3 --consumers 2 --num-destinations 10 topic://TOPIC +``` + +The whole `perf client` all-out throughput command would be: +```bash +# same as in the previous cases +./artemis perf client --warmup 20 --max-pending 100 --s +how-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 \ +--producers 3 --consumers 2 --num-destinations 10 --durable --persistent topic://DURABLE_TOPIC +# this last part above is new +``` +and it would print... +```bash +javax.jms.IllegalStateException: Cannot create durable subscription - client ID has not been set +``` +Given that the generator is creating [unshared durable Topic subscriptions](https://jakarta.ee/specifications/messaging/2.0/apidocs/javax/jms/session#createDurableConsumer-javax.jms.Topic-java.lang.String-), is it +mandatory to set a ClientID for each connection used. + +The `perf client` tool creates a connection for each consumer by default and auto-generates both ClientIDs +and subscriptions names (as required by the [unshared durable Topic subscriptions API](https://jakarta.ee/specifications/messaging/2.0/apidocs/javax/jms/session#createDurableConsumer-javax.jms.Topic-java.lang.String-)). +ClientID still requires users to specify Client ID prefixes with `--clientID ` and takes care to unsubscribe the consumers on test completion. + +The complete commands now looks like: +```bash +./artemis perf client --warmup 20 --max-pending 100 --show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 \ +--producers 3 --consumers 2 --num-destinations 10 --durable --persistent topic://DURABLE_TOPIC --clientID test_id +# after few seconds +--- warmup false +--- sent: 74842 msg/sec +--- blocked: 2702 msg/sec +--- completed: 74641 msg/sec +--- received: 146412 msg/sec +--- send ack time: mean: 37366.13 us - 50.00%: 37119.00 us - 90.00%: 46079.00 us - 99.00%: 68095.00 us - 99.90%: 84479.00 us - 99.99%: 94719.00 us - max: 95743.00 us +--- transfer time: mean: 44060.66 us - 50.00%: 43263.00 us - 90.00%: 54527.00 us - 99.00%: 75775.00 us - 99.90%: 87551.00 us - 99.99%: 91135.00 us - max: 91135.00 us +# CTRL + C +--- SUMMARY +--- result: success +--- total sent: 2377653 +--- total blocked: 80004 +--- total completed: 2377653 +--- total received: 4755306 +--- aggregated send time: mean: 39423.69 us - 50.00%: 38911.00 us - 90.00%: 49663.00 us - 99.00%: 66047.00 us - 99.90%: 85503.00 us - 99.99%: 101887.00 us - max: 115711.00 us +--- aggregated transfer time: mean: 46216.99 us - 50.00%: 45311.00 us - 90.00%: 57855.00 us - 99.00%: 78335.00 us - 99.90%: 97791.00 us - 99.99%: 113151.00 us - max: 125439.00 us +``` +Results shows that `tranfer time` isn't queuing up, meaning that subscribers are capable to keep-up with the producers: hence a reasonable +rate to test could be ~80% of the perceived `sent` rate ie `--rate 60000`: +```bash +./artemis perf client --warmup 20 --max-pending 100 --show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 \ +--producers 3 --consumers 2 --num-destinations 10 --durable --persistent topic://DURABLE_TOPIC --clientID test_id \ +--rate 60000 +# after many seconds while running +--- warmup false +--- sent: 55211 msg/sec +--- blocked: 2134 msg/sec +--- completed: 54444 msg/sec +--- received: 111622 msg/sec +--- send delay time: mean: 6306710.04 us - 50.00%: 6094847.00 us - 90.00%: 7766015.00 us - 99.00%: 8224767.00 us - 99.90%: 8257535.00 us - 99.99%: 8257535.00 us - max: 8257535.00 us +--- send ack time: mean: 50072.92 us - 50.00%: 50431.00 us - 90.00%: 57855.00 us - 99.00%: 65023.00 us - 99.90%: 71167.00 us - 99.99%: 71679.00 us - max: 71679.00 us +--- transfer time: mean: 63672.92 us - 50.00%: 65535.00 us - 90.00%: 78847.00 us - 99.00%: 86015.00 us - 99.90%: 90623.00 us - 99.99%: 93183.00 us - max: 94719.00 us +# it won't get any better :( +``` +What's wrong with the `send delay time`? +Results show that the load generator cannot keep up with the expected rate and it's accumulating a huge delay +on the expected scheduled load: lets trying fixing it by adding more producers +threads, adding +```bash +--threads +``` +By using two producers threads, the command now looks like: +```bash +./artemis perf client --warmup 20 --max-pending 100 --show-latency --url tcp://localhost:61616?confirmationWindowSize=20000 --consumer-url tcp://localhost:61616 \ +--producers 3 --consumers 2 --num-destinations 10 --durable --persistent topic://DURABLE_TOPIC --clientID test_id \ +--rate 60000 --threads 2 +# after few seconds warming up.... +--- warmup false +--- sent: 59894 msg/sec +--- blocked: 694 msg/sec +--- completed: 58925 msg/sec +--- received: 114857 msg/sec +--- send delay time: mean: 3189.96 us - 50.00%: 277.00 us - 90.00%: 10623.00 us - 99.00%: 35583.00 us - 99.90%: 47871.00 us - 99.99%: 56063.00 us - max: 58367.00 us +--- send ack time: mean: 31500.93 us - 50.00%: 31231.00 us - 90.00%: 48383.00 us - 99.00%: 65535.00 us - 99.90%: 83455.00 us - 99.99%: 95743.00 us - max: 98303.00 us +--- transfer time: mean: 38151.21 us - 50.00%: 37119.00 us - 90.00%: 55807.00 us - 99.00%: 84479.00 us - 99.90%: 104959.00 us - 99.99%: 118271.00 us - max: 121855.00 us +``` +`send delay time` now seems under control, meaning that the load generator need some tuning in order to work at its best. + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 43de66dc8c..06bf38a3f3 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,7 @@ 3.12.4 2.1.2 4.1.74.Final + 2.1.12 5.2.0 3.6.3 @@ -578,6 +579,12 @@ ${jctools.version} + + org.hdrhistogram + HdrHistogram + ${hdrhistogram.version} + + io.netty netty-buffer @@ -1024,6 +1031,7 @@ -Xdiags:verbose --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED + --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED -XDcompilePolicy=simple -Xplugin:ErrorProne -Xep:MissingOverride:ERROR -Xep:NonAtomicVolatileUpdate:ERROR -Xep:SynchronizeOnNonFinalField:ERROR -Xep:StaticQualifiedUsingExpression:ERROR -Xep:WaitNotInLoop:ERROR -XepExcludedPaths:.*/generated-sources/.* @@ -1057,6 +1065,7 @@ -Xdiags:verbose --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED + --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED -XDcompilePolicy=simple -Xplugin:ErrorProne -Xep:MissingOverride:WARN -Xep:NonAtomicVolatileUpdate:ERROR -Xep:SynchronizeOnNonFinalField:ERROR -Xep:StaticQualifiedUsingExpression:ERROR -Xep:WaitNotInLoop:ERROR -XepExcludedPaths:.*/generated-sources/.* -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED @@ -1491,7 +1500,10 @@ org.apache.maven.plugins maven-compiler-plugin - true + true + + --add-exports=java.base/jdk.internal.misc=ALL-UNNAMED +