io.mats3.localinspect.LocalHtmlInspectForMatsFactoryImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mats-localinspect Show documentation
Show all versions of mats-localinspect Show documentation
Mats^3 tool that can output an embeddable HTML describing a MatsFactory and all its endpoints, as well as "local statistics", gathered using a Mats Interceptor.
The newest version!
package io.mats3.localinspect;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import io.mats3.MatsConfig;
import io.mats3.MatsEndpoint;
import io.mats3.MatsEndpoint.EndpointConfig;
import io.mats3.MatsFactory;
import io.mats3.MatsFactory.FactoryConfig;
import io.mats3.MatsFactory.MatsPlugin;
import io.mats3.MatsInitiator;
import io.mats3.MatsStage;
import io.mats3.MatsStage.StageConfig;
import io.mats3.api.intercept.MatsInitiateInterceptor;
import io.mats3.api.intercept.MatsOutgoingMessage.MessageType;
import io.mats3.api.intercept.MatsStageInterceptor;
import io.mats3.api.intercept.MatsStageInterceptor.StageCompletedContext.StageProcessResult;
import io.mats3.localinspect.LocalStatsMatsInterceptor.EndpointStats;
import io.mats3.localinspect.LocalStatsMatsInterceptor.IncomingMessageRepresentation;
import io.mats3.localinspect.LocalStatsMatsInterceptor.InitiatorStats;
import io.mats3.localinspect.LocalStatsMatsInterceptor.MessageRepresentation;
import io.mats3.localinspect.LocalStatsMatsInterceptor.OutgoingMessageRepresentation;
import io.mats3.localinspect.LocalStatsMatsInterceptor.StageStats;
import io.mats3.localinspect.LocalStatsMatsInterceptor.StatsSnapshot;
/**
* Implementation of {@link LocalHtmlInspectForMatsFactory} - use
* {@link LocalHtmlInspectForMatsFactory#create(MatsFactory)} to get hold of one.
*
* @author Endre Stølsvik 2021-03-25 - http://stolsvik.com/, [email protected]
*/
public class LocalHtmlInspectForMatsFactoryImpl implements LocalHtmlInspectForMatsFactory {
private final MatsFactory _matsFactory;
LocalHtmlInspectForMatsFactoryImpl(MatsFactory matsFactory) {
_matsFactory = matsFactory;
}
/**
* Note: The return from this method is static, and should only be included once per HTML page, no matter how many
* MatsFactories you display.
*/
@Override
public void getStyleSheet(Appendable out) throws IOException {
includeFile(out, "localhtmlinspect.css");
}
/**
* Note: The return from this method is static, and should only be included once per HTML page, no matter how many
* MatsFactories you display.
*/
@Override
public void getJavaScript(Appendable out) throws IOException {
includeFile(out, "localhtmlinspect.js");
}
private static void includeFile(Appendable out, String file) throws IOException {
String filename = LocalHtmlInspectForMatsFactory.class.getPackage().getName().replace('.', '/') + '/' + file;
InputStream is = LocalHtmlInspectForMatsFactory.class.getClassLoader().getResourceAsStream(filename);
if (is == null) {
throw new IllegalStateException("Missing '" + file + "' from ClassLoader.");
}
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
while (true) {
String line = br.readLine();
if (line == null) {
break;
}
out.append(line).append('\n');
}
}
@Override
public void createFactoryReport(Appendable out, boolean includeInitiators,
boolean includeEndpoints, boolean includeStages) throws IOException {
// We do this dynamically, so as to handle late registration of the LocalStatsMatsInterceptor.
LocalStatsMatsInterceptor localStats = _matsFactory.getFactoryConfig()
.getPlugins(LocalStatsMatsInterceptor.class).stream().findFirst().orElse(null);
FactoryConfig config = _matsFactory.getFactoryConfig();
out.append("\n");
out.append("MatsFactory " + config.getName() + "
\n");
out.append(" - Known number of CPUs: " + config.getNumberOfCpus());
out.append(" - Concurrency: " + formatConcurrency(config));
out.append(" - Running: " + config.isRunning());
out.append("\n");
out.append("");
out.append("");
out.append("Name: " + config.getName());
out.append(" - App: " + config.getAppName() + " v." + config.getAppVersion());
out.append(" - Nodename: " + config.getNodename());
out.append(" - Mats3: " + config.getMatsImplementationName()
+ ", v." + config.getMatsImplementationVersion());
out.append(" - Destination prefix: '" + config.getMatsDestinationPrefix() + "'");
out.append(" - Trace key: '" + config.getMatsTraceKey() + "'
\n");
out.append("\n");
out.append("Factory Summary
");
createFactorySummary(out, includeInitiators, includeEndpoints);
out.append("
\n");
out.append((localStats != null
? "Local Statistics collector present in MatsFactory!"
+ " (" + LocalStatsMatsInterceptor.class.getSimpleName() + "
installed)"
: "\n");
out.append(" MatsFactory SystemInformation
");
out.append("");
out.append(" ");
out.append(_matsFactory.getFactoryConfig().getSystemInformation());
out.append(" \n\n");
out.append("
");
// :: Initiators
boolean first = true;
if (includeInitiators) {
for (MatsInitiator initiator : _matsFactory.getInitiators()) {
out.append(first ? "" : "
");
first = false;
createInitiatorReport(out, initiator);
}
}
// :: Endpoints
if (includeEndpoints) {
for (MatsEndpoint, ?> endpoint : _matsFactory.getEndpoints()) {
out.append(first ? "" : "
");
first = false;
createEndpointReport(out, endpoint, includeStages);
}
}
out.append("\n");
}
@Override
public void createFactorySummary(Appendable out, boolean includeInitiators, boolean includeEndpoints)
throws IOException {
if (includeInitiators || includeEndpoints) {
// We do this dynamically, so as to handle late registration of the LocalStatsMatsInterceptor.
LocalStatsMatsInterceptor localStats = _matsFactory.getFactoryConfig()
.getPlugins(LocalStatsMatsInterceptor.class).stream().findFirst().orElse(null);
out.append("");
out.append("");
out.append("Initiator Name / Endpoint Id ");
out.append("type ");
out.append("msgs ");
out.append("samples ");
out.append("avg ");
out.append("median ");
out.append("75% ");
out.append("95% ");
out.append("99.9% ");
out.append("Stages ");
out.append(" ");
if (includeInitiators) {
for (MatsInitiator matsInitiator : _matsFactory.getInitiators()) {
out.append("");
out.append("").append(" ")
.append(matsInitiator.getName()).append("
\n")
.append(" ");
out.append("Initiator ");
if ((localStats != null) && localStats.getInitiatorStats(matsInitiator).isPresent()) {
InitiatorStats stats = localStats.getInitiatorStats(matsInitiator).get();
long sumOutMsgs = stats.getOutgoingMessageCounts().values()
.stream().mapToLong(Long::longValue).sum();
out.append("").append(formatInt(sumOutMsgs)).append(" ");
StatsSnapshot execSnapshot = stats.getTotalExecutionTimeNanos();
out.append("")
.append(formatInt(execSnapshot.getSamples().length)).append(" ");
timingCellForAverage(out, execSnapshot);
timingCell(out, execSnapshot.getMedian());
timingCell(out, execSnapshot.get75thPercentile());
timingCell(out, execSnapshot.get95thPercentile());
timingCell(out, execSnapshot.get999thPercentile());
}
else {
out.append(" ");
}
out.append(" "); // No stages for Initiator.
out.append(" ");
}
}
if (includeEndpoints) {
List> endpoints = _matsFactory.getEndpoints();
for (int i = 0; i < endpoints.size(); i++) {
MatsEndpoint, ?> matsEndpoint = endpoints.get(i);
if (i == (endpoints.size() - 1)) {
out.append("");
}
else {
out.append(" ");
}
boolean subscription = matsEndpoint.getEndpointConfig().isSubscription();
out.append("").append(" ")
.append(subscription ? "" : "")
.append("")
.append(matsEndpoint.getEndpointConfig().getEndpointId()).append("
\n")
.append(subscription ? "" : "")
.append(" ");
out.append("")
.append(subscription ? "" : "")
.append(deduceEndpointType(matsEndpoint))
.append(subscription ? "" : "")
.append(" ");
if ((localStats != null) && localStats.getEndpointStats(matsEndpoint).isPresent()) {
EndpointStats endpointStats = localStats.getEndpointStats(matsEndpoint).get();
long sumOutMsgs = endpointStats.getStagesStats().get(0).getIncomingMessageCounts().values()
.stream().mapToLong(Long::longValue).sum();
out.append("").append(formatInt(sumOutMsgs)).append(" ");
StatsSnapshot execSnapshot = endpointStats.getTotalEndpointProcessingTimeNanos();
out.append("")
.append(formatInt(execSnapshot.getSamples().length)).append(" ");
timingCellForAverage(out, execSnapshot);
timingCell(out, execSnapshot.getMedian());
timingCell(out, execSnapshot.get75thPercentile());
timingCell(out, execSnapshot.get95thPercentile());
timingCell(out, execSnapshot.get999thPercentile());
}
else {
out.append(" ");
}
// :: STAGES
out.append("");
for (MatsStage, ?, ?> matsStage : matsEndpoint.getStages()) {
// :: Time between stages (in front of stage)
if ((localStats != null) && localStats.getStageStats(matsStage).isPresent()) {
StageStats stageStats = localStats.getStageStats(matsStage).get();
Optional betweenSnapshot = stageStats
.getBetweenStagesTimeNanos();
// ?: Do we have Between-stats? (Do not have for initial stage).
if (betweenSnapshot.isPresent()) {
summaryStageTime(out, betweenSnapshot.get());
}
}
out.append("");
// Queue time:
if ((localStats != null) && localStats.getStageStats(matsStage).isPresent()) {
StageStats stageStats = localStats.getStageStats(matsStage).get();
StatsSnapshot queueSnapshot = stageStats.getSpentQueueTimeNanos();
summaryStageTime(out, queueSnapshot);
}
out.append("");
}
else {
out.append("matsStage_")
.append(_matsFactory.getFactoryConfig().getName())
.append("_")
.append(matsStage.getStageConfig().getStageId())
.append("'>");
}
out.append("S:").append(Integer.toString(matsStage.getStageConfig().getStageIndex()))
.append("");
// Processing time:
if ((localStats != null) && localStats.getStageStats(matsStage).isPresent()) {
StageStats stageStats = localStats.getStageStats(matsStage).get();
StatsSnapshot execSnapshot = stageStats.getStageTotalExecutionTimeNanos();
summaryStageTime(out, execSnapshot);
}
out.append("");
}
out.append(" ");
out.append(" ");
}
out.append("");
out.append("Legend: ");
out.append("{queue time}");
out.append("S:1");
out.append("{process time}");
out.append("");
out.append("{time between}");
out.append("");
out.append("...");
out.append("S:2");
out.append("...");
out.append("... (95th pctl)");
out.append("");
out.append("Timings: ");
legendTimingPatch(out, 0);
legendTimingPatch(out, 25);
legendTimingPatch(out, 50);
legendTimingPatch(out, 75);
legendTimingPatch(out, 100);
legendTimingPatch(out, 150);
legendTimingPatch(out, 200);
legendTimingPatch(out, 250);
legendTimingPatch(out, 300);
legendTimingPatch(out, 400);
legendTimingPatch(out, 500);
legendTimingPatch(out, 750);
legendTimingPatch(out, 1000);
legendTimingPatch(out, 1250);
legendTimingPatch(out, 1500);
legendTimingPatch(out, 1750);
legendTimingPatch(out, 2000);
out.append("
\n");
out.append("Notice:"
+ " #1 Pay attention to the {queue time} of the initial stage: It is not included"
+ " in the Endpoint total times."
+ " #2 The {time between} will in practice include the {queue time} of the following"
+ " stage."
+ " #3 The {queue time} is susceptible to time skews between nodes.\n");
out.append(" ");
}
out.append("
");
}
}
void summaryStageTime(Appendable out, StatsSnapshot stats) throws IOException {
out.append(""
+ "" + formatNanos0(stats.get95thPercentile()) + ""
+ "" + formatStats(stats, true) + ""
+ "");
}
void timingCell(Appendable out, double nanos) throws IOException {
out.append("")
.append(formatNanos1(nanos)).append(" ");
}
void timingCellForAverage(Appendable out, StatsSnapshot snapshot) throws IOException {
out.append("")
.append("")
.append(formatNanos1(snapshot.getAverage()))
.append("")
.append(formatStats(snapshot, true)).append("")
.append(" ");
}
void legendTimingPatch(Appendable out, double ms) throws IOException {
out.append(" " + Math.round(ms)
+ " ");
}
@Override
public void createInitiatorReport(Appendable out, MatsInitiator matsInitiator)
throws IOException {
// We do this dynamically, so as to handle late registration of the LocalStatsMatsInterceptor.
LocalStatsMatsInterceptor localStats = _matsFactory.getFactoryConfig()
.getPlugins(LocalStatsMatsInterceptor.class).stream().findFirst().orElse(null);
out.append("\n");
out.append("Initiator " + matsInitiator.getName() + "
\n");
out.append("\n");
out.append("\n");
if (localStats != null) {
Optional initiatorStats_ = localStats.getInitiatorStats(matsInitiator);
if (initiatorStats_.isPresent()) {
InitiatorStats initiatorStats = initiatorStats_.get();
StatsSnapshot stats = initiatorStats.getTotalExecutionTimeNanos();
out.append("Total initiation time: " + formatStats(stats, false) + "
\n");
SortedMap outgoingMessageCounts = initiatorStats
.getOutgoingMessageCounts();
long sumOutMsgs = outgoingMessageCounts.values().stream().mapToLong(Long::longValue).sum();
if (outgoingMessageCounts.isEmpty()) {
out.append("NO outgoing messages!
\n");
}
else if (outgoingMessageCounts.size() == 1) {
out.append("Outgoing messages: \n");
}
else {
out.append("Outgoing messages (" + formatInt(sumOutMsgs) + "):
\n");
}
for (Entry entry : outgoingMessageCounts.entrySet()) {
OutgoingMessageRepresentation msg = entry.getKey();
out.append(" " + formatInt(entry.getValue()) + " x " + formatClass(msg.getMessageClass())
+ " " + msg.getMessageType() + " from initiatorId " + formatIid(msg.getInitiatorId())
+ " to " + formatEpid(msg.getTo()) + "
");
}
}
else {
out.append("— No statistics gathered —\n");
}
}
out.append(" \n");
out.append("\n");
}
@Override
public void createEndpointReport(Appendable out, MatsEndpoint, ?> matsEndpoint, boolean includeStages)
throws IOException {
// We do this dynamically, so as to handle late registration of the LocalStatsMatsInterceptor.
LocalStatsMatsInterceptor localStats = _matsFactory.getFactoryConfig()
.getPlugins(LocalStatsMatsInterceptor.class).stream().findFirst().orElse(null);
EndpointConfig, ?> config = matsEndpoint.getEndpointConfig();
StatsSnapshot totExecSnapshot = null;
EndpointStats endpointStats = null;
if (localStats != null) {
Optional endpointStats_ = localStats.getEndpointStats(matsEndpoint);
if (endpointStats_.isPresent()) {
endpointStats = endpointStats_.get();
totExecSnapshot = endpointStats.getTotalEndpointProcessingTimeNanos();
}
}
// If we have snapshot, and the 99.5% percentile is too high, add the "mats hot" class.
String hot = (totExecSnapshot != null) && (totExecSnapshot.get999thPercentile() > 1000_000_000d)
? " matsli_hot"
: "";
String type = deduceEndpointType(matsEndpoint);
out.append("\n");
out.append("" + type + " " + config.getEndpointId() + "
");
out.append(" - " + formatIoClass("Incoming", config.getIncomingClass()));
out.append(" - " + formatIoClass("Reply", config.getReplyClass()));
out.append(" - " + formatIoClass("State", config.getStateClass()));
out.append(" - Running: " + config.isRunning());
out.append(" - Concurrency: " + formatConcurrency(config) + "\n");
out.append("
");
out.append("")
.append(matsEndpoint.getEndpointConfig().getOrigin().replace(";", " - \n"))
.append("");
out.append("\n");
out.append("\n");
// out.append()("Worst stage duty cycle: ###
\n");
if (endpointStats != null) {
NavigableMap initiatorToTerminatorTimeNanos = endpointStats
.getInitiatorToTerminatorTimeNanos();
if (!initiatorToTerminatorTimeNanos.isEmpty()) {
out.append("From Initiator to Terminator times: (From start of MatsInitiator.initiate(..),"
+ " to reception on initial stage of terminator. Susceptible to time skews if initiated"
+ " on different app.)
\n");
out.append("");
out.append("");
out.append("Initiated from ");
out.append("from ");
out.append("observ ");
out.append("samples ");
out.append("avg ");
out.append("median ");
out.append("75% ");
out.append("95% ");
out.append("99.9% ");
out.append(" ");
out.append("");
for (Entry entry : initiatorToTerminatorTimeNanos
.entrySet()) {
IncomingMessageRepresentation msg = entry.getKey();
StatsSnapshot snapshot = entry.getValue();
out.append("");
out.append("" + formatIid(msg.getInitiatorId())
+ " @ " + formatAppName(msg.getInitiatingAppName()) + " ");
out.append("" + formatMsgType(msg.getMessageType()) + " from "
+ formatEpid(msg.getFromStageId())
+ " @ " + formatAppName(msg.getFromAppName()) + " ");
out.append("")
.append(formatInt(snapshot.getNumObservations())).append(" ");
out.append("")
.append(formatInt(snapshot.getSamples().length)).append(" ");
timingCellForAverage(out, snapshot);
timingCell(out, snapshot.getMedian());
timingCell(out, snapshot.get75thPercentile());
timingCell(out, snapshot.get95thPercentile());
timingCell(out, snapshot.get999thPercentile());
out.append(" ");
}
out.append("
");
out.append("
\n");
}
}
if (totExecSnapshot != null) {
out.append("Total endpoint time: " + formatStats(totExecSnapshot, false) + "
"
+ "(Note: From entry on Initial Stage to REPLY or NONE."
+ " Does not include queue time for Initial Stage!)
\n");
}
out.append(" \n");
if (includeStages) {
for (MatsStage, ?, ?> stage : matsEndpoint.getStages()) {
createStageReport(out, stage);
}
}
out.append("\n");
}
@Override
public void createStageReport(Appendable out, MatsStage, ?, ?> matsStage) throws IOException {
// We do this dynamically, so as to handle late registration of the LocalStatsMatsInterceptor.
LocalStatsMatsInterceptor localStats = _matsFactory.getFactoryConfig()
.getPlugins(LocalStatsMatsInterceptor.class).stream().findFirst().orElse(null);
StageConfig, ?, ?> config = matsStage.getStageConfig();
String anchorId = "matsStage_" + matsStage.getParentEndpoint().getParentFactory().getFactoryConfig().getName()
+ "_" + config.getStageId();
// :: Time between stages
boolean anchroIdPrinted = false;
if (localStats != null) {
Optional stageStats_ = localStats.getStageStats(matsStage);
if (stageStats_.isPresent()) {
StageStats stageStats = stageStats_.get();
Optional stats = stageStats.getBetweenStagesTimeNanos();
// ?: Do we have Between-stats? (Do not have for initial stage).
if (stats.isPresent()) {
out.append("Time between: ")
.append(formatStats(stats.get(), false)).append("\n");
anchroIdPrinted = true;
}
}
}
StatsSnapshot totExecSnapshot = null;
StageStats stageStats = null;
if (localStats != null) {
Optional stageStats_ = localStats.getStageStats(matsStage);
if (stageStats_.isPresent()) {
stageStats = stageStats_.get();
totExecSnapshot = stageStats.getStageTotalExecutionTimeNanos();
}
}
// If we have snapshot, and the 99.5% percentile is too high, add the "mats hot" class.
String hot = (totExecSnapshot != null) && (totExecSnapshot.get999thPercentile() > 500_000_000d)
? " matsli_hot"
: "";
out.append("\n");
out.append("Stage " + config.getStageId() + "
\n");
out.append(" - Incoming: " + config.getIncomingClass().getSimpleName() + "
\n");
out.append(" - Running: " + config.isRunning());
out.append(" - Concurrency: " + formatConcurrency(config) + "\n");
out.append(" - Running stage processors: " + config.getRunningStageProcessors() + "\n");
out.append("
");
out.append("")
.append(matsStage.getStageConfig().getOrigin().replace(";", " - \n"))
.append("");
out.append("");
out.append("\n");
// out.append()("Duty cycle: ###
\n");
// out.append()("Oldest reported 'check-in' for stage procs: ### seconds ago."
// + " Stuck stage procs: ###
\n");
if ((stageStats != null) && (totExecSnapshot != null)) {
boolean initialStage = config.getStageIndex() == 0;
out.append("Queue time: " + formatStats(stageStats.getSpentQueueTimeNanos(), false)
+ " (susceptible to time skews between nodes)
\n");
Map incomingMessageCounts = stageStats.getIncomingMessageCounts();
if (incomingMessageCounts.isEmpty()) {
out.append("NO incoming messages!\n");
}
else if (incomingMessageCounts.size() == 1) {
out.append("Incoming messages: ");
Entry entry = incomingMessageCounts.entrySet().iterator().next();
IncomingMessageRepresentation msg = entry.getKey();
out.append(formatInt(entry.getValue()) + " x " + formatMsgType(msg.getMessageType())
+ " from " + formatEpid(msg.getFromStageId()) + " @ "
+ formatAppName(msg.getFromAppName()) + formatInit(msg) + "
");
}
else {
out.append(""); // to have the buttons and summary+details in same container
out.append("Incoming messages (" + formatInt(totExecSnapshot.getNumObservations()) + "):"
+ " Details - click for summary"
+ " Summary - click for details ("
+ incomingMessageCounts.size() + ")"
+ "
\n");
out.append("");
Map summer = new TreeMap<>();
for (Entry entry : incomingMessageCounts.entrySet()) {
IncomingMessageRepresentation msg = entry.getKey();
// Different handling whether InitialStage or any other stage.
String key = msg.getMessageType() + "#"
+ (initialStage ? msg.getInitiatingAppName() : msg.getFromStageId());
AtomicLong count = summer.computeIfAbsent(key, s -> new AtomicLong());
count.addAndGet(entry.getValue());
}
for (Entry entry : summer.entrySet()) {
int hash = entry.getKey().indexOf('#');
String messageType = entry.getKey().substring(0, hash);
String fromOrInitiatingApp = entry.getKey().substring(hash + 1);
out.append(formatInt(entry.getValue().get())).append(" x ").append(formatMsgType(messageType));
// Different handling whether InitialStage or any other stage.
if (initialStage) {
out.append(", flows initiated by ").append(formatAppName(fromOrInitiatingApp));
}
else {
out.append(" from stageId ").append(formatEpid(fromOrInitiatingApp));
}
out.append("
\n");
}
out.append(" \n");
out.append("");
for (Entry entry : incomingMessageCounts.entrySet()) {
IncomingMessageRepresentation msg = entry.getKey();
out.append(formatInt(entry.getValue()) + " x " + formatMsgType(msg.getMessageType())
+ " from " + formatEpid(msg.getFromStageId()) + " @ "
+ formatAppName(msg.getFromAppName()) + formatInit(msg) + "
");
}
out.append(" \n");
}
out.append("Total stage time: " + formatStats(totExecSnapshot, false) + "
\n");
// :: ProcessingResults
SortedMap processResultCounts = stageStats.getProcessResultCounts();
if (processResultCounts.isEmpty()) {
out.append("NO processing results!
\n");
}
else {
out.append("Processing results: \n");
boolean first = true;
for (Entry entry : processResultCounts.entrySet()) {
out.append(first ? "" : ", ");
first = false;
StageProcessResult stageProcessResult = entry.getKey();
out.append(formatInt(entry.getValue()) + " x " + formatMsgType(stageProcessResult));
}
out.append("
\n");
}
// :: Outgoing messages
SortedMap outgoingMessageCounts = stageStats
.getOutgoingMessageCounts();
long sumOutMsgs = outgoingMessageCounts.values().stream().mapToLong(Long::longValue).sum();
if (outgoingMessageCounts.isEmpty()) {
out.append("NO outgoing messages!
\n");
}
else if (outgoingMessageCounts.size() == 1) {
out.append("Outgoing messages: ");
Entry entry = outgoingMessageCounts.entrySet().iterator().next();
OutgoingMessageRepresentation msg = entry.getKey();
out.append(formatInt(entry.getValue())
+ " x " + formatClass(msg.getMessageClass())
+ " " + formatMsgType(msg.getMessageType()) + " to " + formatEpid(msg.getTo())
+ formatInit(msg) + "
");
}
else {
out.append(""); // to have the buttons and summary+details in same container
out.append("Outgoing messages (" + formatInt(sumOutMsgs) + "):"
+ " Details - click for summary"
+ " Summary - click for details ("
+ outgoingMessageCounts.size() + ")"
+ "
\n");
out.append("");
Map summer = new TreeMap<>();
// :: Outgoing Messages Summary: Calculate ..
for (Entry entry : outgoingMessageCounts.entrySet()) {
OutgoingMessageRepresentation msg = entry.getKey();
// Different handling whether REPLY/REPLY_SUBSCRIPTION or any other
boolean replyMsg = (msg.getMessageType() == MessageType.REPLY)
|| (msg.getMessageType() == MessageType.REPLY_SUBSCRIPTION);
String messageClassName = msg.getMessageClass() == null
? "null"
: msg.getMessageClass().getSimpleName();
String key = msg.getMessageType() + "#" + messageClassName + "#" +
(replyMsg ? msg.getInitiatingAppName() : msg.getTo());
AtomicLong count = summer.computeIfAbsent(key, s -> new AtomicLong());
count.addAndGet(entry.getValue());
}
// .. then output summary
for (Entry entry : summer.entrySet()) {
int hashIdx1 = entry.getKey().indexOf('#');
int hashIdx2 = entry.getKey().indexOf('#', hashIdx1 + 1);
String messageType = entry.getKey().substring(0, hashIdx1);
boolean isReplyMsg = "REPLY".equals(messageType);
String messageClass = entry.getKey().substring(hashIdx1 + 1, hashIdx2);
String toOrInitiatingApp = entry.getKey().substring(hashIdx2 + 1);
// Different handling whether REPLY or any other
out.append(formatInt(entry.getValue().get())).append(" x ").append(formatClass(messageClass))
.append(' ').append(formatMsgType(messageType));
if (isReplyMsg) {
out.append(", flows initiated by ").append(formatAppName(toOrInitiatingApp));
}
else {
out.append(" to ").append(formatEpid(toOrInitiatingApp));
}
out.append("
\n");
}
out.append(" ");
// :: Outgoing Messages Details
out.append("");
for (Entry entry : outgoingMessageCounts.entrySet()) {
OutgoingMessageRepresentation msg = entry.getKey();
out.append(formatInt(entry.getValue())
+ " x " + formatClass(msg.getMessageClass())
+ " " + formatMsgType(msg.getMessageType()) + " to " + formatEpid(msg.getTo())
+ formatInit(msg) + "
");
}
out.append(" \n");
}
}
else {
out.append("— No statistics gathered —\n");
}
out.append(" \n");
out.append("\n");
}
// Could be static, but aren't, in case anyone wants to override them.
// NOTE: These are NOT part of any "stable API" promises!
static class RgbaColor {
private final int r;
private final int g;
private final int b;
private final double a;
public RgbaColor(int r, int g, int b, double a) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
RgbaColor interpolate(RgbaColor to, double blendTo) {
if ((blendTo > 1.0) || (blendTo < 0.0)) {
throw new IllegalArgumentException("Blend must be [0, 1], not '" + blendTo + "'.");
}
double inverseBlend = 1 - blendTo;
final int newR = (int) Math.round((this.r * inverseBlend) + (to.r * blendTo));
final int newG = (int) Math.round((this.g * inverseBlend) + (to.g * blendTo));
final int newB = (int) Math.round((this.b * inverseBlend) + (to.b * blendTo));
final double newA = this.a * inverseBlend + to.a * blendTo;
return new RgbaColor(newR, newG, newB, newA);
}
RgbaColor interpolate(RgbaColor to, double rangeFrom, double rangeTo, double value) {
if ((value > rangeTo) || (value < rangeFrom)) {
throw new IllegalArgumentException("value must be in range [" + rangeFrom + "," + rangeTo + "], not '"
+ value + "'");
}
double rangeSpan = rangeTo - rangeFrom;
double valueInRange = value - rangeFrom;
double blend = valueInRange / rangeSpan;
return interpolate(to, blend);
}
String toCss() {
return "rgba(" + r + "," + g + "," + b + "," + (Math.round(a * 1000) / 1000d);
}
}
RgbaColor ms0 = new RgbaColor(128, 255, 128, 1);
// 100 ms is about the threshold for perception of instantaneous.
// E.g. https://www.pubnub.com/blog/how-fast-is-realtime-human-perception-and-technology/
// As pointed out, in a continuous information setting, e.g. video, this is reduced to 13ms.
RgbaColor ms100 = new RgbaColor(0, 192, 0, 1);
RgbaColor ms250 = new RgbaColor(0, 128, 192, 1);
RgbaColor ms500 = new RgbaColor(0, 64, 255, 1);
RgbaColor ms1000 = new RgbaColor(255, 0, 192, 1);
RgbaColor ms2000 = new RgbaColor(255, 0, 0, 1);
RgbaColor colorForMs(double ms) {
RgbaColor color;
if (ms < 0) {
// -> Handle negative timings, which must be due to time skews, or 2xstd.dev. calculations.
color = ms0;
}
else if (ms < 100) {
color = ms0.interpolate(ms100, 0, 100, ms);
}
else if (ms < 250) {
color = ms100.interpolate(ms250, 100, 250, ms);
}
else if (ms < 500) {
color = ms250.interpolate(ms500, 250, 500, ms);
}
else if (ms < 1000) {
color = ms500.interpolate(ms1000, 500, 1000, ms);
}
else if (ms < 2000) {
color = ms1000.interpolate(ms2000, 1000, 2000, ms);
}
else {
color = ms2000;
}
return color.interpolate(new RgbaColor(255, 255, 255, 1), 0.5d);
}
RgbaColor colorForNanos(double nanos) {
return colorForMs(nanos / 1_000_000d);
}
String deduceEndpointType(MatsEndpoint, ?> matsEndpoint) {
EndpointConfig, ?> config = matsEndpoint.getEndpointConfig();
String type = config.getReplyClass() == void.class ? "Terminator" : "Endpoint";
if ((matsEndpoint.getStages().size() == 1) && (config.getReplyClass() != void.class)) {
type = "Single " + type;
}
if (matsEndpoint.getStages().size() > 1) {
type = matsEndpoint.getStages().size() + "-Stage " + type;
}
if (config.isSubscription()) {
type = "Subscription " + type;
}
return type;
}
String formatIid(String iid) {
return "" + iid + "";
}
String formatEpid(String epid) {
return "" + epid + "";
}
String formatAppName(String appName) {
return "" + appName + "";
}
String formatMsgType(Object messageType) {
return "" + messageType.toString() + "";
}
String formatInit(MessageRepresentation msg) {
return " — init:" + formatIid(msg.getInitiatorId()) + " @ "
+ formatAppName(msg.getInitiatingAppName()) + "";
}
String formatIoClass(String what, Class> type) throws IOException {
boolean isVoid = type == Void.TYPE;
return (isVoid ? "" : "")
+ "" + what + ": " + formatClass(type)
+ (isVoid ? "" : "") + "\n";
}
String formatClass(Class> type) {
if (type == null) {
return "null
";
}
return "" + type.getSimpleName() + "
";
}
String formatClass(String type) {
if ((type == null) || ("null".equals(type))) {
return "null
";
}
return "" + type + "
";
}
String formatConcurrency(MatsConfig config) {
return config.getConcurrency() + (config.isConcurrencyDefault() ? " (inherited)"
: " (explicitly set)");
}
String formatStats(StatsSnapshot snapshot, boolean tooltipStyle) {
double sd = snapshot.getStdDev();
double avg = snapshot.getAverage();
return "avg:" + colorAndFormatNanos(avg)
+ " sd:" + formatNanos(sd)
+ " — 50%:" + colorAndFormatNanos(snapshot.getMedian())
+ ", 75%:" + colorAndFormatNanos(snapshot.get75thPercentile())
+ ", 95%:" + colorAndFormatNanos(snapshot.get95thPercentile())
+ ", 98%:" + colorAndFormatNanos(snapshot.get98thPercentile())
+ ", 99%:" + colorAndFormatNanos(snapshot.get99thPercentile())
+ ", 99.9%:" + colorAndFormatNanos(snapshot.get999thPercentile())
+ ", max:" + colorAndFormatNanos(snapshot.getMax())
+ " - min:" + formatNanos(snapshot.getMin())
+ (tooltipStyle ? "
\n" : " — ")
+ "number of samples: " + formatInt(snapshot.getSamples().length)
+ ", out of observations:" + formatInt(snapshot.getNumObservations()) + "";
}
String colorAndFormatNanos(double nanos) {
return "" + formatNanos(nanos) + "";
}
static final DecimalFormatSymbols NF_SYMBOLS;
static final DecimalFormat NF_INTEGER;
static final DecimalFormat NF_0_DECIMALS;
static final DecimalFormat NF_1_DECIMALS;
static final DecimalFormat NF_2_DECIMALS;
static final DecimalFormat NF_3_DECIMALS;
static {
NF_SYMBOLS = new DecimalFormatSymbols(Locale.US);
NF_SYMBOLS.setDecimalSeparator('.');
NF_SYMBOLS.setGroupingSeparator('\u202f');
NF_INTEGER = new DecimalFormat("#,##0");
NF_INTEGER.setMaximumFractionDigits(0);
NF_INTEGER.setDecimalFormatSymbols(NF_SYMBOLS);
NF_0_DECIMALS = new DecimalFormat("#,##0");
NF_0_DECIMALS.setMaximumFractionDigits(0);
NF_0_DECIMALS.setDecimalFormatSymbols(NF_SYMBOLS);
NF_1_DECIMALS = new DecimalFormat("#,##0.0");
NF_1_DECIMALS.setMaximumFractionDigits(1);
NF_1_DECIMALS.setDecimalFormatSymbols(NF_SYMBOLS);
NF_2_DECIMALS = new DecimalFormat("#,##0.00");
NF_2_DECIMALS.setMaximumFractionDigits(2);
NF_2_DECIMALS.setDecimalFormatSymbols(NF_SYMBOLS);
NF_3_DECIMALS = new DecimalFormat("#,##0.000");
NF_3_DECIMALS.setMaximumFractionDigits(3);
NF_3_DECIMALS.setDecimalFormatSymbols(NF_SYMBOLS);
}
String formatInt(long number) {
return NF_INTEGER.format(number);
}
String formatNanos0(double nanos) {
if (Double.isNaN(nanos)) {
return "NaN";
}
return NF_0_DECIMALS.format(Math.round(nanos / 1_000_000d));
}
String formatNanos1(double nanos) {
if (Double.isNaN(nanos)) {
return "NaN";
}
if (nanos == 0d) {
return "0";
}
return NF_1_DECIMALS.format(Math.round(nanos / 100_000d) / 10d);
}
String formatNanos(double nanos) {
if (Double.isNaN(nanos)) {
return "NaN";
}
if (nanos == 0d) {
return "0";
}
// >=500 ms?
if (nanos >= 1_000_000L * 500) {
// -> Yes, >500ms, so chop off fraction entirely, e.g. 612
return NF_0_DECIMALS.format(Math.round(nanos / 1_000_000d));
}
// >=50 ms?
if (nanos >= 1_000_000L * 50) {
// -> Yes, >50ms, so use 1 decimal, e.g. 61.2
return NF_1_DECIMALS.format(Math.round(nanos / 100_000d) / 10d);
}
// >=5 ms?
if (nanos >= 1_000_000L * 5) {
// -> Yes, >5ms, so use 2 decimal, e.g. 6.12
return NF_2_DECIMALS.format(Math.round(nanos / 10_000d) / 100d);
}
// Negative? (Can happen when we to 'avg - 2 x std.dev', the result becomes negative)
if (nanos < 0) {
// -> Negative, so use three digits
return NF_3_DECIMALS.format(Math.round(nanos / 1_000d) / 1_000d);
}
// E-> <5 ms
// Use 3 decimals, e.g. 0.612
double round = Math.round(nanos / 1_000d) / 1_000d;
// ?: However, did we round to zero?
if (round == 0) {
// -> Yes, round to zero, so show special case
return "~>0";
}
return NF_3_DECIMALS.format(round);
}
}