2 * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.controller.akka.segjournal;
10 import static org.opendaylight.controller.akka.segjournal.SegmentedFileJournal.STORAGE_MAX_ENTRY_SIZE;
11 import static org.opendaylight.controller.akka.segjournal.SegmentedFileJournal.STORAGE_MAX_ENTRY_SIZE_DEFAULT;
12 import static org.opendaylight.controller.akka.segjournal.SegmentedFileJournal.STORAGE_MAX_SEGMENT_SIZE;
13 import static org.opendaylight.controller.akka.segjournal.SegmentedFileJournal.STORAGE_MAX_SEGMENT_SIZE_DEFAULT;
14 import static org.opendaylight.controller.akka.segjournal.SegmentedFileJournal.STORAGE_MAX_UNFLUSHED_BYTES;
15 import static org.opendaylight.controller.akka.segjournal.SegmentedFileJournal.STORAGE_MEMORY_MAPPED;
17 import com.google.common.base.Stopwatch;
18 import com.google.common.base.Ticker;
19 import com.typesafe.config.Config;
20 import com.typesafe.config.ConfigFactory;
21 import io.atomix.storage.journal.StorageLevel;
23 import java.io.IOException;
24 import java.nio.charset.StandardCharsets;
25 import java.nio.file.Files;
26 import java.util.HashMap;
28 import net.sourceforge.argparse4j.ArgumentParsers;
29 import net.sourceforge.argparse4j.impl.Arguments;
30 import net.sourceforge.argparse4j.inf.ArgumentParser;
31 import net.sourceforge.argparse4j.inf.ArgumentParserException;
33 @SuppressWarnings("RegexpSinglelineJava")
34 final class BenchmarkUtils {
36 static final String PROG_NAME = "segjourlan-benchmark";
38 static final String BENCHMARK_USE_CURRENT = "current";
39 static final String BENCHMARK_NUMBER_OF_MESSAGES = "messages-num";
40 static final String BENCHMARK_PAYLOAD_SIZE = "payload-size";
41 static final String BENCHMARK_PAYLOAD_SIZE_DEFAULT = "10K";
43 static final String CURRENT_CONFIG_RESOURCE = "/initial/factory-akka.conf";
44 static final String CURRENT_CONFIG_PATH = "odl-cluster-data.akka.persistence.journal.segmented-file";
46 private static final String[] BYTE_SFX = {"G", "M", "K"};
47 private static final int[] BYTE_THRESH = {1024 * 1024 * 1024, 1024 * 1024, 1024};
49 record BenchmarkConfig(StorageLevel storage, File workingDir, int maxEntrySize, int maxSegmentSize,
50 int maxUnflushedBytes, int payloadSize, int messagesNum) {
53 private BenchmarkUtils() {
57 static BenchmarkConfig buildConfig(final String[] args) {
58 final var parser = getArgumentParser();
59 final var paramsMap = new HashMap<String, Object>();
61 parser.parseArgs(args, paramsMap);
62 } catch (ArgumentParserException e) {
63 parser.handleError(e);
67 return toConfig(paramsMap);
70 private static ArgumentParser getArgumentParser() {
71 final var parser = ArgumentParsers.newArgumentParser(PROG_NAME).defaultHelp(true);
73 parser.description("Performs asynchronous write to segmented journal, collects and prints variety of metrics");
75 parser.addArgument("--current")
76 .type(Boolean.class).setDefault(Boolean.FALSE)
77 .action(Arguments.storeConst()).setConst(Boolean.TRUE)
78 .dest(BENCHMARK_USE_CURRENT)
79 .help("indicates base configuration to be taken from current cluster configuration, "
80 + "all other arguments excepting 'requests' and 'payload size' will be ignored");
82 parser.addArgument("--memory-mapped")
83 .type(Boolean.class).setDefault(Boolean.FALSE)
84 .action(Arguments.storeConst()).setConst(Boolean.TRUE)
85 .dest(STORAGE_MEMORY_MAPPED)
86 .help("indicates mapping journal segments to memory, otherwise file system is used");
88 parser.addArgument("-e", "--max-entry-size")
89 .type(String.class).setDefault(formatBytes(STORAGE_MAX_ENTRY_SIZE_DEFAULT))
90 .dest(STORAGE_MAX_ENTRY_SIZE)
91 .help("max entry size, bytes format");
93 parser.addArgument("-s", "--max-segment-size")
94 .type(String.class).setDefault(formatBytes(STORAGE_MAX_SEGMENT_SIZE_DEFAULT))
95 .dest(STORAGE_MAX_SEGMENT_SIZE)
96 .help("max segment size, bytes ");
98 parser.addArgument("-u", "--max-unflushed-bytes")
100 .dest(STORAGE_MAX_UNFLUSHED_BYTES)
101 .help("max unflushed bytes, bytes format, "
102 + "if not defined the value is taken from 'max-entry-size'");
104 parser.addArgument("-n", "--messages-num")
105 .type(Integer.class).required(true)
106 .dest(BENCHMARK_NUMBER_OF_MESSAGES)
108 .help("number of messages to write");
110 parser.addArgument("-p", "--payload-size")
111 .type(String.class).setDefault(BENCHMARK_PAYLOAD_SIZE_DEFAULT)
112 .dest(BENCHMARK_PAYLOAD_SIZE)
113 .help("median for request payload size, bytes format supported, "
114 + "actual size is variable 80% to 120% from defined median value");
119 static BenchmarkConfig toConfig(final Map<String, Object> paramsMap) {
120 final var inputConfig = ConfigFactory.parseMap(paramsMap);
121 final var finalConfig = (Boolean) paramsMap.get(BENCHMARK_USE_CURRENT)
122 ? currentConfig().withFallback(inputConfig) : inputConfig;
124 final var benchmarkConfig = new BenchmarkConfig(
125 finalConfig.getBoolean(STORAGE_MEMORY_MAPPED) ? StorageLevel.MAPPED : StorageLevel.DISK,
126 createTempDirectory(),
127 bytes(finalConfig, STORAGE_MAX_ENTRY_SIZE),
128 bytes(finalConfig, STORAGE_MAX_SEGMENT_SIZE),
129 finalConfig.hasPath(STORAGE_MAX_UNFLUSHED_BYTES)
130 ? bytes(finalConfig, STORAGE_MAX_UNFLUSHED_BYTES) : bytes(finalConfig, STORAGE_MAX_ENTRY_SIZE),
131 bytes(finalConfig, BENCHMARK_PAYLOAD_SIZE),
132 finalConfig.getInt(BENCHMARK_NUMBER_OF_MESSAGES)
135 if (benchmarkConfig.payloadSize > benchmarkConfig.maxEntrySize) {
136 printAndExit("payloadSize should be less than maxEntrySize");
138 return benchmarkConfig;
141 private static int bytes(final Config config, final String key) {
142 final var bytesLong = config.getBytes(key);
143 if (bytesLong <= 0 || bytesLong > Integer.MAX_VALUE) {
145 key + " value (" + bytesLong + ") is invalid, expected in range 1 .. " + Integer.MAX_VALUE);
147 return bytesLong.intValue();
150 static Config currentConfig() {
151 try (var in = BenchmarkUtils.class.getResourceAsStream(CURRENT_CONFIG_RESOURCE)) {
152 final var content = new String(in.readAllBytes(), StandardCharsets.UTF_8);
153 final var globalConfig = ConfigFactory.parseString(content);
154 final var currentConfig = globalConfig.getConfig(CURRENT_CONFIG_PATH);
155 System.out.println("Current configuration loaded from " + CURRENT_CONFIG_RESOURCE);
156 return currentConfig;
158 } catch (IOException e) {
159 printAndExit("Error loading current configuration from resource " + CURRENT_CONFIG_RESOURCE, e);
164 private static File createTempDirectory() {
166 return Files.createTempDirectory(PROG_NAME).toFile();
167 } catch (IOException e) {
168 printAndExit("Cannot create temp directory", e);
173 private static void printAndExit(final String message) {
174 printAndExit(message, null);
177 private static void printAndExit(final String message, final Exception exception) {
178 System.err.println(message);
179 if (exception != null) {
180 exception.printStackTrace(System.err);
185 static String formatBytes(int bytes) {
186 for (int i = 0; i < 3; i++) {
187 if (bytes > BYTE_THRESH[i]) {
188 return bytes / BYTE_THRESH[i] + BYTE_SFX[i];
191 return String.valueOf(bytes);
194 static String formatNanos(final double nanos) {
195 return formatNanos(Math.round(nanos));
198 static String formatNanos(final long nanos) {
199 return Stopwatch.createStarted(new Ticker() {
213 static String toMetricId(final String metricKey) {
214 return metricKey.substring(metricKey.lastIndexOf('.') + 1);