30ff871a51ec4f10ad3abaa1cfed4bed6f4f012c
[controller.git] / benchmark / segjournal-benchmark / src / main / java / org / opendaylight / controller / akka / segjournal / BenchmarkUtils.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.controller.akka.segjournal;
9
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;
16
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;
22 import java.io.File;
23 import java.io.IOException;
24 import java.nio.charset.StandardCharsets;
25 import java.nio.file.Files;
26 import java.util.HashMap;
27 import java.util.Map;
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;
32
33 @SuppressWarnings("RegexpSinglelineJava")
34 final class BenchmarkUtils {
35
36     static final String PROG_NAME = "segjourlan-benchmark";
37
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";
42
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";
45
46     private static final String[] BYTE_SFX = {"G", "M", "K"};
47     private static final int[] BYTE_THRESH = {1024 * 1024 * 1024, 1024 * 1024, 1024};
48
49     record BenchmarkConfig(StorageLevel storage, File workingDir, int maxEntrySize, int maxSegmentSize,
50         int maxUnflushedBytes, int payloadSize, int messagesNum) {
51     }
52
53     private BenchmarkUtils() {
54         // utility class
55     }
56
57     static BenchmarkConfig buildConfig(final String[] args) {
58         final var parser = getArgumentParser();
59         final var paramsMap = new HashMap<String, Object>();
60         try {
61             parser.parseArgs(args, paramsMap);
62         } catch (ArgumentParserException e) {
63             parser.handleError(e);
64             System.exit(1);
65             return null;
66         }
67         return toConfig(paramsMap);
68     }
69
70     private static ArgumentParser getArgumentParser() {
71         final var parser = ArgumentParsers.newArgumentParser(PROG_NAME).defaultHelp(true);
72
73         parser.description("Performs asynchronous write to segmented journal, collects and prints variety of metrics");
74
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");
81
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");
87
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");
92
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  ");
97
98         parser.addArgument("-u", "--max-unflushed-bytes")
99             .type(String.class)
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'");
103
104         parser.addArgument("-n", "--messages-num")
105             .type(Integer.class).required(true)
106             .dest(BENCHMARK_NUMBER_OF_MESSAGES)
107             .setDefault(10_000)
108             .help("number of messages to write");
109
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");
115
116         return parser;
117     }
118
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;
123
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)
133         );
134         // validate
135         if (benchmarkConfig.payloadSize > benchmarkConfig.maxEntrySize) {
136             printAndExit("payloadSize should be less than maxEntrySize");
137         }
138         return benchmarkConfig;
139     }
140
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) {
144             printAndExit(
145                 key + " value (" + bytesLong + ") is invalid, expected in range 1 .. " + Integer.MAX_VALUE);
146         }
147         return bytesLong.intValue();
148     }
149
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;
157
158         } catch (IOException e) {
159             printAndExit("Error loading current configuration from resource " + CURRENT_CONFIG_RESOURCE, e);
160             return null;
161         }
162     }
163
164     private static File createTempDirectory() {
165         try {
166             return Files.createTempDirectory(PROG_NAME).toFile();
167         } catch (IOException e) {
168             printAndExit("Cannot create temp directory", e);
169         }
170         return null;
171     }
172
173     private static void printAndExit(final String message) {
174         printAndExit(message, null);
175     }
176
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);
181         }
182         System.exit(1);
183     }
184
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];
189             }
190         }
191         return String.valueOf(bytes);
192     }
193
194     static String formatNanos(final double nanos) {
195         return formatNanos(Math.round(nanos));
196     }
197
198     static String formatNanos(final long nanos) {
199         return Stopwatch.createStarted(new Ticker() {
200             boolean started;
201
202             @Override
203             public long read() {
204                 if (started) {
205                     return nanos;
206                 }
207                 started = true;
208                 return 0;
209             }
210         }).toString();
211     }
212
213     static String toMetricId(final String metricKey) {
214         return metricKey.substring(metricKey.lastIndexOf('.') + 1);
215     }
216 }