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-2021 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.*;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.dropwizard.DropwizardConfig;
import io.micrometer.core.instrument.dropwizard.DropwizardMeterRegistry;
import io.micrometer.core.instrument.util.HierarchicalNameMapper;
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.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.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 MetricRegistry metricRegistry;
  private final Timer latency;
  private final boolean summaryFile;
  private final PrintWriter out;
  private final boolean includeByteRates;
  private final Supplier memoryReportSupplier;
  private volatile Closeable closingSequence = () -> {};

  DefaultPerformanceMetrics(
      CompositeMeterRegistry meterRegistry,
      String metricsPrefix,
      boolean summaryFile,
      boolean includeByteRates,
      Supplier memoryReportSupplier,
      PrintWriter out) {
    this.summaryFile = summaryFile;
    this.includeByteRates = includeByteRates;
    this.memoryReportSupplier = memoryReportSupplier;
    this.out = out;
    DropwizardConfig dropwizardConfig =
        new DropwizardConfig() {
          @Override
          public String prefix() {
            return "";
          }

          @Override
          public String get(String key) {
            return null;
          }
        };
    this.metricRegistry = new MetricRegistry();
    DropwizardMeterRegistry dropwizardMeterRegistry =
        new DropwizardMeterRegistry(
            dropwizardConfig,
            this.metricRegistry,
            HierarchicalNameMapper.DEFAULT,
            io.micrometer.core.instrument.Clock.SYSTEM) {
          @Override
          protected Double nullGaugeValue() {
            return null;
          }
        };
    meterRegistry.add(dropwizardMeterRegistry);
    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);
  }

  private long getPublishedCount() {
    return this.metricRegistry.getMeters().get("rabbitmqStreamPublished").getCount();
  }

  private long getConsumedCount() {
    return this.metricRegistry.getMeters().get("rabbitmqStreamConsumed").getCount();
  }

  private volatile long lastPublishedCount = 0;
  private volatile long lastConsumedCount = 0;

  @Override
  public void start(String description) throws Exception {
    String metricPublished = "rabbitmqStreamPublished";
    String metricProducerConfirmed = "rabbitmqStreamProducer_confirmed";
    String metricConsumed = "rabbitmqStreamConsumed";
    String metricChunkSize = "rabbitmqStreamChunk_size";
    String metricLatency = "rabbitmqStreamLatency";
    String metricWrittenBytes = "rabbitmqStreamWritten_bytes";
    String metricReadBytes = "rabbitmqStreamRead_bytes";

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

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

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

    ScheduledExecutorService scheduledExecutorService =
        Executors.newSingleThreadScheduledExecutor();

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

    SortedMap registryMeters = metricRegistry.getMeters();

    Map meters = new LinkedHashMap<>(metersNamesAndLabels.size());
    metersNamesAndLabels
        .entrySet()
        .forEach(entry -> meters.put(entry.getValue(), registryMeters.get(entry.getKey())));

    Map> formatMeter = new HashMap<>();
    metersNamesAndLabels.entrySet().stream()
        .filter(entry -> !entry.getKey().contains("bytes"))
        .forEach(
            entry -> {
              formatMeter.put(
                  entry.getValue(),
                  meter -> String.format("%s %.0f msg/s, ", entry.getValue(), meter.getMeanRate()));
            });

    metersNamesAndLabels.entrySet().stream()
        .filter(entry -> entry.getKey().contains("bytes"))
        .forEach(
            entry -> {
              formatMeter.put(
                  entry.getValue(),
                  meter -> formatByteRate(entry.getValue(), meter.getMeanRate()) + ", ");
            });

    Histogram chunkSize = metricRegistry.getHistograms().get(metricChunkSize);
    Function formatChunkSize =
        histogram -> String.format("chunk size %.0f", histogram.getSnapshot().getMean());

    com.codahale.metrics.Timer latency = metricRegistry.getTimers().get(metricLatency);

    Function convertDuration =
        in -> in instanceof Long ? in.longValue() / 1000 : in.doubleValue() / 1000;
    Function formatLatency =
        timer -> {
          Snapshot snapshot = timer.getSnapshot();
          return String.format(
              "latency min/median/75th/95th/99th %.0f/%.0f/%.0f/%.0f/%.0f µs",
              convertDuration.apply(snapshot.getMin()),
              convertDuration.apply(snapshot.getMedian()),
              convertDuration.apply(snapshot.get75thPercentile()),
              convertDuration.apply(snapshot.get95thPercentile()),
              convertDuration.apply(snapshot.get99thPercentile()));
        };

    AtomicInteger reportCount = new AtomicInteger(1);
    ScheduledFuture consoleReportingTask =
        scheduledExecutorService.scheduleAtFixedRate(
            () -> {
              try {
                if (checkActivity()) {
                  StringBuilder builder = new StringBuilder();
                  builder.append(reportCount.get()).append(", ");
                  meters
                      .entrySet()
                      .forEach(
                          entry -> {
                            String meterName = entry.getKey();
                            Meter meter = entry.getValue();
                            builder.append(formatMeter.get(meterName).apply(meter));
                          });
                  builder.append(formatLatency.apply(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 metrics report: {}", e.getMessage());
              }
            },
            1,
            1,
            TimeUnit.SECONDS);

    long start = System.currentTimeMillis();

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

          summaryFileClosingSequence.close();

          scheduledExecutorService.shutdownNow();

          long duration = System.currentTimeMillis() - start;

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

          Function formatLatencySummary =
              histogram ->
                  String.format(
                      "latency 95th %.0f µs",
                      convertDuration.apply(latency.getSnapshot().get95thPercentile()));

          StringBuilder builder = new StringBuilder("Summary: ");
          meters.entrySet().forEach(entry -> builder.append(formatMeterSummary.apply(entry)));
          builder.append(formatLatencySummary.apply(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);
      }

      ConsoleReporter fileReporter =
          ConsoleReporter.forRegistry(metricRegistry)
              .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;
    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 close() throws Exception {
    this.closingSequence.close();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy