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.report; 017 018import com.google.gson.JsonArray; 019import com.google.gson.JsonElement; 020import com.google.gson.JsonObject; 021import de.cuioss.benchmarking.common.constants.BenchmarkConstants; 022 023import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Metrics.Modes.*; 024import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Metrics.Units.OPS; 025import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Metrics.Units.SUFFIX_OP; 026import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Errors.*; 027import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Grades.*; 028import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.JsonFields.*; 029 030/** 031 * Computes benchmark-specific metrics from JSON results. 032 * <p> 033 * This class is responsible for extracting and calculating domain-specific benchmark metrics: 034 * <ul> 035 * <li>Extracting throughput and latency values from JMH JSON output</li> 036 * <li>Converting units to standard measurements (ops/s, ms/op)</li> 037 * <li>Calculating composite performance scores</li> 038 * <li>Assigning performance grades based on scores</li> 039 * </ul> 040 * <p> 041 * Use this class when you need to process raw JMH benchmark results into meaningful metrics. 042 * For pure statistical computations, use {@link StatisticsCalculator}. 043 * For time-series analysis and trend detection, use {@link TrendDataProcessor}. 044 * 045 * @see StatisticsCalculator for pure statistical computations 046 * @see TrendDataProcessor for trend analysis and historical data processing 047 */ 048public class MetricsComputer { 049 050 private final String throughputBenchmarkName; 051 private final String latencyBenchmarkName; 052 053 public MetricsComputer(String throughputBenchmarkName, String latencyBenchmarkName) { 054 if (throughputBenchmarkName == null || throughputBenchmarkName.isBlank()) { 055 throw new IllegalArgumentException(THROUGHPUT_NAME_REQUIRED); 056 } 057 if (latencyBenchmarkName == null || latencyBenchmarkName.isBlank()) { 058 throw new IllegalArgumentException(LATENCY_NAME_REQUIRED); 059 } 060 this.throughputBenchmarkName = throughputBenchmarkName; 061 this.latencyBenchmarkName = latencyBenchmarkName; 062 } 063 064 public BenchmarkMetrics computeMetrics(JsonArray benchmarks) { 065 if (benchmarks == null || benchmarks.isEmpty()) { 066 throw new IllegalArgumentException(NO_RESULTS_PROVIDED); 067 } 068 069 double throughput = extractThroughput(benchmarks); 070 double latency = extractLatency(benchmarks); 071 double rawPerformanceScore = calculatePerformanceScore(throughput, latency); 072 // Performance score is always stored as rounded value 073 double performanceScore = Math.round(rawPerformanceScore); 074 String performanceGrade = getPerformanceGrade(performanceScore); 075 076 return new BenchmarkMetrics( 077 throughputBenchmarkName, 078 latencyBenchmarkName, 079 throughput, 080 latency, 081 performanceScore, 082 performanceGrade 083 ); 084 } 085 086 private double extractThroughput(JsonArray benchmarks) { 087 for (JsonElement element : benchmarks) { 088 JsonObject benchmark = element.getAsJsonObject(); 089 String benchmarkName = benchmark.get(BENCHMARK).getAsString(); 090 091 if (benchmarkName.contains(throughputBenchmarkName)) { 092 String mode = benchmark.get(MODE).getAsString(); 093 JsonObject primaryMetric = benchmark.getAsJsonObject(PRIMARY_METRIC); 094 double score = primaryMetric.get(SCORE).getAsDouble(); 095 String unit = primaryMetric.get(SCORE_UNIT).getAsString(); 096 097 if (BenchmarkConstants.Metrics.Modes.THROUGHPUT.equals(mode) || unit.contains(OPS)) { 098 return MetricConversionUtil.convertToOpsPerSecond(score, unit); 099 } 100 } 101 } 102 103 throw new IllegalStateException( 104 THROUGHPUT_NOT_FOUND_FORMAT.formatted(throughputBenchmarkName)); 105 } 106 107 private double extractLatency(JsonArray benchmarks) { 108 for (JsonElement element : benchmarks) { 109 JsonObject benchmark = element.getAsJsonObject(); 110 String benchmarkName = benchmark.get(BENCHMARK).getAsString(); 111 112 if (benchmarkName.contains(latencyBenchmarkName)) { 113 String mode = benchmark.get(MODE).getAsString(); 114 JsonObject primaryMetric = benchmark.getAsJsonObject(PRIMARY_METRIC); 115 double score = primaryMetric.get(SCORE).getAsDouble(); 116 String unit = primaryMetric.get(SCORE_UNIT).getAsString(); 117 118 if (AVERAGE_TIME.equals(mode) || SAMPLE.equals(mode) || unit.contains(SUFFIX_OP)) { 119 return MetricConversionUtil.convertToMillisecondsPerOp(score, unit); 120 } 121 } 122 } 123 124 throw new IllegalStateException( 125 LATENCY_NOT_FOUND_FORMAT.formatted(latencyBenchmarkName)); 126 } 127 128 private double calculatePerformanceScore(double throughput, double latency) { 129 double throughputScore = (throughput / 100.0) * 0.5; 130 double latencyScore = (100.0 / latency) * 0.5; 131 return throughputScore + latencyScore; 132 } 133 134 private String getPerformanceGrade(double score) { 135 if (score >= 95) return A_PLUS; 136 if (score >= 90) return A; 137 if (score >= 75) return B; 138 if (score >= 60) return C; 139 if (score >= 40) return D; 140 return F; 141 } 142 143}