From 1be6ac523df2d2d0cce622d90314948394ad3448 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 2 Mar 2026 18:06:39 +1000 Subject: [PATCH 1/3] Add performance-results-page subproject to generate benchmark dashboard Adds a Java tool that parses JMH JSON result files from performance-results/ and generates an interactive HTML dashboard with Chart.js charts, deployed via the existing GitHub Pages workflow. Co-Authored-By: Claude Opus 4.6 --- pages/index.html | 6176 +++++++++++++++++ performance-results-page/build.gradle | 28 + .../java/graphql/performance/page/Main.java | 71 + .../page/analyzer/BenchmarkAnalyzer.java | 155 + .../page/generator/HtmlGenerator.java | 322 + .../page/model/BenchmarkResult.java | 92 + .../page/model/BenchmarkSeries.java | 86 + .../performance/page/model/PrimaryMetric.java | 45 + .../performance/page/model/ResultFile.java | 34 + .../page/parser/ResultFileParser.java | 51 + settings.gradle | 2 + 11 files changed, 7062 insertions(+) create mode 100644 pages/index.html create mode 100644 performance-results-page/build.gradle create mode 100644 performance-results-page/src/main/java/graphql/performance/page/Main.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/analyzer/BenchmarkAnalyzer.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/generator/HtmlGenerator.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkResult.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/model/BenchmarkSeries.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/model/PrimaryMetric.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/model/ResultFile.java create mode 100644 performance-results-page/src/main/java/graphql/performance/page/parser/ResultFileParser.java diff --git a/pages/index.html b/pages/index.html new file mode 100644 index 0000000000..54a1dfe3be --- /dev/null +++ b/pages/index.html @@ -0,0 +1,6176 @@ + + + + + +graphql-java Performance Results + + + + + +
+

graphql-java Performance Results

+

11 benchmark classes · 55 series · 242 result files · Generated 2026-03-02 08:04 UTC

+
+ +
+
+

ComplexQueryPerformance

+
+

Throughput (ops/s)

+
+ +
+
+ + + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkSimpleQueriesThroughputthrpthowManyItems=101.6775± 0.050564ops/s09b4ae612025-11-20 02:37
benchMarkSimpleQueriesThroughputthrpthowManyItems=200.848840± 0.022428ops/s09b4ae612025-11-20 02:37
benchMarkSimpleQueriesThroughputthrpthowManyItems=53.3326± 0.053326ops/s09b4ae612025-11-20 02:37
+
+
+

DFSelectionSetPerformance

+
+

Throughput (ops/ms)

+
+ +
+
+ +
+

Average Time (ms/op)

+
+ +
+
+ + + + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-0.061715± 0.001068ms/op09b4ae612025-11-20 02:37
benchMarkAvgTime_getImmediateFieldsavgt-0.000355± 0.000001ms/op09b4ae612025-11-20 02:37
benchMarkThroughputthrpt-15.8991± 0.272533ops/ms09b4ae612025-11-20 02:37
benchMarkThroughput_getImmediateFieldsthrpt-2797± 37.7069ops/ms09b4ae612025-11-20 02:37
+
+
+

DataLoaderPerformance

+
+

Average Time (ms/op)

+
+ +
+
+ + + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
executeRequestWithDataLoadersavgt-2.3021± 0.052317ms/op09b4ae612025-11-20 02:37
executeRequestWithDataLoadersavgtenableDataLoaderChaining=false0.133413± 0.000521ms/op572fbdf62025-04-11 11:32
executeRequestWithDataLoadersavgtenableDataLoaderChaining=true0.152753± 0.002702ms/op572fbdf62025-04-11 11:32
+
+
+

ENF1Performance

+
+

Throughput (ops/s)

+
+ +
+
+ +
+

Average Time (ms/op)

+
+ +
+
+ + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-0.013628± 0.000331ms/op09b4ae612025-11-20 02:37
benchMarkThroughputthrpt-73420± 3271ops/s09b4ae612025-11-20 02:37
+
+
+

ENF2Performance

+
+

Average Time (ms/op)

+
+ +
+
+ + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-1.0020± 0.022477ms/op09b4ae612025-11-20 02:37
+
+
+

ENFDeepIntrospectionPerformance

+
+

Average Time (ms/op)

+
+ +
+
+ + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgthowDeep=103.1299± 0.279609ms/op09b4ae612025-11-20 02:37
benchMarkAvgTimeavgthowDeep=20.010475± 0.000421ms/op09b4ae612025-11-20 02:37
+
+
+

ENFExtraLargePerformance

+
+

Throughput (ops/s)

+
+ +
+
+ +
+

Average Time (ms/op)

+
+ +
+
+ + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-2.8905± 0.023284ms/op09b4ae612025-11-20 02:37
benchMarkThroughputthrpt-348.2845± 2.9443ops/s09b4ae612025-11-20 02:37
+
+
+

LargeInMemoryQueryPerformance

+
+

Throughput (ops/s)

+
+ +
+
+ +
+

Average Time (s/op)

+
+ +
+
+ + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkSimpleQueriesAvgTimeavgt-6.2262± 1.0121s/op5f0713802025-05-06 06:59
benchMarkSimpleQueriesThroughputthrpt-0.161911± 0.031687ops/s5f0713802025-05-06 06:59
+
+
+

OverlappingFieldValidationPerformance

+
+

Throughput (ops/s)

+
+ +
+
+ +
+

Average Time (ms/op)

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
benchmarkDeepAbstractConcreteavgtsize=100.051882± 0.001629ms/op4f40d2ee2025-02-24 00:16
benchmarkDeepAbstractConcreteavgtsize=1000.179177± 0.001226ms/op09b4ae612025-11-20 02:37
benchmarkDeepAbstractConcreteavgtsize=226553± 639.1806ns/op422035b12025-02-23 23:27
benchmarkNoOverlapFragavgtsize=100.052744± 0.000730ms/op4f40d2ee2025-02-24 00:16
benchmarkNoOverlapFragavgtsize=1000.334308± 0.011187ms/op09b4ae612025-11-20 02:37
benchmarkNoOverlapFragavgtsize=211428± 324.3176ns/op422035b12025-02-23 23:27
benchmarkNoOverlapNoFragavgtsize=100.023302± 0.000860ms/op4f40d2ee2025-02-24 00:16
benchmarkNoOverlapNoFragavgtsize=1000.146270± 0.004956ms/op09b4ae612025-11-20 02:37
benchmarkNoOverlapNoFragavgtsize=25611± 199.0334ns/op422035b12025-02-23 23:27
benchmarkOverlapFragavgtsize=100.100415± 0.003447ms/op4f40d2ee2025-02-24 00:16
benchmarkOverlapFragavgtsize=1000.405875± 0.015854ms/op09b4ae612025-11-20 02:37
benchmarkOverlapFragavgtsize=223528± 750.0966ns/op422035b12025-02-23 23:27
benchmarkOverlapNoFragavgtsize=100.044369± 0.001216ms/op4f40d2ee2025-02-24 00:16
benchmarkOverlapNoFragavgtsize=1000.158894± 0.004881ms/op09b4ae612025-11-20 02:37
benchmarkOverlapNoFragavgtsize=211315± 172.0249ns/op422035b12025-02-23 23:27
benchmarkRepeatedFieldsavgtsize=100.011723± 0.000220ms/op4f40d2ee2025-02-24 00:16
benchmarkRepeatedFieldsavgtsize=1000.046875± 0.000071ms/op09b4ae612025-11-20 02:37
benchmarkRepeatedFieldsavgtsize=26503± 155.8866ns/op422035b12025-02-23 23:27
overlappingFieldValidationAbgTimeavgt-0.023578± 0.000685s/opa38c75c42025-02-18 21:00
overlappingFieldValidationAbgTimeavgtsize=106569577± 384955ns/op422035b12025-02-23 23:27
overlappingFieldValidationAbgTimeavgtsize=1006478659± 49070ns/op422035b12025-02-23 23:27
overlappingFieldValidationAbgTimeavgtsize=26398709± 126184ns/op422035b12025-02-23 23:27
overlappingFieldValidationAvgTimeavgtsize=1022874525± 920787ns/op4f40d2ee2025-02-24 00:16
overlappingFieldValidationAvgTimeavgtsize=1008755461± 231833ns/op09b4ae612025-11-20 02:37
overlappingFieldValidationThroughputavgtsize=106263377± 223312ns/op422035b12025-02-23 23:27
overlappingFieldValidationThroughputavgtsize=1006448862± 94674ns/op422035b12025-02-23 23:27
overlappingFieldValidationThroughputavgtsize=26376575± 194988ns/op422035b12025-02-23 23:27
overlappingFieldValidationThroughputthrpt-43.1658± 2.0669ops/sa38c75c42025-02-18 21:00
overlappingFieldValidationThroughputthrptsize=1043.7254± 1.2467ops/s4f40d2ee2025-02-24 00:16
overlappingFieldValidationThroughputthrptsize=100113.5994± 1.9749ops/s09b4ae612025-11-20 02:37
+
+
+

