All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.mats3.localinspect.LocalHtmlInspectForMatsFactoryImpl Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 0.19.22-2024-11-09
Show 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(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); if (includeInitiators) { for (MatsInitiator matsInitiator : _matsFactory.getInitiators()) { out.append(""); out.append(""); out.append(""); 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(""); StatsSnapshot execSnapshot = stats.getTotalExecutionTimeNanos(); out.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(""); out.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(""); StatsSnapshot execSnapshot = endpointStats.getTotalEndpointProcessingTimeNanos(); out.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(""); out.append(""); } out.append(""); } out.append("
Initiator Name / Endpoint Idtypemsgssamplesavgmedian75%95%99.9%Stages
").append("  ") .append(matsInitiator.getName()).append("
\n") .append("
Initiator").append(formatInt(sumOutMsgs)).append("") .append(formatInt(execSnapshot.getSamples().length)).append("
").append("  ") .append(subscription ? "" : "") .append("") .append(matsEndpoint.getEndpointConfig().getEndpointId()).append("
\n") .append(subscription ? "
" : "") .append("
") .append(subscription ? "" : "") .append(deduceEndpointType(matsEndpoint)) .append(subscription ? "" : "") .append("").append(formatInt(sumOutMsgs)).append("") .append(formatInt(execSnapshot.getSamples().length)).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("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("
"); } } 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(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); for (Entry entry : initiatorToTerminatorTimeNanos .entrySet()) { IncomingMessageRepresentation msg = entry.getKey(); StatsSnapshot snapshot = entry.getValue(); out.append(""); out.append(""); out.append(""); out.append(""); out.append(""); timingCellForAverage(out, snapshot); timingCell(out, snapshot.getMedian()); timingCell(out, snapshot.get75thPercentile()); timingCell(out, snapshot.get95thPercentile()); timingCell(out, snapshot.get999thPercentile()); out.append(""); } out.append("
Initiated fromfromobservsamplesavgmedian75%95%99.9%
" + formatIid(msg.getInitiatorId()) + " @ " + formatAppName(msg.getInitiatingAppName()) + "" + formatMsgType(msg.getMessageType()) + " from " + formatEpid(msg.getFromStageId()) + " @ " + formatAppName(msg.getFromAppName()) + "") .append(formatInt(snapshot.getNumObservations())).append("") .append(formatInt(snapshot.getSamples().length)).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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy