Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,7 +22,6 @@ concurrency:
cancel-in-progress: false

jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
Expand All @@ -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
28 changes: 28 additions & 0 deletions performance-results-page/build.gradle
Original file line number Diff line number Diff line change
@@ -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
]
}
Original file line number Diff line number Diff line change
@@ -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 <inputDir> <outputDir>");
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<ResultFile> 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<String, List<BenchmarkSeries>> grouped = analyzer.analyze(resultFiles);

System.out.println("Found " + grouped.size() + " benchmark classes:");
for (Map.Entry<String, List<BenchmarkSeries>> 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"));
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Double> 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<String, Double> 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<String, List<BenchmarkSeries>> analyze(List<ResultFile> files) {
// Sort files by timestamp
files.sort(Comparator.comparing(ResultFile::getTimestamp));

// Build series map: seriesKey -> BenchmarkSeries
Map<String, BenchmarkSeries> 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<String, List<BenchmarkSeries>> 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<BenchmarkSeries> 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<BenchmarkSeries.DataPoint> 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;
}
}
}
Loading
Loading