ValidatorBenchmark

+
+

Average Time (ms/op)

+
+ +
+
+ + + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
largeSchema1avgt-0.035889± 0.000471ms/op886710752025-05-08 10:51
largeSchema4avgt-14.3146± 0.481589ms/op886710752025-05-08 10:51
manyFragmentsavgt-9.5796± 0.390688ms/op886710752025-05-08 10:51
+
+
+

ValidatorPerformance

+
+

Average Time (ms/op)

+
+ +
+
+ + + + + + + +
BenchmarkModeParamsScoreErrorUnitCommitDate
largeSchema1avgt-0.025288± 0.002937ms/op061262142025-05-12 06:46
largeSchema4avgt-10.2073± 0.655811ms/op061262142025-05-12 06:46
manyFragmentsavgt-7.1175± 0.368346ms/op061262142025-05-12 06:46
+
+
+ + diff --git a/performance-results-page/build.gradle b/performance-results-page/build.gradle new file mode 100644 index 0000000000..de2e8ca42e --- /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("${rootProject.projectDir}/pages").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 0000000000..aafbd52eef --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/Main.java @@ -0,0 +1,71 @@ +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.util.ArrayList; +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"); + } + + HtmlGenerator generator = new HtmlGenerator(); + generator.generate(grouped, outputDir, jsonFiles.length); + + 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 0000000000..724eca5f45 --- /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 0000000000..d4fd66bfdb --- /dev/null +++ b/performance-results-page/src/main/java/graphql/performance/page/generator/HtmlGenerator.java @@ -0,0 +1,322 @@ +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 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) 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); + appendNav(html, groupedSeries); + appendSections(html, groupedSeries); + + 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; } + .nav { + background: white; + border-bottom: 1px solid #ddd; + padding: 1rem 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 a { + text-decoration: none; + color: #4e79a7; + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.85rem; + background: #f0f4f8; + transition: background 0.2s; + } + .nav-links a:hover { background: #dce8f1; } + .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; } + """); + } + + 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 appendNav(StringBuilder html, Map> groupedSeries) { + html.append("
\n"); + html.append("
Benchmark Classes
\n"); + html.append("
\n"); + for (String className : groupedSeries.keySet()) { + html.append("").append(className).append("\n"); + } + html.append("
\n
\n"); + } + + private void appendSections(StringBuilder html, Map> groupedSeries) { + html.append("
\n"); + + int chartId = 0; + for (Map.Entry> entry : groupedSeries.entrySet()) { + String className = entry.getKey(); + List seriesList = entry.getValue(); + + html.append("
\n"); + html.append("

").append(className).append("

\n"); + + // Group series by mode for separate charts + List thrptSeries = seriesList.stream() + .filter(s -> "thrpt".equals(s.getMode())) + .toList(); + List avgtSeries = seriesList.stream() + .filter(s -> "avgt".equals(s.getMode())) + .toList(); + + if (!thrptSeries.isEmpty()) { + String unit = thrptSeries.getFirst().getScoreUnit(); + appendChart(html, "Throughput (" + unit + ")", "chart_" + chartId++, thrptSeries); + } + if (!avgtSeries.isEmpty()) { + String unit = avgtSeries.getFirst().getScoreUnit(); + appendChart(html, "Average Time (" + unit + ")", "chart_" + chartId++, avgtSeries); + } + + // Latest results table + appendLatestTable(html, seriesList); + + html.append("
\n"); + } + + html.append("
\n"); + } + + private void appendChart(StringBuilder html, String title, String canvasId, List seriesList) { + 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) { + html.append("\n"); + html.append(""); + html.append(""); + html.append("\n\n"); + + for (BenchmarkSeries series : seriesList) { + if (series.getDataPoints().isEmpty()) { + continue; + } + BenchmarkSeries.DataPoint latest = series.getDataPoints().getLast(); + String modeBadge = "" + series.getMode() + ""; + + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append("\n"); + } + + html.append("
BenchmarkModeParamsScoreErrorUnitCommitDate
").append(series.getMethodName()).append("").append(modeBadge).append("").append(series.getParamsString().isEmpty() ? "-" : series.getParamsString()).append("").append(formatScore(latest.score())).append("± ").append(formatScore(latest.scoreError())).append("").append(latest.scoreUnit()).append("").append(latest.commitHash(), 0, Math.min(8, latest.commitHash().length())).append("").append(DATE_FORMAT.format(latest.timestamp())).append("
\n"); + } + + 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 0000000000..c3aecb8973 --- /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 0000000000..19024498ff --- /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 0000000000..a6d41c8f8e --- /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 0000000000..6a9cd4c83a --- /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 0000000000..f28ac07d5e --- /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 62e3428491..a15c470338 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,3 +17,5 @@ plugins { } rootProject.name = 'graphql-java' + +include 'performance-results-page' From 54597be4f3d125023735a78ff0723c573319a80b Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 2 Mar 2026 18:09:52 +1000 Subject: [PATCH 2/3] Generate index.html inside subproject build dir and update Pages workflow Output now goes to performance-results-page/build/site/ instead of pages/. The GitHub Actions workflow runs the Gradle task with JDK 21 before deploying. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/static.yml | 15 +- pages/index.html | 6176 ------------------------- performance-results-page/build.gradle | 2 +- 3 files changed, 11 insertions(+), 6182 deletions(-) delete mode 100644 pages/index.html diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index a74e525e80..30353352ed 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/pages/index.html b/pages/index.html deleted file mode 100644 index 54a1dfe3be..0000000000 --- a/pages/index.html +++ /dev/null @@ -1,6176 +0,0 @@ - - - - - -graphql-java Performance Results - - - - - -

-

graphql-java Performance Results

-

11 benchmark classes · 55 series · 242 result files · Generated 2026-03-02 08:04 UTC

-
- -
-
-

ComplexQueryPerformance

-
-

Throughput (ops/s)

-
- -
-
- - - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkSimpleQueriesThroughputthrpthowManyItems=101.6775± 0.050564ops/s09b4ae612025-11-20 02:37
benchMarkSimpleQueriesThroughputthrpthowManyItems=200.848840± 0.022428ops/s09b4ae612025-11-20 02:37
benchMarkSimpleQueriesThroughputthrpthowManyItems=53.3326± 0.053326ops/s09b4ae612025-11-20 02:37
-
-
-

DFSelectionSetPerformance

-
-

Throughput (ops/ms)

-
- -
-
- -
-

Average Time (ms/op)

-
- -
-
- - - - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-0.061715± 0.001068ms/op09b4ae612025-11-20 02:37
benchMarkAvgTime_getImmediateFieldsavgt-0.000355± 0.000001ms/op09b4ae612025-11-20 02:37
benchMarkThroughputthrpt-15.8991± 0.272533ops/ms09b4ae612025-11-20 02:37
benchMarkThroughput_getImmediateFieldsthrpt-2797± 37.7069ops/ms09b4ae612025-11-20 02:37
-
-
-

DataLoaderPerformance

-
-

Average Time (ms/op)

-
- -
-
- - - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
executeRequestWithDataLoadersavgt-2.3021± 0.052317ms/op09b4ae612025-11-20 02:37
executeRequestWithDataLoadersavgtenableDataLoaderChaining=false0.133413± 0.000521ms/op572fbdf62025-04-11 11:32
executeRequestWithDataLoadersavgtenableDataLoaderChaining=true0.152753± 0.002702ms/op572fbdf62025-04-11 11:32
-
-
-

ENF1Performance

-
-

Throughput (ops/s)

-
- -
-
- -
-

Average Time (ms/op)

-
- -
-
- - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-0.013628± 0.000331ms/op09b4ae612025-11-20 02:37
benchMarkThroughputthrpt-73420± 3271ops/s09b4ae612025-11-20 02:37
-
-
-

ENF2Performance

-
-

Average Time (ms/op)

-
- -
-
- - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-1.0020± 0.022477ms/op09b4ae612025-11-20 02:37
-
-
-

ENFDeepIntrospectionPerformance

-
-

Average Time (ms/op)

-
- -
-
- - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgthowDeep=103.1299± 0.279609ms/op09b4ae612025-11-20 02:37
benchMarkAvgTimeavgthowDeep=20.010475± 0.000421ms/op09b4ae612025-11-20 02:37
-
-
-

ENFExtraLargePerformance

-
-

Throughput (ops/s)

-
- -
-
- -
-

Average Time (ms/op)

-
- -
-
- - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkAvgTimeavgt-2.8905± 0.023284ms/op09b4ae612025-11-20 02:37
benchMarkThroughputthrpt-348.2845± 2.9443ops/s09b4ae612025-11-20 02:37
-
-
-

LargeInMemoryQueryPerformance

-
-

Throughput (ops/s)

-
- -
-
- -
-

Average Time (s/op)

-
- -
-
- - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchMarkSimpleQueriesAvgTimeavgt-6.2262± 1.0121s/op5f0713802025-05-06 06:59
benchMarkSimpleQueriesThroughputthrpt-0.161911± 0.031687ops/s5f0713802025-05-06 06:59
-
-
-

OverlappingFieldValidationPerformance

-
-

Throughput (ops/s)

-
- -
-
- -
-

Average Time (ms/op)

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
benchmarkDeepAbstractConcreteavgtsize=100.051882± 0.001629ms/op4f40d2ee2025-02-24 00:16
benchmarkDeepAbstractConcreteavgtsize=1000.179177± 0.001226ms/op09b4ae612025-11-20 02:37
benchmarkDeepAbstractConcreteavgtsize=226553± 639.1806ns/op422035b12025-02-23 23:27
benchmarkNoOverlapFragavgtsize=100.052744± 0.000730ms/op4f40d2ee2025-02-24 00:16
benchmarkNoOverlapFragavgtsize=1000.334308± 0.011187ms/op09b4ae612025-11-20 02:37
benchmarkNoOverlapFragavgtsize=211428± 324.3176ns/op422035b12025-02-23 23:27
benchmarkNoOverlapNoFragavgtsize=100.023302± 0.000860ms/op4f40d2ee2025-02-24 00:16
benchmarkNoOverlapNoFragavgtsize=1000.146270± 0.004956ms/op09b4ae612025-11-20 02:37
benchmarkNoOverlapNoFragavgtsize=25611± 199.0334ns/op422035b12025-02-23 23:27
benchmarkOverlapFragavgtsize=100.100415± 0.003447ms/op4f40d2ee2025-02-24 00:16
benchmarkOverlapFragavgtsize=1000.405875± 0.015854ms/op09b4ae612025-11-20 02:37
benchmarkOverlapFragavgtsize=223528± 750.0966ns/op422035b12025-02-23 23:27
benchmarkOverlapNoFragavgtsize=100.044369± 0.001216ms/op4f40d2ee2025-02-24 00:16
benchmarkOverlapNoFragavgtsize=1000.158894± 0.004881ms/op09b4ae612025-11-20 02:37
benchmarkOverlapNoFragavgtsize=211315± 172.0249ns/op422035b12025-02-23 23:27
benchmarkRepeatedFieldsavgtsize=100.011723± 0.000220ms/op4f40d2ee2025-02-24 00:16
benchmarkRepeatedFieldsavgtsize=1000.046875± 0.000071ms/op09b4ae612025-11-20 02:37
benchmarkRepeatedFieldsavgtsize=26503± 155.8866ns/op422035b12025-02-23 23:27
overlappingFieldValidationAbgTimeavgt-0.023578± 0.000685s/opa38c75c42025-02-18 21:00
overlappingFieldValidationAbgTimeavgtsize=106569577± 384955ns/op422035b12025-02-23 23:27
overlappingFieldValidationAbgTimeavgtsize=1006478659± 49070ns/op422035b12025-02-23 23:27
overlappingFieldValidationAbgTimeavgtsize=26398709± 126184ns/op422035b12025-02-23 23:27
overlappingFieldValidationAvgTimeavgtsize=1022874525± 920787ns/op4f40d2ee2025-02-24 00:16
overlappingFieldValidationAvgTimeavgtsize=1008755461± 231833ns/op09b4ae612025-11-20 02:37
overlappingFieldValidationThroughputavgtsize=106263377± 223312ns/op422035b12025-02-23 23:27
overlappingFieldValidationThroughputavgtsize=1006448862± 94674ns/op422035b12025-02-23 23:27
overlappingFieldValidationThroughputavgtsize=26376575± 194988ns/op422035b12025-02-23 23:27
overlappingFieldValidationThroughputthrpt-43.1658± 2.0669ops/sa38c75c42025-02-18 21:00
overlappingFieldValidationThroughputthrptsize=1043.7254± 1.2467ops/s4f40d2ee2025-02-24 00:16
overlappingFieldValidationThroughputthrptsize=100113.5994± 1.9749ops/s09b4ae612025-11-20 02:37
-
-
-

ValidatorBenchmark

-
-

Average Time (ms/op)

-
- -
-
- - - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
largeSchema1avgt-0.035889± 0.000471ms/op886710752025-05-08 10:51
largeSchema4avgt-14.3146± 0.481589ms/op886710752025-05-08 10:51
manyFragmentsavgt-9.5796± 0.390688ms/op886710752025-05-08 10:51
-
-
-

ValidatorPerformance

-
-

Average Time (ms/op)

-
- -
-
- - - - - - - -
BenchmarkModeParamsScoreErrorUnitCommitDate
largeSchema1avgt-0.025288± 0.002937ms/op061262142025-05-12 06:46
largeSchema4avgt-10.2073± 0.655811ms/op061262142025-05-12 06:46
manyFragmentsavgt-7.1175± 0.368346ms/op061262142025-05-12 06:46
-
-
- - diff --git a/performance-results-page/build.gradle b/performance-results-page/build.gradle index de2e8ca42e..ea1421e449 100644 --- a/performance-results-page/build.gradle +++ b/performance-results-page/build.gradle @@ -23,6 +23,6 @@ tasks.register('generatePerformancePage', JavaExec) { mainClass = 'graphql.performance.page.Main' args = [ file("${rootProject.projectDir}/performance-results").absolutePath, - file("${rootProject.projectDir}/pages").absolutePath + file("${project.projectDir}/build/site").absolutePath ] } From 5cb39d753d14f2664687be15120be7cb21134290 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 2 Mar 2026 18:29:37 +1000 Subject: [PATCH 3/3] Enhance dashboard with date range filter, class selector, and per-series charts - Add date range filter bar with preset buttons (30d, 90d, 6mo, all time) - Turn nav into a class filter that shows/hides sections - Give each benchmark series (method+mode+params) its own chart - Tables update dynamically with the date filter Co-Authored-By: Claude Opus 4.6 --- .../java/graphql/performance/page/Main.java | 7 +- .../page/generator/HtmlGenerator.java | 336 +++++++++++++----- 2 files changed, 257 insertions(+), 86 deletions(-) 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 index aafbd52eef..a8960c7594 100644 --- a/performance-results-page/src/main/java/graphql/performance/page/Main.java +++ b/performance-results-page/src/main/java/graphql/performance/page/Main.java @@ -9,7 +9,9 @@ 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; @@ -63,8 +65,11 @@ public static void main(String[] args) throws IOException { 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); + 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/generator/HtmlGenerator.java b/performance-results-page/src/main/java/graphql/performance/page/generator/HtmlGenerator.java index d4fd66bfdb..78fa4a474c 100644 --- 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 @@ -16,13 +16,17 @@ 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) throws IOException { + public void generate(Map> groupedSeries, Path outputDir, int totalFiles, + Instant earliestDate, Instant latestDate) throws IOException { Files.createDirectories(outputDir); Path outputFile = outputDir.resolve("index.html"); @@ -46,8 +50,10 @@ public void generate(Map> groupedSeries, Path outp """); appendHeader(html, groupedSeries, totalFiles); + appendFilterBar(html, earliestDate, latestDate); appendNav(html, groupedSeries); appendSections(html, groupedSeries); + appendFilterScript(html); html.append(""" @@ -74,10 +80,49 @@ private void appendCss(StringBuilder html) { } .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: 1rem 2rem; + padding: 0.75rem 2rem; position: sticky; top: 0; z-index: 100; @@ -85,16 +130,19 @@ private void appendCss(StringBuilder html) { } .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 a { - text-decoration: none; + .nav-links button { + border: 1px solid transparent; color: #4e79a7; padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.85rem; background: #f0f4f8; - transition: background 0.2s; + cursor: pointer; + font-family: inherit; + transition: background 0.2s, border-color 0.2s; } - .nav-links a:hover { background: #dce8f1; } + .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 { @@ -140,6 +188,7 @@ private void appendCss(StringBuilder html) { } .mode-thrpt { background: #e8f5e9; color: #2e7d32; } .mode-avgt { background: #e3f2fd; color: #1565c0; } + .no-data { color: #999; font-style: italic; padding: 1rem; text-align: center; } """); } @@ -156,12 +205,33 @@ private void appendHeader(StringBuilder html, Map> 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("").append(className).append("\n"); + html.append("\n"); } html.append("
\n
\n"); } @@ -170,32 +240,30 @@ private void appendSections(StringBuilder html, Map\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("
\n"); html.append("

").append(className).append("

\n"); - // Group series by mode for separate charts - List thrptSeries = seriesList.stream() - .filter(s -> "thrpt".equals(s.getMode())) - .toList(); - List avgtSeries = seriesList.stream() - .filter(s -> "avgt".equals(s.getMode())) - .toList(); - - if (!thrptSeries.isEmpty()) { - String unit = thrptSeries.getFirst().getScoreUnit(); - appendChart(html, "Throughput (" + unit + ")", "chart_" + chartId++, thrptSeries); - } - if (!avgtSeries.isEmpty()) { - String unit = avgtSeries.getFirst().getScoreUnit(); - appendChart(html, "Average Time (" + unit + ")", "chart_" + chartId++, avgtSeries); + 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); } - // Latest results table - appendLatestTable(html, seriesList); + appendLatestTable(html, seriesList, "table_" + tableId++); html.append("
\n"); } @@ -203,7 +271,7 @@ private void appendSections(StringBuilder html, Map\n"); } - private void appendChart(StringBuilder html, String title, String canvasId, List seriesList) { + 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"); @@ -211,95 +279,193 @@ private void appendChart(StringBuilder html, String title, String canvasId, List html.append("
\n
\n"); html.append("\n"); } - private void appendLatestTable(StringBuilder html, List seriesList) { - 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\n"); + html.append("\n
BenchmarkModeParamsScoreErrorUnitCommitDate
\n"); + html.append("\n"); + } + + private void appendFilterScript(StringBuilder html) { + html.append(""" + + """); } private static String toAnchor(String text) {