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

com.rabbitmq.stream.perf.DefaultPerformanceMetrics Maven / Gradle / Ivy

// Copyright (c) 2020-2022 VMware, Inc. or its affiliates.  All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
// For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL,
// please see LICENSE-APACHE2.
//
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
// either express or implied. See the LICENSE file for specific language governing
// rights and limitations of this software.
//
// If you have any questions regarding licensing, please contact us at
// [email protected].
package com.rabbitmq.stream.perf;

import com.codahale.metrics.ConsoleReporter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.distribution.HistogramSnapshot;
import io.micrometer.core.instrument.distribution.HistogramSupport;
import io.micrometer.core.instrument.distribution.ValueAtPercentile;
import io.micrometer.core.instrument.dropwizard.DropwizardMeterRegistry;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.CharacterIterator;
import java.text.SimpleDateFormat;
import java.text.StringCharacterIterator;
import java.time.Duration;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class DefaultPerformanceMetrics implements PerformanceMetrics {

  private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPerformanceMetrics.class);

  private final String metricsPrefix;
  private final CompositeMeterRegistry meterRegistry;
  private final Timer latency, confirmLatency;
  private final boolean summaryFile;
  private final PrintWriter out;
  private final boolean includeByteRates;
  private final Supplier memoryReportSupplier;
  private volatile Closeable closingSequence = () -> {};
  private volatile long lastPublishedCount = 0;
  private volatile long lastConsumedCount = 0;
  private volatile long offset;

  DefaultPerformanceMetrics(
      CompositeMeterRegistry meterRegistry,
      String metricsPrefix,
      boolean summaryFile,
      boolean includeByteRates,
      boolean confirmLatency,
      Supplier memoryReportSupplier,
      PrintWriter out) {
    this.summaryFile = summaryFile;
    this.includeByteRates = includeByteRates;
    this.memoryReportSupplier = memoryReportSupplier;
    this.out = out;
    this.metricsPrefix = metricsPrefix;
    this.meterRegistry = meterRegistry;

    this.latency =
        Timer.builder(metricsPrefix + ".latency")
            .description("message latency")
            .publishPercentiles(0.5, 0.75, 0.95, 0.99)
            .distributionStatisticExpiry(Duration.ofSeconds(1))
            .serviceLevelObjectives()
            .register(meterRegistry);
    if (confirmLatency) {
      this.confirmLatency =
          Timer.builder(metricsPrefix + ".confirm_latency")
              .description("publish confirm latency")
              .publishPercentiles(0.5, 0.75, 0.95, 0.99)
              .distributionStatisticExpiry(Duration.ofSeconds(1))
              .serviceLevelObjectives()
              .register(meterRegistry);
    } else {
      this.confirmLatency = null;
    }
  }

  private long getPublishedCount() {
    return (long) this.meterRegistry.get(metricsName("published")).counter().count();
  }

  private long getConsumedCount() {
    return (long) this.meterRegistry.get(metricsName("consumed")).counter().count();
  }

  @Override
  public void start(String description) throws Exception {
    long startTime = System.nanoTime();

    String metricPublished = metricsName("published");
    String metricProducerConfirmed = metricsName("confirmed");
    String metricConsumed = metricsName("consumed");
    String metricChunkSize = metricsName("chunk_size");
    String metricLatency = metricsName("latency");
    String metricConfirmLatency = metricsName("confirm_latency");
    String metricWrittenBytes = metricsName("written_bytes");
    String metricReadBytes = metricsName("read_bytes");

    Set allMetrics =
        new HashSet<>(
            Arrays.asList(
                metricPublished,
                metricProducerConfirmed,
                metricConsumed,
                metricChunkSize,
                metricLatency));

    if (confirmLatency()) {
      allMetrics.add(metricConfirmLatency);
    }

    Map countersNamesAndLabels = new LinkedHashMap<>();
    countersNamesAndLabels.put(metricPublished, "published");
    countersNamesAndLabels.put(metricProducerConfirmed, "confirmed");
    countersNamesAndLabels.put(metricConsumed, "consumed");

    if (this.includeByteRates) {
      allMetrics.add(metricWrittenBytes);
      allMetrics.add(metricReadBytes);
      countersNamesAndLabels.put(metricWrittenBytes, "written bytes");
      countersNamesAndLabels.put(metricReadBytes, "read bytes");
    }

    ScheduledExecutorService scheduledExecutorService =
        Executors.newSingleThreadScheduledExecutor();

    Closeable summaryFileClosingSequence =
        maybeSetSummaryFile(description, allMetrics, scheduledExecutorService);

    Map counters = new LinkedHashMap<>(countersNamesAndLabels.size());
    countersNamesAndLabels
        .entrySet()
        .forEach(
            entry -> counters.put(entry.getValue(), meterRegistry.get(entry.getKey()).counter()));

    Map formatCounter = new HashMap<>();
    countersNamesAndLabels.entrySet().stream()
        .filter(entry -> !entry.getKey().contains("bytes"))
        .forEach(
            entry -> {
              formatCounter.put(
                  entry.getValue(),
                  (lastValue, currentValue, duration) -> {
                    long rate = 1000 * (currentValue - lastValue) / duration.toMillis();
                    return String.format("%s %d msg/s, ", entry.getValue(), rate);
                  });
            });

    countersNamesAndLabels.entrySet().stream()
        .filter(entry -> entry.getKey().contains("bytes"))
        .forEach(
            entry -> {
              formatCounter.put(
                  entry.getValue(),
                  (lastValue, currentValue, duration) -> {
                    long rate = 1000 * (currentValue - lastValue) / duration.toMillis();
                    return formatByteRate(entry.getValue(), rate) + ", ";
                  });
            });

    HistogramSupport chunkSize = meterRegistry.get(metricChunkSize).summary();
    Function formatChunkSize =
        histogram -> String.format("chunk size %.0f", histogram.takeSnapshot().mean());

    Function convertDuration =
        in -> in instanceof Long ? in.longValue() / 1_000_000 : in.doubleValue() / 1_000_000;
    BiFunction formatLatency =
        (name, timer) -> {
          HistogramSnapshot snapshot = timer.takeSnapshot();

          return String.format(
              name + " median/75th/95th/99th %.0f/%.0f/%.0f/%.0f ms",
              convertDuration.apply(percentile(snapshot, 0.5).value()),
              convertDuration.apply(percentile(snapshot, 0.75).value()),
              convertDuration.apply(percentile(snapshot, 0.95).value()),
              convertDuration.apply(percentile(snapshot, 0.99).value()));
        };

    AtomicInteger reportCount = new AtomicInteger(1);

    AtomicLong lastTick = new AtomicLong(startTime);
    Map lastMetersValues = new ConcurrentHashMap<>(counters.size());
    counters.entrySet().forEach(e -> lastMetersValues.put(e.getKey(), (long) e.getValue().count()));

    ScheduledFuture consoleReportingTask =
        scheduledExecutorService.scheduleAtFixedRate(
            () -> {
              try {
                if (checkActivity()) {
                  long currentTime = System.nanoTime();
                  Duration duration = Duration.ofNanos(currentTime - lastTick.get());
                  lastTick.set(currentTime);
                  StringBuilder builder = new StringBuilder();
                  builder.append(reportCount.get()).append(", ");
                  counters
                      .entrySet()
                      .forEach(
                          entry -> {
                            String meterName = entry.getKey();
                            Counter counter = entry.getValue();
                            long lastValue = lastMetersValues.get(meterName);
                            long currentValue = (long) counter.count();
                            builder.append(
                                formatCounter
                                    .get(meterName)
                                    .compute(lastValue, currentValue, duration));
                            lastMetersValues.put(meterName, currentValue);
                          });
                  if (confirmLatency()) {
                    builder
                        .append(formatLatency.apply("confirm latency", confirmLatency))
                        .append(", ");
                  }
                  builder.append(formatLatency.apply("latency", latency)).append(", ");
                  builder.append(formatChunkSize.apply(chunkSize));
                  this.out.println(builder);
                  String memoryReport = this.memoryReportSupplier.get();
                  if (!memoryReport.isEmpty()) {
                    this.out.println(memoryReport);
                  }
                }
                reportCount.incrementAndGet();
              } catch (Exception e) {
                LOGGER.warn("Error while computing metrics report: {}", e.getMessage());
              }
            },
            1,
            1,
            TimeUnit.SECONDS);

    this.closingSequence =
        () -> {
          consoleReportingTask.cancel(true);

          summaryFileClosingSequence.close();

          scheduledExecutorService.shutdownNow();

          Duration d = Duration.ofNanos(System.nanoTime() - startTime);
          Duration duration = d.getSeconds() <= 0 ? Duration.ofSeconds(1) : d;

          Function, String> formatMeterSummary =
              entry -> {
                if (entry.getKey().contains("bytes")) {
                  return formatByteRate(
                          entry.getKey(),
                          1000 * (long) entry.getValue().count() / duration.toMillis())
                      + ", ";
                } else {
                  return String.format(
                      "%s %d msg/s, ",
                      entry.getKey(), 1000 * (long) entry.getValue().count() / duration.toMillis());
                }
              };

          BiFunction formatLatencySummary =
              (name, histogram) ->
                  String.format(
                      name + " 95th %.0f ms",
                      convertDuration.apply(percentile(histogram.takeSnapshot(), 0.95).value()));

          StringBuilder builder = new StringBuilder("Summary: ");
          counters.entrySet().forEach(entry -> builder.append(formatMeterSummary.apply(entry)));
          if (confirmLatency()) {
            builder
                .append(formatLatencySummary.apply("confirm latency", confirmLatency))
                .append(", ");
          }
          builder.append(formatLatencySummary.apply("latency", latency)).append(", ");
          builder.append(formatChunkSize.apply(chunkSize));
          this.out.println();
          this.out.println(builder);
        };
  }

  static String formatByteRate(String label, double bytes) {
    // based on
    // https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java
    if (-1000 < bytes && bytes < 1000) {
      return bytes + " B/s";
    }
    CharacterIterator ci = new StringCharacterIterator("kMGTPE");
    while (bytes <= -999_950 || bytes >= 999_950) {
      bytes /= 1000;
      ci.next();
    }
    return String.format("%s %.1f %cB/s", label, bytes / 1000.0, ci.current());
  }

  private Closeable maybeSetSummaryFile(
      String description, Set allMetrics, ScheduledExecutorService scheduledExecutorService)
      throws IOException {
    Closeable summaryFileClosingSequence;
    if (this.summaryFile) {
      String currentFilename = "stream-perf-test-current.txt";
      String finalFilename =
          "stream-perf-test-"
              + new SimpleDateFormat("yyyy-MM-dd-HHmmss").format(new Date())
              + ".txt";
      Path currentFile = Paths.get(currentFilename);
      if (Files.exists(currentFile)) {
        if (!Files.deleteIfExists(Paths.get(currentFilename))) {
          LOGGER.warn("Could not delete file {}", currentFilename);
        }
      }
      OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(currentFilename));
      PrintStream printStream = new PrintStream(outputStream);
      if (description != null && !description.trim().isEmpty()) {
        printStream.println(description);
      }

      DropwizardMeterRegistry dropwizardMeterRegistry =
          this.meterRegistry.getRegistries().stream()
              .filter(r -> r instanceof DropwizardMeterRegistry)
              .map(r -> (DropwizardMeterRegistry) r)
              .findAny()
              .orElseGet(() -> Utils.dropwizardMeterRegistry());

      if (!this.meterRegistry.getRegistries().contains(dropwizardMeterRegistry)) {
        this.meterRegistry.add(dropwizardMeterRegistry);
      }

      ConsoleReporter fileReporter =
          ConsoleReporter.forRegistry(dropwizardMeterRegistry.getDropwizardRegistry())
              .filter((name, metric) -> allMetrics.contains(name))
              .convertRatesTo(TimeUnit.SECONDS)
              .convertDurationsTo(TimeUnit.MILLISECONDS)
              .outputTo(printStream)
              .scheduleOn(scheduledExecutorService)
              .shutdownExecutorOnStop(false)
              .build();
      fileReporter.start(1, TimeUnit.SECONDS);
      summaryFileClosingSequence =
          () -> {
            fileReporter.stop();
            printStream.close();
            Files.move(currentFile, currentFile.resolveSibling(finalFilename));
          };
    } else {
      summaryFileClosingSequence = () -> {};
    }
    return summaryFileClosingSequence;
  }

  boolean checkActivity() {
    long currentPublishedCount = getPublishedCount();
    long currentConsumedCount = getConsumedCount();
    boolean activity =
        this.lastPublishedCount != currentPublishedCount
            || this.lastConsumedCount != currentConsumedCount;
    LOGGER.debug(
        "Activity check: published {} vs {}, consumed {} vs {}, activity {}, offset {}",
        this.lastPublishedCount,
        currentPublishedCount,
        this.lastConsumedCount,
        currentConsumedCount,
        activity,
        this.offset);
    if (activity) {
      this.lastPublishedCount = currentPublishedCount;
      this.lastConsumedCount = currentConsumedCount;
    }
    return activity;
  }

  @Override
  public void latency(long latency, TimeUnit unit) {
    this.latency.record(latency, unit);
  }

  @Override
  public void confirmLatency(long latency, TimeUnit unit) {
    this.confirmLatency.record(latency, unit);
  }

  @Override
  public void offset(long offset) {
    this.offset = offset;
  }

  @Override
  public void close() throws Exception {
    this.closingSequence.close();
  }

  private boolean confirmLatency() {
    return this.confirmLatency != null;
  }

  private interface FormatCallback {

    String compute(long lastValue, long currentValue, Duration duration);
  }

  private String metricsName(String name) {
    return this.metricsPrefix + "." + name;
  }

  private static ValueAtPercentile percentile(HistogramSnapshot snapshot, double expected) {
    for (ValueAtPercentile percentile : snapshot.percentileValues()) {
      if (percentile.percentile() == expected) {
        return percentile;
      }
    }
    return null;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy