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}