001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.benchmarking.common.config; 017 018import de.cuioss.tools.logging.CuiLogger; 019import org.openjdk.jmh.results.format.ResultFormatType; 020import org.openjdk.jmh.runner.options.TimeValue; 021 022import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Integration.Jmh; 023import static de.cuioss.benchmarking.common.repository.TokenRepositoryConfig.requireProperty; 024import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.WARN.UNKNOWN_TIME_UNIT; 025 026/** 027 * Modern configuration API for JMH benchmarks. 028 * Provides a fluent builder pattern and immutable configuration objects. 029 * Combines JMH runtime configuration with report generation configuration. 030 * 031 * <p>Example usage: 032 * <pre>{@code 033 * var config = BenchmarkConfiguration.builder() 034 * .withReportConfig(ReportConfiguration.builder() 035 * .withBenchmarkType(BenchmarkType.MICRO) 036 * .withThroughputBenchmarkName("myThroughputTest") 037 * .withLatencyBenchmarkName("myLatencyTest") 038 * .build()) 039 * .withForks(2) 040 * .withThreads(10) 041 * .build(); 042 * 043 * Options jmhOptions = config.toJmhOptions(); 044 * }</pre> 045 * 046 */ 047public record BenchmarkConfiguration( 048ReportConfiguration reportConfig, 049String includePattern, 050int forks, 051int warmupIterations, 052int measurementIterations, 053TimeValue measurementTime, 054TimeValue warmupTime, 055int threads, 056IntegrationConfiguration integrationConfig 057) { 058 059 private static final CuiLogger LOGGER = new CuiLogger(BenchmarkConfiguration.class); 060 061 062 /** 063 * Creates a configuration builder with system properties for JMH settings. 064 * Note: Report configuration must be set explicitly via the builder. 065 * 066 * @return a new builder initialized from system properties 067 */ 068 public static Builder builder() { 069 return new Builder() 070 .withIncludePattern(requireJmhProperty(Jmh.INCLUDE, "JMH include pattern")) 071 .withForks(requireIntProperty(Jmh.FORKS, "JMH fork count")) 072 .withWarmupIterations(requireIntProperty(Jmh.WARMUP_ITERATIONS, "JMH warmup iterations")) 073 .withMeasurementIterations(requireIntProperty(Jmh.MEASUREMENT_ITERATIONS, "JMH measurement iterations")) 074 .withMeasurementTime(parseTimeValue(requireJmhProperty(Jmh.MEASUREMENT_TIME, "JMH measurement time"))) 075 .withWarmupTime(parseTimeValue(requireJmhProperty(Jmh.WARMUP_TIME, "JMH warmup time"))) 076 .withThreads(parseThreadCount(requireJmhProperty(Jmh.THREADS, "JMH thread count"))) 077 ; 078 } 079 080 /** 081 * Creates a default configuration. 082 * 083 * @return a new builder with default values 084 */ 085 public static Builder defaults() { 086 return new Builder(); 087 } 088 089 /** 090 * Creates a builder from this configuration. 091 * 092 * @return a new builder initialized with this configuration's values 093 */ 094 public Builder toBuilder() { 095 var builder = new Builder() 096 .withReportConfig(reportConfig) 097 .withIncludePattern(includePattern) 098 .withForks(forks) 099 .withWarmupIterations(warmupIterations) 100 .withMeasurementIterations(measurementIterations) 101 .withMeasurementTime(measurementTime) 102 .withWarmupTime(warmupTime) 103 .withThreads(threads); 104 105 if (integrationConfig != null) { 106 builder.withIntegrationConfig(integrationConfig); 107 } 108 return builder; 109 } 110 111 112 /** 113 * Checks if this configuration includes integration configuration. 114 * Integration configuration is required for integration benchmarks. 115 * 116 * @return true if integration configuration is present, false otherwise 117 */ 118 public boolean hasIntegrationConfig() { 119 return integrationConfig != null; 120 } 121 122 /** 123 * Convenience methods for accessing nested report configuration. 124 */ 125 public BenchmarkType benchmarkType() { 126 return reportConfig.benchmarkType(); 127 } 128 129 public String throughputBenchmarkName() { 130 return reportConfig.throughputBenchmarkName(); 131 } 132 133 public String latencyBenchmarkName() { 134 return reportConfig.latencyBenchmarkName(); 135 } 136 137 public String resultsDirectory() { 138 return reportConfig.resultsDirectory(); 139 } 140 141 public String resultFile() { 142 return reportConfig.resultFile(); 143 } 144 145 public String projectName() { 146 return reportConfig.projectName(); 147 } 148 149 150 private static TimeValue parseTimeValue(String timeStr) { 151 if (timeStr == null || timeStr.isEmpty()) { 152 return TimeValue.seconds(1); 153 } 154 155 // Check if the last character is a digit (no unit specified) 156 char lastChar = timeStr.charAt(timeStr.length() - 1); 157 if (Character.isDigit(lastChar)) { 158 // No unit specified, assume seconds 159 return TimeValue.seconds(Long.parseLong(timeStr)); 160 } 161 162 // Parse value and unit 163 long value = Long.parseLong(timeStr.substring(0, timeStr.length() - 1)); 164 165 return switch (lastChar) { 166 case 's' -> TimeValue.seconds(value); 167 case 'm' -> TimeValue.minutes(value); 168 case 'h' -> TimeValue.hours(value); 169 default -> { 170 LOGGER.warn(UNKNOWN_TIME_UNIT, lastChar, timeStr); 171 yield TimeValue.seconds(value); 172 } 173 }; 174 } 175 176 private static int parseThreadCount(String threads) { 177 if ("MAX".equalsIgnoreCase(threads)) { 178 return Runtime.getRuntime().availableProcessors(); 179 } 180 if ("HALF".equalsIgnoreCase(threads)) { 181 return Math.max(1, Runtime.getRuntime().availableProcessors() / 2); 182 } 183 try { 184 return Integer.parseInt(threads); 185 } catch (NumberFormatException e) { 186 throw new IllegalArgumentException("JMH thread count must be a valid integer or 'MAX' or 'HALF', but got: %s. Set system property: %s".formatted( 187 threads, Jmh.THREADS)); 188 } 189 } 190 191 private static String requireJmhProperty(String key, String description) { 192 return requireProperty(System.getProperty(key), description, key); 193 } 194 195 private static int requireIntProperty(String key, String description) { 196 String value = requireProperty(System.getProperty(key), description, key); 197 try { 198 return Integer.parseInt(value); 199 } catch (NumberFormatException e) { 200 throw new IllegalArgumentException("%s must be a valid integer, but got: %s. Set system property: %s".formatted( 201 description, value, key)); 202 } 203 } 204 205 /** 206 * Builder for BenchmarkConfiguration. 207 */ 208 public static class Builder { 209 private ReportConfiguration reportConfig; 210 private ReportConfiguration.Builder reportConfigBuilder; 211 private String includePattern; 212 private int forks; 213 private int warmupIterations; 214 private int measurementIterations; 215 private TimeValue measurementTime; 216 private TimeValue warmupTime; 217 private int threads; 218 private IntegrationConfiguration integrationConfig; 219 220 /** 221 * Sets the complete report configuration. 222 */ 223 public Builder withReportConfig(ReportConfiguration config) { 224 this.reportConfig = config; 225 this.reportConfigBuilder = null; // Clear builder if direct config is set 226 return this; 227 } 228 229 /** 230 * Convenience method to set benchmark type for report config. 231 * Creates a report config builder if needed. 232 */ 233 public Builder withBenchmarkType(BenchmarkType type) { 234 ensureReportConfigBuilder(); 235 this.reportConfigBuilder.withBenchmarkType(type); 236 return this; 237 } 238 239 /** 240 * Convenience method to set throughput benchmark name. 241 * Creates a report config builder if needed. 242 */ 243 public Builder withThroughputBenchmarkName(String name) { 244 ensureReportConfigBuilder(); 245 this.reportConfigBuilder.withThroughputBenchmarkName(name); 246 return this; 247 } 248 249 /** 250 * Convenience method to set latency benchmark name. 251 * Creates a report config builder if needed. 252 */ 253 public Builder withLatencyBenchmarkName(String name) { 254 ensureReportConfigBuilder(); 255 this.reportConfigBuilder.withLatencyBenchmarkName(name); 256 return this; 257 } 258 259 /** 260 * Convenience method to set results directory. 261 * Creates a report config builder if needed. 262 */ 263 public Builder withResultsDirectory(String dir) { 264 ensureReportConfigBuilder(); 265 this.reportConfigBuilder.withResultsDirectory(dir); 266 return this; 267 } 268 269 /** 270 * Convenience method to set result file. 271 * Creates a report config builder if needed. 272 */ 273 public Builder withResultFile(String file) { 274 ensureReportConfigBuilder(); 275 this.reportConfigBuilder.withResultFile(file); 276 return this; 277 } 278 279 /** 280 * Convenience method to set result format. 281 * Creates a report config builder if needed. 282 */ 283 public Builder withResultFormat(ResultFormatType format) { 284 ensureReportConfigBuilder(); 285 this.reportConfigBuilder.withResultFormat(format); 286 return this; 287 } 288 289 /** 290 * Convenience method to set project name for dashboard display. 291 * Creates a report config builder if needed. 292 */ 293 public Builder withProjectName(String name) { 294 ensureReportConfigBuilder(); 295 this.reportConfigBuilder.withProjectName(name); 296 return this; 297 } 298 299 private void ensureReportConfigBuilder() { 300 if (reportConfigBuilder == null && reportConfig == null) { 301 reportConfigBuilder = ReportConfiguration.builder(); 302 } else if (reportConfigBuilder == null) { 303 // reportConfig must be non-null here 304 reportConfigBuilder = reportConfig.toBuilder(); 305 reportConfig = null; // Will be rebuilt from builder 306 } 307 } 308 309 public Builder withIncludePattern(String pattern) { 310 this.includePattern = pattern; 311 return this; 312 } 313 314 public Builder withForks(int forks) { 315 this.forks = forks; 316 return this; 317 } 318 319 public Builder withWarmupIterations(int iterations) { 320 this.warmupIterations = iterations; 321 return this; 322 } 323 324 public Builder withMeasurementIterations(int iterations) { 325 this.measurementIterations = iterations; 326 return this; 327 } 328 329 public Builder withMeasurementTime(TimeValue time) { 330 this.measurementTime = time; 331 return this; 332 } 333 334 public Builder withWarmupTime(TimeValue time) { 335 this.warmupTime = time; 336 return this; 337 } 338 339 public Builder withThreads(int threads) { 340 this.threads = threads; 341 return this; 342 } 343 344 /** 345 * Sets the integration configuration for integration benchmarks. 346 * This configuration contains URLs needed for integration testing. 347 */ 348 public Builder withIntegrationConfig(IntegrationConfiguration config) { 349 this.integrationConfig = config; 350 return this; 351 } 352 353 /** 354 * Builds the configuration. 355 * 356 * @return the built configuration 357 * @throws IllegalArgumentException if required fields are not set 358 */ 359 public BenchmarkConfiguration build() { 360 // Build report config if using builder 361 ReportConfiguration finalReportConfig = reportConfig; 362 if (finalReportConfig == null) { 363 if (reportConfigBuilder == null) { 364 throw new IllegalArgumentException("Report configuration must be set"); 365 } 366 finalReportConfig = reportConfigBuilder.build(); 367 } 368 369 return new BenchmarkConfiguration( 370 finalReportConfig, 371 includePattern, 372 forks, 373 warmupIterations, 374 measurementIterations, 375 measurementTime, 376 warmupTime, 377 threads, 378 integrationConfig 379 ); 380 } 381 } 382}