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}