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.Gson; 019import com.google.gson.GsonBuilder; 020import de.cuioss.benchmarking.common.config.BenchmarkType; 021import de.cuioss.tools.logging.CuiLogger; 022 023import java.io.IOException; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.time.Instant; 027import java.time.ZoneOffset; 028import java.time.format.DateTimeFormatter; 029import java.util.LinkedHashMap; 030import java.util.Locale; 031import java.util.Map; 032 033import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.*; 034import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Arrows; 035import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Colors.*; 036import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.FileNames; 037import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Labels; 038import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.Messages; 039import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Badge.TrendDirection; 040import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Grades; 041import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.INFO; 042 043/** 044 * Generates shields.io compatible JSON badge files for benchmark metrics. 045 * <p> 046 * This generator creates three types of badges: 047 * <ul> 048 * <li>Performance badge - Shows the current performance grade</li> 049 * <li>Trend badge - Shows the trend direction with percentage change</li> 050 * <li>Last run badge - Shows when the benchmarks were last executed</li> 051 * </ul> 052 * <p> 053 * All badges follow the shields.io JSON endpoint schema for easy integration 054 * with GitHub README files and documentation. 055 */ 056public class BadgeGenerator { 057 058 private static final CuiLogger LOGGER = new CuiLogger(BadgeGenerator.class); 059 private static final DateTimeFormatter DATE_FORMAT = 060 DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC); 061 062 private final Gson gson = new GsonBuilder() 063 .setPrettyPrinting() 064 .create(); 065 066 /** 067 * Generates a performance badge JSON based on the benchmark metrics. 068 * Format: "Grade A (45k ops/s, 0.15ms)" 069 * 070 * @param metrics the current benchmark metrics 071 * @return JSON string for the performance badge 072 */ 073 public String generatePerformanceBadge(BenchmarkMetrics metrics) { 074 Map<String, Object> badge = new LinkedHashMap<>(); 075 badge.put(SCHEMA_VERSION, 1); 076 badge.put(LABEL, Labels.PERFORMANCE); 077 078 String formattedThroughput = formatThroughput(metrics.throughput()); 079 String formattedLatency = formatLatency(metrics.latency()); 080 String message = String.format(Locale.US, "Grade %s (%s ops/s, %sms)", 081 metrics.performanceGrade(), formattedThroughput, formattedLatency); 082 083 badge.put(MESSAGE, message); 084 badge.put(COLOR, getGradeColor(metrics.performanceGrade())); 085 086 return gson.toJson(badge); 087 } 088 089 /** 090 * Generates a trend badge JSON based on the trend metrics. 091 * 092 * @param trendMetrics the calculated trend metrics 093 * @return JSON string for the trend badge 094 */ 095 public String generateTrendBadge(TrendDataProcessor.TrendMetrics trendMetrics) { 096 Map<String, Object> badge = new LinkedHashMap<>(); 097 badge.put(SCHEMA_VERSION, 1); 098 badge.put(LABEL, Labels.TREND); 099 100 String arrow = getTrendArrow(trendMetrics.direction()); 101 String percentage = String.format(Locale.US, "%.1f%%", Math.abs(trendMetrics.changePercentage())); 102 badge.put(MESSAGE, arrow + " " + percentage); 103 badge.put(COLOR, getTrendColor(trendMetrics.direction())); 104 105 return gson.toJson(badge); 106 } 107 108 /** 109 * Generates a last run timestamp badge. 110 * 111 * @param timestamp the benchmark run timestamp 112 * @return JSON string for the last run badge 113 */ 114 public String generateLastRunBadge(Instant timestamp) { 115 Map<String, Object> badge = new LinkedHashMap<>(); 116 badge.put(SCHEMA_VERSION, 1); 117 badge.put(LABEL, Labels.LAST_RUN); 118 badge.put(MESSAGE, DATE_FORMAT.format(timestamp)); 119 badge.put(COLOR, BLUE); 120 121 return gson.toJson(badge); 122 } 123 124 /** 125 * Generates a default trend badge when no historical data is available. 126 * 127 * @return JSON string for the default trend badge 128 */ 129 public String generateDefaultTrendBadge() { 130 Map<String, Object> badge = new LinkedHashMap<>(); 131 badge.put(SCHEMA_VERSION, 1); 132 badge.put(LABEL, Labels.TREND); 133 badge.put(MESSAGE, Messages.NO_HISTORY); 134 badge.put(COLOR, LIGHT_GRAY); 135 136 return gson.toJson(badge); 137 } 138 139 /** 140 * Writes all badge files to the specified directory using default MICRO benchmark type. 141 * Convenience overload that defaults to {@link BenchmarkType#MICRO}. 142 * 143 * @param metrics the current benchmark metrics 144 * @param trendMetrics the trend metrics (can be null) 145 * @param outputDir the output directory path 146 * @throws IOException if writing badge files fails 147 */ 148 public void writeBadgeFiles(BenchmarkMetrics metrics, 149 TrendDataProcessor.TrendMetrics trendMetrics, 150 String outputDir) throws IOException { 151 writeBadgeFiles(metrics, trendMetrics, BenchmarkType.MICRO, outputDir); 152 } 153 154 /** 155 * Writes all badge files to the specified directory. 156 * 157 * @param metrics the current benchmark metrics 158 * @param trendMetrics the trend metrics (can be null) 159 * @param type the benchmark type (MICRO or INTEGRATION) 160 * @param outputDir the output directory path 161 * @throws IOException if writing badge files fails 162 */ 163 public void writeBadgeFiles(BenchmarkMetrics metrics, 164 TrendDataProcessor.TrendMetrics trendMetrics, 165 BenchmarkType type, 166 String outputDir) throws IOException { 167 Path badgesDir = Path.of(outputDir, "badges"); 168 Files.createDirectories(badgesDir); 169 170 // Write performance badge with appropriate file name based on benchmark type 171 String perfBadge = generatePerformanceBadge(metrics); 172 Path perfBadgePath = badgesDir.resolve(type.getPerformanceBadgeFileName()); 173 Files.writeString(perfBadgePath, perfBadge); 174 LOGGER.info(INFO.BADGE_GENERATED, "performance", perfBadgePath); 175 176 // Write trend badge with appropriate file name based on benchmark type 177 String trendBadge = trendMetrics != null 178 ? generateTrendBadge(trendMetrics) 179 : generateDefaultTrendBadge(); 180 Path trendBadgePath = badgesDir.resolve(type.getTrendBadgeFileName()); 181 Files.writeString(trendBadgePath, trendBadge); 182 LOGGER.info(INFO.BADGE_GENERATED, "trend", trendBadgePath); 183 184 // Write last run badge (same for both types) 185 String lastRunBadge = generateLastRunBadge(Instant.now()); 186 Path lastRunBadgePath = badgesDir.resolve(FileNames.LAST_RUN_BADGE_JSON); 187 Files.writeString(lastRunBadgePath, lastRunBadge); 188 LOGGER.info(INFO.BADGE_GENERATED, "last-run", lastRunBadgePath); 189 } 190 191 /** 192 * Determines the color for a performance grade. 193 */ 194 private String getGradeColor(String grade) { 195 return switch (grade) { 196 case Grades.A_PLUS -> BRIGHT_GREEN; 197 case Grades.A -> GREEN; 198 case Grades.B -> YELLOW_GREEN; 199 case Grades.C -> YELLOW; 200 case Grades.D -> ORANGE; 201 case Grades.F -> RED; 202 default -> LIGHT_GRAY; 203 }; 204 } 205 206 /** 207 * Determines the color for a trend direction. 208 */ 209 private String getTrendColor(String direction) { 210 return switch (direction) { 211 case TrendDirection.UP -> GREEN; 212 case TrendDirection.DOWN -> RED; 213 case TrendDirection.STABLE -> BLUE; 214 default -> LIGHT_GRAY; 215 }; 216 } 217 218 /** 219 * Gets the arrow symbol for a trend direction. 220 */ 221 private String getTrendArrow(String direction) { 222 return switch (direction) { 223 case TrendDirection.UP -> Arrows.UP; 224 case TrendDirection.DOWN -> Arrows.DOWN; 225 case TrendDirection.STABLE -> Arrows.RIGHT; 226 default -> Arrows.BULLET; 227 }; 228 } 229 230 /** 231 * Formats throughput value with appropriate unit (k for thousands). 232 * Examples: 500 -> "500", 1500 -> "1.5k", 45000 -> "45k" 233 */ 234 private String formatThroughput(double throughput) { 235 if (throughput >= 1000) { 236 double kThroughput = throughput / 1000.0; 237 if (kThroughput >= 10) { 238 // For values >= 10k, no decimal places 239 return String.format(Locale.US, "%.0fk", kThroughput); 240 } else { 241 // For values < 10k, show one decimal if not .0 242 String formatted = String.format(Locale.US, "%.1fk", kThroughput); 243 return formatted.endsWith(".0k") ? 244 formatted.substring(0, formatted.length() - 3) + "k" : formatted; 245 } 246 } else { 247 // For values < 1000, show as integer 248 return String.format(Locale.US, "%.0f", throughput); 249 } 250 } 251 252 /** 253 * Formats latency value with appropriate precision. 254 * For values < 1ms: 2 decimals, for values >= 1ms: 2 decimals with trailing zeros 255 */ 256 private String formatLatency(double latency) { 257 return String.format(Locale.US, "%.2f", latency); 258 } 259}