diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index a74e525e8..30353352e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,5 +1,5 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages +# Workflow for generating and deploying performance results to GitHub Pages +name: Deploy performance results to Pages on: # Runs on pushes targeting the default branch @@ -22,7 +22,6 @@ concurrency: cancel-in-progress: false jobs: - # Single deploy job since we're just deploying deploy: environment: name: github-pages @@ -31,13 +30,19 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Generate performance page + run: ./gradlew :performance-results-page:generatePerformancePage - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - # Upload entire repository - path: './pages' + path: './performance-results-page/build/site' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/performance-results-page/build.gradle b/performance-results-page/build.gradle new file mode 100644 index 000000000..ea1421e44 --- /dev/null +++ b/performance-results-page/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.1' +} + +tasks.register('generatePerformancePage', JavaExec) { + description = 'Generates the performance results HTML page' + group = 'reporting' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'graphql.performance.page.Main' + args = [ + file("${rootProject.projectDir}/performance-results").absolutePath, + file("${project.projectDir}/build/site").absolutePath + ] +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/Main.java b/performance-results-page/src/main/java/graphql/performance/page/Main.java new file mode 100644 index 000000000..a8960c759 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/Main.java @@ -0,0 +1,76 @@ +package graphql.performance.page; + +import graphql.performance.page.analyzer.BenchmarkAnalyzer; +import graphql.performance.page.generator.HtmlGenerator; +import graphql.performance.page.model.BenchmarkSeries; +import graphql.performance.page.model.ResultFile; +import graphql.performance.page.parser.ResultFileParser; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +public class Main { + + public static void main(String[] args) throws IOException { + if (args.length < 2) { + System.err.println("Usage: Main "); + System.exit(1); + } + + File inputDir = new File(args[0]); + Path outputDir = Path.of(args[1]); + + if (!inputDir.isDirectory()) { + System.err.println("Input directory does not exist: " + inputDir); + System.exit(1); + } + + File[] jsonFiles = inputDir.listFiles((dir, name) -> name.endsWith(".json")); + if (jsonFiles == null || jsonFiles.length == 0) { + System.err.println("No JSON files found in: " + inputDir); + System.exit(1); + } + + System.out.println("Found " + jsonFiles.length + " result files in " + inputDir); + + ResultFileParser parser = new ResultFileParser(); + List resultFiles = new ArrayList<>(); + int errors = 0; + + for (File file : jsonFiles) { + try { + resultFiles.add(parser.parse(file)); + } catch (Exception e) { + System.err.println("Warning: Failed to parse " + file.getName() + ": " + e.getMessage()); + errors++; + } + } + + System.out.println("Parsed " + resultFiles.size() + " files successfully" + (errors > 0 ? " (" + errors + " errors)" : "")); + + BenchmarkAnalyzer analyzer = new BenchmarkAnalyzer(); + Map> grouped = analyzer.analyze(resultFiles); + + System.out.println("Found " + grouped.size() + " benchmark classes:"); + for (Map.Entry> entry : grouped.entrySet()) { + int totalPoints = entry.getValue().stream() + .mapToInt(s -> s.getDataPoints().size()) + .sum(); + System.out.println(" " + entry.getKey() + ": " + entry.getValue().size() + " series, " + totalPoints + " data points"); + } + + Instant earliest = resultFiles.stream().map(ResultFile::getTimestamp).min(Comparator.naturalOrder()).orElse(Instant.now()); + Instant latest = resultFiles.stream().map(ResultFile::getTimestamp).max(Comparator.naturalOrder()).orElse(Instant.now()); + + HtmlGenerator generator = new HtmlGenerator(); + generator.generate(grouped, outputDir, jsonFiles.length, earliest, latest); + + System.out.println("Generated " + outputDir.resolve("index.html")); + } +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/analyzer/BenchmarkAnalyzer.java b/performance-results-page/src/main/java/graphql/performance/page/analyzer/BenchmarkAnalyzer.java new file mode 100644 index 000000000..724eca5f4 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/analyzer/BenchmarkAnalyzer.java @@ -0,0 +1,155 @@ +package graphql.performance.page.analyzer; + +import graphql.performance.page.model.BenchmarkResult; +import graphql.performance.page.model.BenchmarkSeries; +import graphql.performance.page.model.ResultFile; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class BenchmarkAnalyzer { + + /** + * Unit conversion factors to a common base unit. + * For time units: base is nanoseconds. + * For throughput units: base is ops/s. + */ + private static final Map TIME_UNIT_TO_NANOS = Map.of( + "ns/op", 1.0, + "us/op", 1_000.0, + "ms/op", 1_000_000.0, + "s/op", 1_000_000_000.0 + ); + + private static final Map THROUGHPUT_UNIT_TO_OPS_PER_S = Map.of( + "ops/s", 1.0, + "ops/ms", 1_000.0, + "ops/us", 1_000_000.0, + "ops/ns", 1_000_000_000.0 + ); + + /** + * Groups all results from all files into benchmark series, sorted by timestamp. + * Returns a map of benchmarkClassName -> list of series for that class. + */ + public Map> analyze(List files) { + // Sort files by timestamp + files.sort(Comparator.comparing(ResultFile::getTimestamp)); + + // Build series map: seriesKey -> BenchmarkSeries + Map seriesMap = new LinkedHashMap<>(); + + for (ResultFile file : files) { + for (BenchmarkResult result : file.getResults()) { + String key = result.getSeriesKey(); + BenchmarkSeries series = seriesMap.computeIfAbsent(key, k -> + new BenchmarkSeries( + key, + result.getBenchmark(), + result.getBenchmarkClassName(), + result.getBenchmarkMethodName(), + result.getMode(), + result.getParamsString() + ) + ); + series.addDataPoint(new BenchmarkSeries.DataPoint( + file.getTimestamp(), + file.getCommitHash(), + file.getJdkVersion(), + result.getPrimaryMetric().getScore(), + result.getPrimaryMetric().getScoreError(), + result.getPrimaryMetric().getScoreUnit() + )); + } + } + + // Normalize units within each series + for (BenchmarkSeries series : seriesMap.values()) { + normalizeUnits(series); + } + + // Group by benchmark class name, sorted alphabetically + Map> grouped = new TreeMap<>(); + for (BenchmarkSeries series : seriesMap.values()) { + grouped.computeIfAbsent(series.getBenchmarkClassName(), k -> new java.util.ArrayList<>()) + .add(series); + } + + // Sort series within each class: by mode (thrpt first), then by display label + for (List seriesList : grouped.values()) { + seriesList.sort(Comparator + .comparing(BenchmarkSeries::getMode) + .thenComparing(BenchmarkSeries::getDisplayLabel)); + } + + return grouped; + } + + /** + * Normalizes all data points in a series to the most recent unit. + * This handles cases where a benchmark changed units over time (e.g. ns/op -> ms/op). + */ + private void normalizeUnits(BenchmarkSeries series) { + List points = series.getDataPoints(); + if (points.size() < 2) { + return; + } + + // Target unit is the most recent data point's unit + String targetUnit = points.getLast().scoreUnit(); + + // Check if all points already have the same unit + boolean allSame = points.stream().allMatch(dp -> dp.scoreUnit().equals(targetUnit)); + if (allSame) { + return; + } + + // Determine if these are time or throughput units + boolean isTime = TIME_UNIT_TO_NANOS.containsKey(targetUnit); + boolean isThroughput = THROUGHPUT_UNIT_TO_OPS_PER_S.containsKey(targetUnit); + + if (!isTime && !isThroughput) { + // Unknown unit family, skip normalization + return; + } + + // Replace data points with normalized values + for (int i = 0; i < points.size(); i++) { + BenchmarkSeries.DataPoint dp = points.get(i); + if (!dp.scoreUnit().equals(targetUnit)) { + double factor = computeConversionFactor(dp.scoreUnit(), targetUnit, isTime); + if (!Double.isNaN(factor)) { + points.set(i, new BenchmarkSeries.DataPoint( + dp.timestamp(), + dp.commitHash(), + dp.jdkVersion(), + dp.score() * factor, + dp.scoreError() * factor, + targetUnit + )); + } + } + } + } + + private double computeConversionFactor(String fromUnit, String toUnit, boolean isTime) { + if (isTime) { + Double fromFactor = TIME_UNIT_TO_NANOS.get(fromUnit); + Double toFactor = TIME_UNIT_TO_NANOS.get(toUnit); + if (fromFactor == null || toFactor == null) { + return Double.NaN; + } + return fromFactor / toFactor; + } else { + Double fromFactor = THROUGHPUT_UNIT_TO_OPS_PER_S.get(fromUnit); + Double toFactor = THROUGHPUT_UNIT_TO_OPS_PER_S.get(toUnit); + if (fromFactor == null || toFactor == null) { + return Double.NaN; + } + return fromFactor / toFactor; + } + } +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/generator/HtmlGenerator.java b/performance-results-page/src/main/java/graphql/performance/page/generator/HtmlGenerator.java new file mode 100644 index 000000000..78fa4a474 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/generator/HtmlGenerator.java @@ -0,0 +1,488 @@ +package graphql.performance.page.generator; + +import graphql.performance.page.model.BenchmarkSeries; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +public class HtmlGenerator { + + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .withZone(ZoneOffset.UTC); + + private static final DateTimeFormatter DATE_ONLY = DateTimeFormatter.ofPattern("yyyy-MM-dd") + .withZone(ZoneOffset.UTC); + + private static final String[] COLORS = { + "#4e79a7", "#f28e2b", "#e15759", "#76b7b2", "#59a14f", + "#edc948", "#b07aa1", "#ff9da7", "#9c755f", "#bab0ac", + "#af7aa1", "#86bcb6", "#d37295", "#8cd17d", "#b6992d" + }; + + public void generate(Map> groupedSeries, Path outputDir, int totalFiles, + Instant earliestDate, Instant latestDate) throws IOException { + Files.createDirectories(outputDir); + Path outputFile = outputDir.resolve("index.html"); + + StringBuilder html = new StringBuilder(); + html.append(""" + + + + + + graphql-java Performance Results + + + + + + """); + + appendHeader(html, groupedSeries, totalFiles); + appendFilterBar(html, earliestDate, latestDate); + appendNav(html, groupedSeries); + appendSections(html, groupedSeries); + appendFilterScript(html); + + html.append(""" + + + """); + + Files.writeString(outputFile, html.toString()); + } + + private void appendCss(StringBuilder html) { + html.append(""" + * { margin: 0; padding: 0; box-sizing: border-box; } + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #f5f5f5; + color: #333; + line-height: 1.6; + } + .header { + background: linear-gradient(135deg, #1a1a2e, #16213e); + color: white; + padding: 2rem; + text-align: center; + } + .header h1 { font-size: 1.8rem; margin-bottom: 0.5rem; } + .header .meta { font-size: 0.9rem; opacity: 0.8; } + .filter-bar { + background: #fff; + border-bottom: 1px solid #ddd; + padding: 0.75rem 2rem; + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + } + .filter-bar label { + font-size: 0.85rem; + font-weight: 600; + color: #555; + } + .filter-bar input[type="date"] { + padding: 0.3rem 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 0.85rem; + color: #333; + } + .filter-bar .presets { + display: flex; + gap: 0.35rem; + margin-left: 0.5rem; + } + .filter-bar .presets button { + padding: 0.3rem 0.7rem; + border: 1px solid #ccc; + border-radius: 4px; + background: #f0f4f8; + font-size: 0.8rem; + cursor: pointer; + color: #4e79a7; + font-weight: 500; + transition: background 0.2s, border-color 0.2s; + } + .filter-bar .presets button:hover { background: #dce8f1; border-color: #4e79a7; } + .filter-bar .presets button.active { background: #4e79a7; color: white; border-color: #4e79a7; } + .nav { + background: white; + border-bottom: 1px solid #ddd; + padding: 0.75rem 2rem; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .nav-title { font-weight: 600; margin-bottom: 0.5rem; font-size: 0.85rem; color: #666; text-transform: uppercase; letter-spacing: 0.05em; } + .nav-links { display: flex; flex-wrap: wrap; gap: 0.5rem; } + .nav-links button { + border: 1px solid transparent; + color: #4e79a7; + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.85rem; + background: #f0f4f8; + cursor: pointer; + font-family: inherit; + transition: background 0.2s, border-color 0.2s; + } + .nav-links button:hover { background: #dce8f1; } + .nav-links button.active { background: #4e79a7; color: white; border-color: #4e79a7; } + .content { max-width: 1400px; margin: 0 auto; padding: 2rem; } + .section { margin-bottom: 3rem; } + .section h2 { + font-size: 1.4rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #4e79a7; + scroll-margin-top: 4rem; + } + .chart-container { + background: white; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .chart-container h3 { + font-size: 1rem; + color: #555; + margin-bottom: 1rem; + } + .chart-wrapper { position: relative; height: 350px; } + table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + margin-bottom: 1rem; + } + th, td { padding: 0.6rem 1rem; text-align: left; border-bottom: 1px solid #eee; } + th { background: #f8f9fa; font-weight: 600; color: #555; } + tr:hover { background: #f8f9fa; } + .mode-badge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 3px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + } + .mode-thrpt { background: #e8f5e9; color: #2e7d32; } + .mode-avgt { background: #e3f2fd; color: #1565c0; } + .no-data { color: #999; font-style: italic; padding: 1rem; text-align: center; } + """); + } + + private void appendHeader(StringBuilder html, Map> groupedSeries, int totalFiles) { + int totalSeries = groupedSeries.values().stream().mapToInt(List::size).sum(); + html.append("
\n"); + html.append("

graphql-java Performance Results

\n"); + html.append("

"); + html.append(groupedSeries.size()).append(" benchmark classes · "); + html.append(totalSeries).append(" series · "); + html.append(totalFiles).append(" result files · "); + html.append("Generated ").append(DATE_FORMAT.format(Instant.now())).append(" UTC"); + html.append("

\n"); + html.append("
\n"); + } + + private void appendFilterBar(StringBuilder html, Instant earliestDate, Instant latestDate) { + String minDate = DATE_ONLY.format(earliestDate); + String maxDate = DATE_ONLY.format(latestDate); + html.append("
\n"); + html.append("\n"); + html.append("\n"); + html.append("to\n"); + html.append("\n"); + html.append("
\n"); + html.append("\n"); + html.append("\n"); + html.append("\n"); + html.append("\n"); + html.append("
\n"); + html.append("
\n"); + } + + private void appendNav(StringBuilder html, Map> groupedSeries) { + html.append("
\n"); + html.append("
Benchmark Classes
\n"); + html.append("
\n"); + html.append("\n"); + for (String className : groupedSeries.keySet()) { + html.append("\n"); + } + html.append("
\n
\n"); + } + + private void appendSections(StringBuilder html, Map> groupedSeries) { + html.append("
\n"); + + int chartId = 0; + int tableId = 0; + for (Map.Entry> entry : groupedSeries.entrySet()) { + String className = entry.getKey(); + List seriesList = entry.getValue(); + + html.append("
\n"); + html.append("

