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.jfr;
017
018import de.cuioss.tools.logging.CuiLogger;
019import jdk.jfr.consumer.RecordedEvent;
020import jdk.jfr.consumer.RecordingFile;
021
022import java.io.IOException;
023import java.nio.file.Path;
024import java.time.Duration;
025import java.util.*;
026
027import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.ERROR.JFR_VARIANCE_USAGE;
028
029/**
030 * Analyzes JFR recordings to extract operation variance metrics.
031 * <p>
032 * This utility reads JFR files generated during benchmarks and calculates:
033 * <ul>
034 *   <li>Time variance across different operations</li>
035 *   <li>Percentile distributions (P50, P95, P99)</li>
036 *   <li>Coefficient of variation for operation times</li>
037 *   <li>Concurrent load impact on variance</li>
038 * </ul>
039 * <p>
040 * Usage:
041 * <pre>{@code
042 * Path jfrFile = Path.of("target/benchmark-results/benchmark.jfr");
043 * JfrVarianceAnalyzer analyzer = new JfrVarianceAnalyzer();
044 * VarianceReport report = analyzer.analyze(jfrFile);
045 * report.printSummary();
046 * }</pre>
047 */
048public class JfrVarianceAnalyzer {
049
050    private static final CuiLogger LOGGER = new CuiLogger(JfrVarianceAnalyzer.class);
051
052    /**
053     * Analyzes a JFR recording file and generates a variance report.
054     *
055     * @param jfrFile path to the JFR recording file
056     * @return variance analysis report
057     * @throws IOException if the file cannot be read
058     */
059    public VarianceReport analyze(Path jfrFile) throws IOException {
060        VarianceReport report = new VarianceReport();
061
062        try (RecordingFile recordingFile = new RecordingFile(jfrFile)) {
063            while (recordingFile.hasMoreEvents()) {
064                RecordedEvent event = recordingFile.readEvent();
065
066                if ("de.cuioss.benchmark.Operation".equals(event.getEventType().getName())) {
067                    processOperationEvent(event, report);
068                } else if ("de.cuioss.benchmark.OperationStatistics".equals(event.getEventType().getName())) {
069                    processStatisticsEvent(event, report);
070                }
071            }
072        }
073
074        report.computeFinalStatistics();
075        return report;
076    }
077
078    private void processOperationEvent(RecordedEvent event, VarianceReport report) {
079        String benchmarkName = event.getString("benchmarkName");
080        String operationType = event.getString("operationType");
081        Duration duration = event.getDuration();
082        boolean success = event.getBoolean("success");
083        int concurrentOps = event.getInt("concurrentOperations");
084
085        OperationMetrics metrics = report.getOrCreateMetrics(benchmarkName, operationType);
086        metrics.addOperation(duration.toNanos(), success, concurrentOps);
087    }
088
089    private void processStatisticsEvent(RecordedEvent event, VarianceReport report) {
090        String benchmarkName = event.getString("benchmarkName");
091        String operationType = event.getString("operationType");
092
093        StatisticsSnapshot snapshot = new StatisticsSnapshot();
094        snapshot.sampleCount = event.getLong("sampleCount");
095        snapshot.p50Latency = event.getDuration("p50Latency").toNanos();
096        snapshot.p95Latency = event.getDuration("p95Latency").toNanos();
097        snapshot.p99Latency = event.getDuration("p99Latency").toNanos();
098        snapshot.variance = event.getDouble("variance");
099        snapshot.coefficientOfVariation = event.getDouble("coefficientOfVariation");
100        snapshot.concurrentThreads = event.getInt("concurrentThreads");
101
102        OperationMetrics metrics = report.getOrCreateMetrics(benchmarkName, operationType);
103        metrics.addStatisticsSnapshot(snapshot);
104    }
105
106    /**
107     * Variance analysis report containing metrics for all operations.
108     */
109    public static class VarianceReport {
110        private final Map<String, OperationMetrics> operationMetrics = new HashMap<>();
111
112        OperationMetrics getOrCreateMetrics(String benchmarkName, String operationType) {
113            String key = benchmarkName + ":" + operationType;
114            return operationMetrics.computeIfAbsent(key, k -> new OperationMetrics(benchmarkName, operationType));
115        }
116
117        void computeFinalStatistics() {
118            operationMetrics.values().forEach(OperationMetrics::computeStatistics);
119        }
120
121        /**
122         * Prints a summary of the variance analysis to stdout.
123         */
124        // cui-rewrite:disable CuiLogRecordPatternRecipe
125        // This is a CLI analysis tool that outputs formatted reports to console
126        public void printSummary() {
127            LOGGER.info("\n=== JFR Variance Analysis Report ===\n");
128
129            operationMetrics.values().stream()
130                    .sorted(Comparator.comparing(m -> m.benchmarkName))
131                    .forEach(metrics -> {
132                        // Report output - suppressed from LogRecord requirements
133                        LOGGER.info("Benchmark: %s - Operation: %s%n",
134                                metrics.benchmarkName, metrics.operationType);
135                        LOGGER.info("  Total Operations: %s (Success: %s, Failed: %s)%n",
136                                metrics.totalOperations, metrics.successfulOperations, metrics.failedOperations);
137                        LOGGER.info("  Latency (μs) - P50: %s, P95: %s, P99: %s, Max: %s%n",
138                                String.format(Locale.US, "%.2f", metrics.p50Latency / 1000.0),
139                                String.format(Locale.US, "%.2f", metrics.p95Latency / 1000.0),
140                                String.format(Locale.US, "%.2f", metrics.p99Latency / 1000.0),
141                                String.format(Locale.US, "%.2f", metrics.maxLatency / 1000.0));
142                        LOGGER.info("  Variance: %s ns² (StdDev: %s μs)%n",
143                                String.format(Locale.US, "%.2e", metrics.variance),
144                                String.format(Locale.US, "%.2f", metrics.standardDeviation / 1000.0));
145                        LOGGER.info("  Coefficient of Variation: %s%%%n",
146                                String.format(Locale.US, "%.2f", metrics.coefficientOfVariation));
147                        LOGGER.info("  Max Concurrent Operations: %s%n", metrics.maxConcurrentOperations);
148
149                        if (!metrics.statisticsSnapshots.isEmpty()) {
150                            LOGGER.info("  Periodic Statistics:");
151                            LOGGER.info("    Average CV over time: %s%%%n",
152                                    String.format(Locale.US, "%.2f", metrics.averageCV));
153                            LOGGER.info("    CV Range: %s%% - %s%%%n",
154                                    String.format(Locale.US, "%.2f", metrics.minCV),
155                                    String.format(Locale.US, "%.2f", metrics.maxCV));
156                        }
157
158                        LOGGER.info("");
159                    });
160        }
161
162        /**
163         * Returns the collected operation metrics map.
164         */
165        public Map<String, OperationMetrics> getOperationMetrics() {
166            return new HashMap<>(operationMetrics);
167        }
168    }
169
170    /**
171     * Metrics for a specific operation type within a benchmark.
172     */
173    public static class OperationMetrics {
174        final String benchmarkName;
175        final String operationType;
176
177        // Raw operation data
178        final List<Long> operationDurations = new ArrayList<>();
179        long totalOperations = 0;
180        long successfulOperations = 0;
181        long failedOperations = 0;
182        int maxConcurrentOperations = 0;
183
184        // Computed statistics
185        double p50Latency = 0;
186        double p95Latency = 0;
187        double p99Latency = 0;
188        double maxLatency = 0;
189        double variance = 0;
190        double standardDeviation = 0;
191        double coefficientOfVariation = 0;
192
193        // Periodic statistics
194        final List<StatisticsSnapshot> statisticsSnapshots = new ArrayList<>();
195        double averageCV = 0;
196        double minCV = Double.MAX_VALUE;
197        double maxCV = 0;
198
199        OperationMetrics(String benchmarkName, String operationType) {
200            this.benchmarkName = benchmarkName;
201            this.operationType = operationType;
202        }
203
204        void addOperation(long durationNanos, boolean success, int concurrentOps) {
205            operationDurations.add(durationNanos);
206            totalOperations++;
207            if (success) {
208                successfulOperations++;
209            } else {
210                failedOperations++;
211            }
212            maxConcurrentOperations = Math.max(maxConcurrentOperations, concurrentOps);
213        }
214
215        void addStatisticsSnapshot(StatisticsSnapshot snapshot) {
216            statisticsSnapshots.add(snapshot);
217            minCV = Math.min(minCV, snapshot.coefficientOfVariation);
218            maxCV = Math.max(maxCV, snapshot.coefficientOfVariation);
219        }
220
221        void computeStatistics() {
222            if (operationDurations.isEmpty()) {
223                return;
224            }
225
226            // Sort durations for percentile calculation
227            Collections.sort(operationDurations);
228
229            // Calculate percentiles
230            int size = operationDurations.size();
231            p50Latency = operationDurations.get((int) (size * 0.50));
232            p95Latency = operationDurations.get((int) (size * 0.95));
233            p99Latency = operationDurations.get((int) (size * 0.99));
234            maxLatency = operationDurations.get(size - 1);
235
236            // Calculate mean
237            double mean = operationDurations.stream()
238                    .mapToLong(Long::longValue)
239                    .average()
240                    .orElse(0);
241
242            // Calculate variance and standard deviation
243            variance = operationDurations.stream()
244                    .mapToDouble(d -> Math.pow(d - mean, 2))
245                    .average()
246                    .orElse(0);
247
248            standardDeviation = Math.sqrt(variance);
249
250            // Calculate coefficient of variation
251            coefficientOfVariation = mean > 0 ? (standardDeviation / mean * 100) : 0;
252
253            // Calculate average CV from periodic snapshots
254            if (!statisticsSnapshots.isEmpty()) {
255                averageCV = statisticsSnapshots.stream()
256                        .mapToDouble(s -> s.coefficientOfVariation)
257                        .average()
258                        .orElse(0);
259            }
260        }
261    }
262
263    /**
264     * Snapshot of statistics from a periodic event.
265     */
266    static class StatisticsSnapshot {
267        long sampleCount;
268        long p50Latency;
269        long p95Latency;
270        long p99Latency;
271        double variance;
272        double coefficientOfVariation;
273        int concurrentThreads;
274    }
275
276    /**
277     * Main method for command-line usage.
278     */
279    public static void main(String[] args) throws IOException {
280        if (args.length != 1) {
281            LOGGER.error(JFR_VARIANCE_USAGE);
282            System.exit(1);
283        }
284
285        Path jfrFile = Path.of(args[0]);
286        JfrVarianceAnalyzer analyzer = new JfrVarianceAnalyzer();
287        VarianceReport report = analyzer.analyze(jfrFile);
288        report.printSummary();
289    }
290}