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.converter; 017 018import de.cuioss.benchmarking.common.config.BenchmarkType; 019import de.cuioss.benchmarking.common.model.BenchmarkData; 020import de.cuioss.tools.logging.CuiLogger; 021 022import java.io.IOException; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.time.Instant; 026import java.time.ZoneOffset; 027import java.time.format.DateTimeFormatter; 028import java.util.*; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031 032import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.ERROR.FAILED_PARSE_WRK_FILE; 033 034/** 035 * Converts WRK benchmark output to the central BenchmarkData model 036 */ 037@SuppressWarnings("java:S5852") // ok for test-data 038public class WrkBenchmarkConverter implements BenchmarkConverter { 039 040 private static final CuiLogger LOGGER = new CuiLogger(WrkBenchmarkConverter.class); 041 042 // Regex patterns for parsing WRK output 043 private static final Pattern REQUESTS_PER_SEC = Pattern.compile("Requests/sec:\\s+([\\d.]+)"); 044 private static final Pattern LATENCY_STATS = Pattern.compile( 045 "Latency\\s+([\\d.]+)(\\w+)\\s+([\\d.]+)(\\w+)\\s+([\\d.]+)(\\w+)\\s+([\\d.]+)%"); 046 private static final Pattern LATENCY_PERCENTILE = Pattern.compile( 047 "\\s+(\\d+(?:\\.\\d+)?)%\\s+([\\d.]+)(\\w+)"); 048 049 @Override 050 public BenchmarkData convert(Path sourcePath) throws IOException { 051 if (sourcePath.toFile().isDirectory()) { 052 // Convert all WRK output files in directory 053 return convertDirectory(sourcePath); 054 } else { 055 // Convert single WRK output file 056 return convertFile(sourcePath); 057 } 058 } 059 060 @Override 061 public boolean canConvert(Path sourcePath) { 062 if (sourcePath.toFile().isDirectory()) { 063 return sourcePath.getFileName().toString().contains("wrk"); 064 } 065 String fileName = sourcePath.getFileName().toString(); 066 return fileName.contains("wrk") && fileName.endsWith(".txt"); 067 } 068 069 private BenchmarkData convertDirectory(Path dir) throws IOException { 070 List<BenchmarkData.Benchmark> benchmarks = new ArrayList<>(); 071 072 // Process all .txt files in the directory (should only contain WRK results) 073 try (var stream = Files.list(dir)) { 074 stream.filter(p -> p.getFileName().toString().endsWith(".txt")) 075 .forEach(file -> { 076 try { 077 LOGGER.debug("Processing WRK result file: " + file.getFileName()); 078 BenchmarkData.Benchmark benchmark = parseWrkFile(file); 079 if (benchmark != null) { 080 benchmarks.add(benchmark); 081 } 082 } catch (IOException e) { 083 LOGGER.error(e, FAILED_PARSE_WRK_FILE, file); 084 } 085 }); 086 } 087 088 return BenchmarkData.builder() 089 .metadata(createMetadata()) 090 .overview(createOverview(benchmarks)) 091 .benchmarks(benchmarks) 092 .build(); 093 } 094 095 private BenchmarkData convertFile(Path file) throws IOException { 096 BenchmarkData.Benchmark benchmark = parseWrkFile(file); 097 List<BenchmarkData.Benchmark> benchmarks = benchmark != null ? 098 List.of(benchmark) : Collections.emptyList(); 099 100 return BenchmarkData.builder() 101 .metadata(createMetadata()) 102 .overview(createOverview(benchmarks)) 103 .benchmarks(benchmarks) 104 .build(); 105 } 106 107 private BenchmarkData.Benchmark parseWrkFile(Path file) throws IOException { 108 String content = Files.readString(file); 109 String name = extractBenchmarkName(file); 110 111 // Parse requests per second (throughput) 112 double requestsPerSec = 0; 113 Matcher m = REQUESTS_PER_SEC.matcher(content); 114 if (m.find()) { 115 requestsPerSec = Double.parseDouble(m.group(1)); 116 } 117 118 // Parse latency statistics 119 double latencyAvg = 0; 120 double latencyStdev = 0; 121 m = LATENCY_STATS.matcher(content); 122 if (m.find()) { 123 latencyAvg = convertToMs(Double.parseDouble(m.group(1)), m.group(2)); 124 latencyStdev = convertToMs(Double.parseDouble(m.group(3)), m.group(4)); 125 } 126 127 // Parse percentiles 128 Map<String, Double> percentiles = new LinkedHashMap<>(); 129 m = LATENCY_PERCENTILE.matcher(content); 130 while (m.find()) { 131 double percentile = Double.parseDouble(m.group(1)); 132 double value = convertToMs(Double.parseDouble(m.group(2)), m.group(3)); 133 percentiles.put(String.valueOf(percentile), value); 134 } 135 136 // Ensure standard percentiles exist 137 ensureStandardPercentiles(percentiles, latencyAvg, latencyStdev); 138 139 return BenchmarkData.Benchmark.builder() 140 .name(name) 141 .fullName("wrk." + name) 142 .mode("thrpt") 143 .rawScore(requestsPerSec) 144 .score(formatThroughput(requestsPerSec)) 145 .scoreUnit("ops/s") 146 .throughput(formatThroughput(requestsPerSec)) 147 .latency(formatLatency(latencyAvg)) 148 .error(latencyStdev) 149 .variabilityCoefficient(latencyAvg > 0 ? (latencyStdev / latencyAvg * 100) : 0) 150 .confidenceLow(Math.max(0, latencyAvg - latencyStdev)) 151 .confidenceHigh(latencyAvg + latencyStdev) 152 .percentiles(percentiles) 153 .build(); 154 } 155 156 private String extractBenchmarkName(Path file) { 157 String fileName = file.getFileName().toString(); 158 159 // First, try to extract from embedded metadata if available 160 try { 161 List<String> lines = Files.readAllLines(file); 162 for (String line : lines) { 163 if (line.startsWith("benchmark_name: ")) { 164 return line.substring(16).trim(); 165 } 166 // Stop looking after WRK output starts 167 if ("=== WRK OUTPUT ===".equals(line)) { 168 break; 169 } 170 } 171 } catch (IOException e) { 172 LOGGER.debug("Could not read metadata from file: " + file, e); 173 } 174 175 // Fallback to filename-based extraction 176 if (fileName.contains("jwt")) { 177 return "jwtValidation"; 178 } else if (fileName.contains("health-live")) { 179 return "healthLiveCheck"; 180 } else if (fileName.contains("health")) { 181 return "healthCheck"; 182 } 183 return fileName.replace("-results.txt", "").replace("wrk-", "").replace("-output", ""); 184 } 185 186 private double convertToMs(double value, String unit) { 187 return switch (unit.toLowerCase()) { 188 case "us" -> value / 1000.0; 189 case "s" -> value * 1000.0; 190 case "m" -> value * 60000.0; 191 default -> value; // Handles "ms" and any other units as-is 192 }; 193 } 194 195 private void ensureStandardPercentiles(Map<String, Double> percentiles, double avg, double stdev) { 196 String[] standard = {"0.0", "50.0", "90.0", "95.0", "99.0", "99.9", "99.99", "100.0"}; 197 198 for (String p : standard) { 199 if (percentiles.containsKey(p)) { 200 continue; 201 } 202 double percentileValue = Double.parseDouble(p); 203 // Simple estimation based on normal distribution 204 double estimated = estimatePercentile(percentileValue, avg, stdev); 205 percentiles.put(p, estimated); 206 } 207 } 208 209 private double estimatePercentile(double percentile, double avg, double stdev) { 210 if (percentile <= 50) { 211 return Math.max(0, avg - stdev * (50 - percentile) / 50); 212 } else { 213 return avg + stdev * (percentile - 50) / 50 * 2; 214 } 215 } 216 217 private BenchmarkData.Metadata createMetadata() { 218 Instant now = Instant.now(); 219 return BenchmarkData.Metadata.builder() 220 .timestamp(now.toString()) 221 .displayTimestamp(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss 'UTC'") 222 .withZone(ZoneOffset.UTC) 223 .format(now)) 224 .benchmarkType(BenchmarkType.INTEGRATION.getDisplayName()) 225 .reportVersion("2.0") 226 .build(); 227 } 228 229 private BenchmarkData.Overview createOverview(List<BenchmarkData.Benchmark> benchmarks) { 230 // Find JWT benchmark (primary) 231 BenchmarkData.Benchmark primary = benchmarks.stream() 232 .filter(b -> b.getName().contains("jwt") || b.getName().contains("Validation")) 233 .findFirst() 234 .orElse(benchmarks.isEmpty() ? null : benchmarks.getFirst()); 235 236 if (primary == null) { 237 return BenchmarkData.Overview.builder() 238 .throughput("N/A") 239 .latency("N/A") 240 .throughputOpsPerSec(0.0) 241 .latencyMs(0.0) 242 .performanceScore(0) 243 .performanceGrade("F") 244 .performanceGradeClass("grade-f") 245 .build(); 246 } 247 248 double throughput = primary.getRawScore(); 249 double latencyMs = primary.getPercentiles().getOrDefault("50.0", 100.0); 250 int score = calculatePerformanceScore(throughput, latencyMs); 251 String grade = calculatePerformanceGrade(score); 252 253 return BenchmarkData.Overview.builder() 254 .throughput(primary.getThroughput()) 255 .latency(primary.getLatency()) 256 .throughputOpsPerSec(throughput) // Store numeric value used for score calculation 257 .latencyMs(latencyMs) // Store numeric value used for score calculation 258 .throughputBenchmarkName(primary.getName()) 259 .latencyBenchmarkName(primary.getName()) 260 .performanceScore(score) 261 .performanceGrade(grade) 262 .performanceGradeClass("grade-" + grade.toLowerCase()) 263 .build(); 264 } 265 266 private String formatThroughput(double reqPerSec) { 267 if (reqPerSec >= 1000) { 268 return String.format(Locale.US, "%.1fK ops/s", reqPerSec / 1000); 269 } 270 return String.format(Locale.US, "%.0f ops/s", reqPerSec); 271 } 272 273 private String formatLatency(double ms) { 274 if (ms < 1) { 275 return String.format(Locale.US, "%.0fμs", ms * 1000); 276 } 277 return String.format(Locale.US, "%.1fms", ms); 278 } 279 280 private int calculatePerformanceScore(double throughput, double latencyMs) { 281 // Score based on throughput (0-50 points) and latency (0-50 points) 282 int throughputScore = (int) Math.min(50, throughput / 200); 283 int latencyScore = (int) Math.max(0, 50 - latencyMs / 2); 284 return throughputScore + latencyScore; 285 } 286 287 private String calculatePerformanceGrade(int score) { 288 if (score >= 90) return "A"; 289 if (score >= 80) return "B"; 290 if (score >= 70) return "C"; 291 if (score >= 60) return "D"; 292 return "F"; 293 } 294}