Skip to content

Commit 32c996c

Browse files
authored
Merge pull request #4277 from graphql-java/performance-results-page
Add performance results dashboard subproject
2 parents a0449b5 + 5cb39d7 commit 32c996c

11 files changed

Lines changed: 1067 additions & 5 deletions

File tree

.github/workflows/static.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Simple workflow for deploying static content to GitHub Pages
2-
name: Deploy static content to Pages
1+
# Workflow for generating and deploying performance results to GitHub Pages
2+
name: Deploy performance results to Pages
33

44
on:
55
# Runs on pushes targeting the default branch
@@ -22,7 +22,6 @@ concurrency:
2222
cancel-in-progress: false
2323

2424
jobs:
25-
# Single deploy job since we're just deploying
2625
deploy:
2726
environment:
2827
name: github-pages
@@ -31,13 +30,19 @@ jobs:
3130
steps:
3231
- name: Checkout
3332
uses: actions/checkout@v4
33+
- name: Set up JDK 21
34+
uses: actions/setup-java@v4
35+
with:
36+
distribution: 'temurin'
37+
java-version: '21'
38+
- name: Generate performance page
39+
run: ./gradlew :performance-results-page:generatePerformancePage
3440
- name: Setup Pages
3541
uses: actions/configure-pages@v5
3642
- name: Upload artifact
3743
uses: actions/upload-pages-artifact@v3
3844
with:
39-
# Upload entire repository
40-
path: './pages'
45+
path: './performance-results-page/build/site'
4146
- name: Deploy to GitHub Pages
4247
id: deployment
4348
uses: actions/deploy-pages@v4
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
java {
6+
toolchain {
7+
languageVersion = JavaLanguageVersion.of(21)
8+
}
9+
}
10+
11+
repositories {
12+
mavenCentral()
13+
}
14+
15+
dependencies {
16+
implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.1'
17+
}
18+
19+
tasks.register('generatePerformancePage', JavaExec) {
20+
description = 'Generates the performance results HTML page'
21+
group = 'reporting'
22+
classpath = sourceSets.main.runtimeClasspath
23+
mainClass = 'graphql.performance.page.Main'
24+
args = [
25+
file("${rootProject.projectDir}/performance-results").absolutePath,
26+
file("${project.projectDir}/build/site").absolutePath
27+
]
28+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package graphql.performance.page;
2+
3+
import graphql.performance.page.analyzer.BenchmarkAnalyzer;
4+
import graphql.performance.page.generator.HtmlGenerator;
5+
import graphql.performance.page.model.BenchmarkSeries;
6+
import graphql.performance.page.model.ResultFile;
7+
import graphql.performance.page.parser.ResultFileParser;
8+
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.nio.file.Path;
12+
import java.time.Instant;
13+
import java.util.ArrayList;
14+
import java.util.Comparator;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
public class Main {
19+
20+
public static void main(String[] args) throws IOException {
21+
if (args.length < 2) {
22+
System.err.println("Usage: Main <inputDir> <outputDir>");
23+
System.exit(1);
24+
}
25+
26+
File inputDir = new File(args[0]);
27+
Path outputDir = Path.of(args[1]);
28+
29+
if (!inputDir.isDirectory()) {
30+
System.err.println("Input directory does not exist: " + inputDir);
31+
System.exit(1);
32+
}
33+
34+
File[] jsonFiles = inputDir.listFiles((dir, name) -> name.endsWith(".json"));
35+
if (jsonFiles == null || jsonFiles.length == 0) {
36+
System.err.println("No JSON files found in: " + inputDir);
37+
System.exit(1);
38+
}
39+
40+
System.out.println("Found " + jsonFiles.length + " result files in " + inputDir);
41+
42+
ResultFileParser parser = new ResultFileParser();
43+
List<ResultFile> resultFiles = new ArrayList<>();
44+
int errors = 0;
45+
46+
for (File file : jsonFiles) {
47+
try {
48+
resultFiles.add(parser.parse(file));
49+
} catch (Exception e) {
50+
System.err.println("Warning: Failed to parse " + file.getName() + ": " + e.getMessage());
51+
errors++;
52+
}
53+
}
54+
55+
System.out.println("Parsed " + resultFiles.size() + " files successfully" + (errors > 0 ? " (" + errors + " errors)" : ""));
56+
57+
BenchmarkAnalyzer analyzer = new BenchmarkAnalyzer();
58+
Map<String, List<BenchmarkSeries>> grouped = analyzer.analyze(resultFiles);
59+
60+
System.out.println("Found " + grouped.size() + " benchmark classes:");
61+
for (Map.Entry<String, List<BenchmarkSeries>> entry : grouped.entrySet()) {
62+
int totalPoints = entry.getValue().stream()
63+
.mapToInt(s -> s.getDataPoints().size())
64+
.sum();
65+
System.out.println(" " + entry.getKey() + ": " + entry.getValue().size() + " series, " + totalPoints + " data points");
66+
}
67+
68+
Instant earliest = resultFiles.stream().map(ResultFile::getTimestamp).min(Comparator.naturalOrder()).orElse(Instant.now());
69+
Instant latest = resultFiles.stream().map(ResultFile::getTimestamp).max(Comparator.naturalOrder()).orElse(Instant.now());
70+
71+
HtmlGenerator generator = new HtmlGenerator();
72+
generator.generate(grouped, outputDir, jsonFiles.length, earliest, latest);
73+
74+
System.out.println("Generated " + outputDir.resolve("index.html"));
75+
}
76+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package graphql.performance.page.analyzer;
2+
3+
import graphql.performance.page.model.BenchmarkResult;
4+
import graphql.performance.page.model.BenchmarkSeries;
5+
import graphql.performance.page.model.ResultFile;
6+
7+
import java.util.Comparator;
8+
import java.util.LinkedHashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.TreeMap;
12+
13+
public class BenchmarkAnalyzer {
14+
15+
/**
16+
* Unit conversion factors to a common base unit.
17+
* For time units: base is nanoseconds.
18+
* For throughput units: base is ops/s.
19+
*/
20+
private static final Map<String, Double> TIME_UNIT_TO_NANOS = Map.of(
21+
"ns/op", 1.0,
22+
"us/op", 1_000.0,
23+
"ms/op", 1_000_000.0,
24+
"s/op", 1_000_000_000.0
25+
);
26+
27+
private static final Map<String, Double> THROUGHPUT_UNIT_TO_OPS_PER_S = Map.of(
28+
"ops/s", 1.0,
29+
"ops/ms", 1_000.0,
30+
"ops/us", 1_000_000.0,
31+
"ops/ns", 1_000_000_000.0
32+
);
33+
34+
/**
35+
* Groups all results from all files into benchmark series, sorted by timestamp.
36+
* Returns a map of benchmarkClassName -> list of series for that class.
37+
*/
38+
public Map<String, List<BenchmarkSeries>> analyze(List<ResultFile> files) {
39+
// Sort files by timestamp
40+
files.sort(Comparator.comparing(ResultFile::getTimestamp));
41+
42+
// Build series map: seriesKey -> BenchmarkSeries
43+
Map<String, BenchmarkSeries> seriesMap = new LinkedHashMap<>();
44+
45+
for (ResultFile file : files) {
46+
for (BenchmarkResult result : file.getResults()) {
47+
String key = result.getSeriesKey();
48+
BenchmarkSeries series = seriesMap.computeIfAbsent(key, k ->
49+
new BenchmarkSeries(
50+
key,
51+
result.getBenchmark(),
52+
result.getBenchmarkClassName(),
53+
result.getBenchmarkMethodName(),
54+
result.getMode(),
55+
result.getParamsString()
56+
)
57+
);
58+
series.addDataPoint(new BenchmarkSeries.DataPoint(
59+
file.getTimestamp(),
60+
file.getCommitHash(),
61+
file.getJdkVersion(),
62+
result.getPrimaryMetric().getScore(),
63+
result.getPrimaryMetric().getScoreError(),
64+
result.getPrimaryMetric().getScoreUnit()
65+
));
66+
}
67+
}
68+
69+
// Normalize units within each series
70+
for (BenchmarkSeries series : seriesMap.values()) {
71+
normalizeUnits(series);
72+
}
73+
74+
// Group by benchmark class name, sorted alphabetically
75+
Map<String, List<BenchmarkSeries>> grouped = new TreeMap<>();
76+
for (BenchmarkSeries series : seriesMap.values()) {
77+
grouped.computeIfAbsent(series.getBenchmarkClassName(), k -> new java.util.ArrayList<>())
78+
.add(series);
79+
}
80+
81+
// Sort series within each class: by mode (thrpt first), then by display label
82+
for (List<BenchmarkSeries> seriesList : grouped.values()) {
83+
seriesList.sort(Comparator
84+
.comparing(BenchmarkSeries::getMode)
85+
.thenComparing(BenchmarkSeries::getDisplayLabel));
86+
}
87+
88+
return grouped;
89+
}
90+
91+
/**
92+
* Normalizes all data points in a series to the most recent unit.
93+
* This handles cases where a benchmark changed units over time (e.g. ns/op -> ms/op).
94+
*/
95+
private void normalizeUnits(BenchmarkSeries series) {
96+
List<BenchmarkSeries.DataPoint> points = series.getDataPoints();
97+
if (points.size() < 2) {
98+
return;
99+
}
100+
101+
// Target unit is the most recent data point's unit
102+
String targetUnit = points.getLast().scoreUnit();
103+
104+
// Check if all points already have the same unit
105+
boolean allSame = points.stream().allMatch(dp -> dp.scoreUnit().equals(targetUnit));
106+
if (allSame) {
107+
return;
108+
}
109+
110+
// Determine if these are time or throughput units
111+
boolean isTime = TIME_UNIT_TO_NANOS.containsKey(targetUnit);
112+
boolean isThroughput = THROUGHPUT_UNIT_TO_OPS_PER_S.containsKey(targetUnit);
113+
114+
if (!isTime && !isThroughput) {
115+
// Unknown unit family, skip normalization
116+
return;
117+
}
118+
119+
// Replace data points with normalized values
120+
for (int i = 0; i < points.size(); i++) {
121+
BenchmarkSeries.DataPoint dp = points.get(i);
122+
if (!dp.scoreUnit().equals(targetUnit)) {
123+
double factor = computeConversionFactor(dp.scoreUnit(), targetUnit, isTime);
124+
if (!Double.isNaN(factor)) {
125+
points.set(i, new BenchmarkSeries.DataPoint(
126+
dp.timestamp(),
127+
dp.commitHash(),
128+
dp.jdkVersion(),
129+
dp.score() * factor,
130+
dp.scoreError() * factor,
131+
targetUnit
132+
));
133+
}
134+
}
135+
}
136+
}
137+
138+
private double computeConversionFactor(String fromUnit, String toUnit, boolean isTime) {
139+
if (isTime) {
140+
Double fromFactor = TIME_UNIT_TO_NANOS.get(fromUnit);
141+
Double toFactor = TIME_UNIT_TO_NANOS.get(toUnit);
142+
if (fromFactor == null || toFactor == null) {
143+
return Double.NaN;
144+
}
145+
return fromFactor / toFactor;
146+
} else {
147+
Double fromFactor = THROUGHPUT_UNIT_TO_OPS_PER_S.get(fromUnit);
148+
Double toFactor = THROUGHPUT_UNIT_TO_OPS_PER_S.get(toUnit);
149+
if (fromFactor == null || toFactor == null) {
150+
return Double.NaN;
151+
}
152+
return fromFactor / toFactor;
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)