").append(className).append("

\n"); + + String sectionId = toAnchor(className); + + // Each series (method+mode+params) gets its own chart + for (BenchmarkSeries series : seriesList) { + String mode = series.getMode(); + String unit = series.getScoreUnit(); + String modeLabel = "thrpt".equals(mode) ? "Throughput" : "Average Time"; + String title = series.getMethodName(); + if (!series.getParamsString().isEmpty()) { + title += " (" + series.getParamsString() + ")"; + } + title += " - " + modeLabel + " (" + unit + ")"; + appendChart(html, title, "chart_" + chartId++, List.of(series), sectionId); + } + + appendLatestTable(html, seriesList, "table_" + tableId++); + + html.append("
\n"); + } + + html.append("
\n"); + } + + private void appendChart(StringBuilder html, String title, String canvasId, List seriesList, String sectionId) { + html.append("
\n"); + html.append("

").append(title).append("

\n"); + html.append("
\n"); + html.append("\n"); + html.append("
\n
\n"); + + html.append("\n"); + } + + private void appendLatestTable(StringBuilder html, List seriesList, String tableId) { + // Emit table data as JS for dynamic filtering + html.append("\n"); + html.append(""); + html.append(""); + html.append("\n
BenchmarkModeParamsScoreErrorUnitCommitDate
\n"); + + html.append("\n"); + } + + private void appendFilterScript(StringBuilder html) { + html.append(""" + + """); + } + + private static String toAnchor(String text) { + return text.toLowerCase().replaceAll("[^a-z0-9]+", "-"); + } + + private static String jsString(String s) { + return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; + } + + private static String formatScore(double score) { + if (score >= 1000) { + return String.format("%.0f", score); + } else if (score >= 1) { + return String.format("%.4f", score); + } else { + return String.format("%.6f", score); + } + } +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkResult.java b/performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkResult.java new file mode 100644 index 000000000..c3aecb897 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkResult.java @@ -0,0 +1,92 @@ +package graphql.performance.page.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class BenchmarkResult { + private String benchmark; + private String mode; + private PrimaryMetric primaryMetric; + private Map params; + + public String getBenchmark() { + return benchmark; + } + + public void setBenchmark(String benchmark) { + this.benchmark = benchmark; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public PrimaryMetric getPrimaryMetric() { + return primaryMetric; + } + + public void setPrimaryMetric(PrimaryMetric primaryMetric) { + this.primaryMetric = primaryMetric; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + + /** + * Returns the simple class name from the fully qualified benchmark name. + * e.g. "performance.ComplexQueryPerformance.benchMarkSimpleQueriesThroughput" -> "ComplexQueryPerformance" + */ + public String getBenchmarkClassName() { + String[] parts = benchmark.split("\\."); + return parts.length >= 2 ? parts[parts.length - 2] : benchmark; + } + + /** + * Returns the method name from the fully qualified benchmark name. + * e.g. "performance.ComplexQueryPerformance.benchMarkSimpleQueriesThroughput" -> "benchMarkSimpleQueriesThroughput" + */ + public String getBenchmarkMethodName() { + String[] parts = benchmark.split("\\."); + return parts.length >= 1 ? parts[parts.length - 1] : benchmark; + } + + /** + * Returns a params string for display, e.g. "howManyItems=5" or "" if no params. + */ + public String getParamsString() { + if (params == null || params.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + params.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> { + if (!sb.isEmpty()) { + sb.append(", "); + } + sb.append(e.getKey()).append("=").append(e.getValue()); + }); + return sb.toString(); + } + + /** + * Returns a unique series key: benchmark + mode + sorted params. + */ + public String getSeriesKey() { + String key = benchmark + ":" + mode; + String p = getParamsString(); + if (!p.isEmpty()) { + key += ":" + p; + } + return key; + } +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkSeries.java b/performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkSeries.java new file mode 100644 index 000000000..19024498f --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkSeries.java @@ -0,0 +1,86 @@ +package graphql.performance.page.model; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public class BenchmarkSeries { + private final String seriesKey; + private final String benchmarkName; + private final String benchmarkClassName; + private final String methodName; + private final String mode; + private final String paramsString; + private final List dataPoints = new ArrayList<>(); + + public BenchmarkSeries(String seriesKey, String benchmarkName, String benchmarkClassName, + String methodName, String mode, String paramsString) { + this.seriesKey = seriesKey; + this.benchmarkName = benchmarkName; + this.benchmarkClassName = benchmarkClassName; + this.methodName = methodName; + this.mode = mode; + this.paramsString = paramsString; + } + + public String getSeriesKey() { + return seriesKey; + } + + public String getBenchmarkName() { + return benchmarkName; + } + + public String getBenchmarkClassName() { + return benchmarkClassName; + } + + public String getMethodName() { + return methodName; + } + + public String getMode() { + return mode; + } + + public String getParamsString() { + return paramsString; + } + + public List getDataPoints() { + return dataPoints; + } + + public void addDataPoint(DataPoint dp) { + dataPoints.add(dp); + } + + /** + * Returns a display label for the series, e.g. "benchMarkSimpleQueriesThroughput (howManyItems=5)" + */ + public String getDisplayLabel() { + if (paramsString.isEmpty()) { + return methodName; + } + return methodName + " (" + paramsString + ")"; + } + + /** + * Returns the score unit of the most recent data point (used as the normalized unit). + */ + public String getScoreUnit() { + if (dataPoints.isEmpty()) { + return ""; + } + return dataPoints.getLast().scoreUnit(); + } + + public static record DataPoint( + Instant timestamp, + String commitHash, + String jdkVersion, + double score, + double scoreError, + String scoreUnit + ) {} +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/model/PrimaryMetric.java b/performance-results-page/src/main/java/graphql/performance/page/model/PrimaryMetric.java new file mode 100644 index 000000000..a6d41c8f8 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/model/PrimaryMetric.java @@ -0,0 +1,45 @@ +package graphql.performance.page.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PrimaryMetric { + private double score; + private double scoreError; + private String scoreUnit; + private List scoreConfidence; + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } + + public double getScoreError() { + return scoreError; + } + + public void setScoreError(double scoreError) { + this.scoreError = scoreError; + } + + public String getScoreUnit() { + return scoreUnit; + } + + public void setScoreUnit(String scoreUnit) { + this.scoreUnit = scoreUnit; + } + + public List getScoreConfidence() { + return scoreConfidence; + } + + public void setScoreConfidence(List scoreConfidence) { + this.scoreConfidence = scoreConfidence; + } +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/model/ResultFile.java b/performance-results-page/src/main/java/graphql/performance/page/model/ResultFile.java new file mode 100644 index 000000000..6a9cd4c83 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/model/ResultFile.java @@ -0,0 +1,34 @@ +package graphql.performance.page.model; + +import java.time.Instant; +import java.util.List; + +public class ResultFile { + private final Instant timestamp; + private final String commitHash; + private final String jdkVersion; + private final List results; + + public ResultFile(Instant timestamp, String commitHash, String jdkVersion, List results) { + this.timestamp = timestamp; + this.commitHash = commitHash; + this.jdkVersion = jdkVersion; + this.results = results; + } + + public Instant getTimestamp() { + return timestamp; + } + + public String getCommitHash() { + return commitHash; + } + + public String getJdkVersion() { + return jdkVersion; + } + + public List getResults() { + return results; + } +} diff --git a/performance-results-page/src/main/java/graphql/performance/page/parser/ResultFileParser.java b/performance-results-page/src/main/java/graphql/performance/page/parser/ResultFileParser.java new file mode 100644 index 000000000..f28ac07d5 --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/parser/ResultFileParser.java @@ -0,0 +1,51 @@ +package graphql.performance.page.parser; + +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.performance.page.model.BenchmarkResult; +import graphql.performance.page.model.ResultFile; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Arrays; +import java.util.List; + +public class ResultFileParser { + + private static final DateTimeFormatter TIMESTAMP_FORMAT = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH-mm-ss'Z'") + .toFormatter() + .withZone(java.time.ZoneOffset.UTC); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Parses a JMH result JSON file. Extracts metadata from the filename and benchmark results from the JSON content. + *

+ * Filename format: {timestamp}-{commitHash}-{jdkVersion}.json + * e.g. 2025-02-28T05-10-58Z-77adc96ca0deeb4098d1ff1450312cf30d18e6a4-jdk17.json + */ + public ResultFile parse(File file) throws IOException { + String filename = file.getName().replace(".json", ""); + + // Parse the filename: timestamp is the first 20 chars, then commit hash, then jdk version + // Format: 2025-02-28T05-10-58Z-{commitHash}-{jdkVersion} + String timestampStr = filename.substring(0, 20); // "2025-02-28T05-10-58Z" + String remainder = filename.substring(21); // "{commitHash}-{jdkVersion}" + + // The remainder is commitHash-jdkVersion. The jdk version is at the end after the last '-' + int lastDash = remainder.lastIndexOf('-'); + String commitHash = remainder.substring(0, lastDash); + String jdkVersion = remainder.substring(lastDash + 1); + + Instant timestamp = TIMESTAMP_FORMAT.parse(timestampStr, Instant::from); + + List results = Arrays.asList( + objectMapper.readValue(file, BenchmarkResult[].class) + ); + + return new ResultFile(timestamp, commitHash, jdkVersion, results); + } +} diff --git a/settings.gradle b/settings.gradle index 62e342849..a15c47033 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,3 +17,5 @@ plugins { } rootProject.name = 'graphql-java' + +include 'performance-results-page'