Add graphs to the website (#63)

Co-authored-by: Hugo Locurcio <hugo.locurcio@hugo.pro>
This commit is contained in:
Gilles Roudière
2024-04-26 21:59:57 +02:00
committed by GitHub
parent da07e229e4
commit c4b7c4ecf4
9 changed files with 546 additions and 63 deletions

View File

@@ -1,60 +1,66 @@
#!/usr/bin/env python3
import json
import random
NUM_EXPRESSIONS = 20
NUM_VARS = 3000 # Should be kept in sync with NUM_VARS in expression.gd
def _var_names():
rv = []
for i in range(NUM_VARS):
rv.append("x" + str(i))
return rv
rv = []
for i in range(NUM_VARS):
rv.append("x" + str(i))
return rv
def _var_values():
rv = []
for i in range (NUM_VARS):
rv.append((i+1)*10)
return rv
rv = []
for i in range(NUM_VARS):
rv.append((i + 1) * 10)
return rv
def _combine(nodes, ia, ib, op):
na = nodes[ia]
nb = nodes[ib]
del nodes[ib]
del nodes[ia]
nodes.append("(" + str(na) + " " + op + " " + str(nb) + ")")
na = nodes[ia]
nb = nodes[ib]
del nodes[ib]
del nodes[ia]
nodes.append("(" + str(na) + " " + op + " " + str(nb) + ")")
def _generate_string():
nodes = []
operators = ["+", "-", "*", "/"]
nodes = []
operators = ["+", "-", "*", "/"]
nodes += _var_names()
nodes += _var_names()
while len(nodes) > 1:
ia = random.randrange(0, len(nodes))
ib = random.randrange(0, len(nodes))
io = random.randrange(0, len(operators))
op = operators[io]
while len(nodes) > 1:
ia = random.randrange(0, len(nodes))
ib = random.randrange(0, len(nodes))
io = random.randrange(0, len(operators))
op = operators[io]
if ia == ib:
ib = ia+1
if ib == len(nodes):
ib = 0
if ia == ib:
ib = ia + 1
if ib == len(nodes):
ib = 0
if ia < ib:
_combine(nodes, ia, ib, op)
elif ib < ia:
_combine(nodes, ib, ia, op)
if ia < ib:
_combine(nodes, ia, ib, op)
elif ib < ia:
_combine(nodes, ib, ia, op)
return nodes[0]
return nodes[0]
def _generate_strings():
random.seed(234)
rv = []
for i in range(NUM_EXPRESSIONS):
rv.append(_generate_string())
return rv
random.seed(234)
rv = []
for i in range(NUM_EXPRESSIONS):
rv.append(_generate_string())
return rv
strings = _generate_strings()
print("const EXPRESSIONS = ", json.dumps(strings, indent=4).replace(' ', '\t'))
print("const EXPRESSIONS = ", json.dumps(strings, indent=4).replace(" ", "\t"))

12
web/.gitignore vendored
View File

@@ -4,7 +4,11 @@ public/
# Temporary lock file while building
.hugo_build.lock
# Result JSON files
# (should be committed to https://github.com/godotengine/godot-benchmarks-results instead)
content/*.md
!_content/_index.md
# Generated files
content/benchmark/*.md
content/graph/*.md
data/data.json
# Untracked source files
src-data/benchmarks/*.md
src-data/benchmarks/*.json

102
web/generate-content.py Executable file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
#
# Usage:
# generate-content.py [benchmarks-folder]
#
# This script generates the content and data files for hugo.
# This should be ran before trying to build the site.
#
# It takes as input two files, a "graph.json" in the ./src-data folder,
# and the results of our godot benchmarks (produced by ../run-benchmarks.sh)
# By default, benchmarks (.json or .md, but all should be JSON inside ¯\_(ツ)_/¯)
# are taken from the "./src-data/benchmarks". But you can specify an optional
# folder otherwise as argument.
import json
import sys
from os import listdir
from os.path import isdir, isfile, join
# Source data paths.
graphs_path = "./src-data/graphs.json"
if len(sys.argv) == 1:
benchmarks_path = "./src-data/benchmarks"
elif len(sys.argv) == 2:
benchmarks_path = sys.argv[1]
if not isdir(benchmarks_path):
raise ValueError(benchmarks_path + " is not a valid folder")
else:
raise ValueError("Invalid number of arguments")
# Bnase data.json dictionary.
data_output_json = {
"benchmarks": [],
"graphs": [],
}
### BENCHMARKS ###
# Fetch the list of benchmark files
benchmark_input_filename_test = lambda f: (f.endswith(".json") or f.endswith(".md"))
benchmarks_files = [
f for f in listdir(benchmarks_path) if (isfile(join(benchmarks_path, f)) and benchmark_input_filename_test(f))
]
# Add the list of benchmarks.
for f in benchmarks_files:
json_file = open(join(benchmarks_path, f))
# Extract data from filename.
key = f.removesuffix(".json")
date = key.split("_")[0]
commit = key.split("_")[1]
# Load and modify the benchmark file.
output_dict = json.load(json_file)
output_dict["date"] = date
output_dict["commit"] = commit
# Merge category and name into a single "path" field.
output_benchmark_list = []
for benchmark in output_dict["benchmarks"]:
output_benchmark_list.append(
{
"path": [el.strip() for el in benchmark["category"].split(">")] + [benchmark["name"]],
"results": benchmark["results"],
}
)
output_dict["benchmarks"] = output_benchmark_list
# Add it to the list.
data_output_json["benchmarks"].append(output_dict)
json_file.close()
### GRAPHS ###
# Add the graphs.
json_file = open(graphs_path)
data_output_json["graphs"] = json.load(json_file)
json_file.close()
### DUMPING data.json ###
# Create a big json with all of the data.
data_filename = "./data/data.json"
data_file = open(data_filename, "w")
json.dump(data_output_json, data_file, indent=4)
data_file.close()
### CREATE .md FILES (for the pages) ###
# Create a .md file for each benchmark.
benchmarks_content_path = "./content/benchmark"
for benchmark in data_output_json["benchmarks"]:
filename = benchmark["date"] + "_" + benchmark["commit"] + ".md"
open(join(benchmarks_content_path, filename), "a").close()
# Create a .md file for each graph.
graphs_content_path = "./content/graph"
for graph in data_output_json["graphs"]:
filename = graph["id"] + ".md"
open(join(graphs_content_path, filename), "a").close()

View File

@@ -30,6 +30,9 @@
window.location.href = window.location.href.replace('://godotengine.github.io/godot-benchmarks-results/','://benchmarks.godotengine.org/');
}
</script>
<script>
const Database = {{ .Site.Data.data }}
</script>
{{ block "javascript" . }}{{end}}
</body>
</html>

View File

@@ -1,20 +1,31 @@
{{ define "main" }}
{{ $date := index (split (path.BaseName .Permalink) "_") 0 }}
{{ $commit := index (split (path.BaseName .Permalink) "_") 1 }}
{{ $benchmark := where .Site.Data.data.benchmarks "commit" "eq" $commit }}
{{ $benchmark := where $benchmark "date" "eq" $date }}
{{ $benchmark := index $benchmark 0 }}
<h1>{{ index (split (path.BaseName .Permalink) "_") 0 }}
<a href="https://github.com/godotengine/godot/commit/{{ .Params.engine.version_hash }}"><code>{{ slicestr .Params.engine.version_hash 0 9 }}</code></a></h1>
<a href="https://github.com/godotengine/godot/commit/{{ $benchmark.engine.version_hash }}"><code>{{ slicestr $benchmark.engine.version_hash 0 9 }}</code></a>
</h1>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); grid-gap: 10px;">
{{/* Order is inverted for this site. */}}
<div>
{{with .Site.RegularPages.Next . }}
<a href="{{ .RelPermalink }}">« Previous: {{ index (split (path.BaseName .Permalink) "_") 0 }}
<code>{{ slicestr .Params.engine.version_hash 0 9 }}</code></a>
<code>{{ slicestr $benchmark.engine.version_hash 0 9 }}</code>
</a>
{{end}}
</div>
<div style="text-align: right">
{{with .Site.RegularPages.Prev . }}
<a href="{{ .RelPermalink }}">Next: {{ index (split (path.BaseName .Permalink) "_") 0 }}
<code>{{ slicestr .Params.engine.version_hash 0 9 }}</code> »</a>
<code>{{ slicestr $benchmark.engine.version_hash 0 9 }}</code> »
</a>
{{end}}
</div>
</div>
@@ -50,36 +61,36 @@
<tr>
<td><abbr title="Using GCC compiler with all caches cleared">Time to build</abbr></td>
<td>
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul .Params.build_time.debug 0.001 | lang.FormatNumber 0 }} seconds<br></span>
<sub>Release</sub> {{ mul .Params.build_time.release 0.001 | lang.FormatNumber 0 }} seconds
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul $benchmark.build_time.debug 0.001 | lang.FormatNumber 0 }} seconds<br></span>
<sub>Release</sub> {{ mul $benchmark.build_time.release 0.001 | lang.FormatNumber 0 }} seconds
</td>
</tr>
<tr>
<td><abbr title="Using GCC compiler with all caches cleared">Build peak memory usage</abbr></td>
<td>
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul .Params.build_peak_memory_usage.debug 0.001 | lang.FormatNumber 2 }} MB<br></span>
<sub>Release</sub> {{ mul .Params.build_peak_memory_usage.release 0.001 | lang.FormatNumber 2 }} MB
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul $benchmark.build_peak_memory_usage.debug 0.001 | lang.FormatNumber 2 }} MB<br></span>
<sub>Release</sub> {{ mul $benchmark.build_peak_memory_usage.release 0.001 | lang.FormatNumber 2 }} MB
</td>
</tr>
<tr>
<td><abbr title="Measured on an empty project">Startup + shutdown time</abbr></td>
<td>
<span style="opacity: 0.65"><sub>Debug</sub> {{ .Params.empty_project_startup_shutdown_time.debug | lang.FormatNumber 0 }} ms<br></span>
<sub>Release</sub> {{ .Params.empty_project_startup_shutdown_time.release | lang.FormatNumber 0 }} ms
<span style="opacity: 0.65"><sub>Debug</sub> {{ $benchmark.empty_project_startup_shutdown_time.debug | lang.FormatNumber 0 }} ms<br></span>
<sub>Release</sub> {{ $benchmark.empty_project_startup_shutdown_time.release | lang.FormatNumber 0 }} ms
</td>
</tr>
<tr>
<td><abbr title="Measured on an empty project">Startup + shutdown peak memory usage</abbr></td>
<td>
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul .Params.empty_project_startup_shutdown_peak_memory_usage.debug 0.001 | lang.FormatNumber 2 }} MB<br></span>
<sub>Release</sub> {{ mul .Params.empty_project_startup_shutdown_peak_memory_usage.release 0.001 | lang.FormatNumber 2 }} MB
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul $benchmark.empty_project_startup_shutdown_peak_memory_usage.debug 0.001 | lang.FormatNumber 2 }} MB<br></span>
<sub>Release</sub> {{ mul $benchmark.empty_project_startup_shutdown_peak_memory_usage.release 0.001 | lang.FormatNumber 2 }} MB
</td>
</tr>
<tr>
<td><abbr title="Measured after stripping debug symbols">Binary size</abbr></td>
<td>
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul .Params.binary_size.debug 0.001 | lang.FormatNumber 0 }} KB<br></span>
<sub>Release</sub> {{ mul .Params.binary_size.release 0.001 | lang.FormatNumber 0 }} KB
<span style="opacity: 0.65"><sub>Debug</sub> {{ mul $benchmark.binary_size.debug 0.001 | lang.FormatNumber 0 }} KB<br></span>
<sub>Release</sub> {{ mul $benchmark.binary_size.release 0.001 | lang.FormatNumber 0 }} KB
</td>
</tr>
</table>
@@ -100,10 +111,16 @@
<tbody>
{{/* Check CPU debug data only, but also get data from release CPU runs. */}}
{{/* These runs are expected to have the same number of results available. */}}
{{ range .Params.benchmarks }}
{{ range $benchmark.benchmarks }}
{{ if gt .results.cpu_debug.time 0 }}
<tr>
<td><sub style="opacity: 0.65">{{ .category }}</sub><br><strong>{{ .name }}</strong></td>
<td>
<sub style="opacity: 0.65">
{{ delimit (first (sub (len .path) 1) .path) " > "}}
</sub>
<br>
<strong>{{ index (last 1 .path) 0 }}</strong>
</td>
<td>
{{ if gt .results.cpu_debug.idle 0 }}
<span style="opacity: 0.65"><sub>Debug</sub> {{ .results.cpu_debug.idle }} <sub>mspf</sub><br></span>
@@ -113,7 +130,7 @@
<td>
{{ if gt .results.cpu_debug.physics 0 }}
<span style="opacity: 0.65"><sub>Debug</sub> {{ .results.cpu_debug.physics }} <sub>mspf</sub><br></span>
<sub>Release</sub> {{ .results.cpu_debug.physics }} <sub>mspf</sub>
<sub>Release</sub> {{ .results.cpu_release.physics }} <sub>mspf</sub>
{{ end }}
</td>
<td>
@@ -142,10 +159,16 @@
<tbody>
{{/* Check GPU AMD data only, but also get data from Intel and NVIDIA GPU runs. */}}
{{/* These runs are expected to have the same number of results available. */}}
{{ range .Params.benchmarks }}
{{ range $benchmark.benchmarks }}
{{ if gt .results.amd.render_cpu 0 }}
<tr>
<td><sub style="opacity: 0.65">{{ .category }}</sub><br><strong>{{ .name }}</strong></td>
<td>
<sub style="opacity: 0.65">
{{ delimit (first (sub (len .path) 1) .path) " > "}}
</sub>
<br>
<strong>{{ index (last 1 .path) 0 }}</strong></td>
</td>
<td>
{{ if gt .results.amd.render_cpu 0 }}
<!-- <span title="Intel HD Graphics">🔵</span> {{ .results.intel.render_cpu }} <sub>mspf</sub><br> -->

View File

@@ -0,0 +1,29 @@
{{ define "main" }}
{{ $graphID := path.BaseName .Permalink }}
{{ $graph := index (where .Site.Data.data.graphs "id" $graphID) 0 }}
<h1>
{{ $graph.title }} (lower is better)
</h1>
<div style="display: flex; flex-direction: column;">
<input style="margin-right: 0" type="text" id="filter" name="filter" placeholder="Filter..." oninput="updateChart()"/>
<div id="chart" style="min-height: 365px;">
</div>
</div>
{{end}}
{{ define "javascript" }}
<script language="javascript" type="text/javascript" src="{{ "/graphs.js" | urlize | relURL }}"></script>
<script>
function updateChart() {
const filterInput = document.getElementById("filter");
displayGraph("#chart", {{ path.BaseName .Permalink }}, "full", filterInput.value );
};
updateChart()
</script>
{{ end }}

View File

@@ -1,6 +1,9 @@
{{ define "main" }}
<main aria-role="main">
{{ $benchmarks := .Site.Data.data.benchmarks}}
{{ $graphs := .Site.Data.data.graphs}}
<main aria-role="main">
<p>
This page tracks <a href="https://godotengine.org/">Godot Engine</a> performance running on a
<a href="https://github.com/godotengine/godot-benchmarks">benchmark suite</a>.
@@ -8,14 +11,32 @@
regressions over time.
</p>
<h2>Graphs</h2>
<div style="display: flex; align-items: center; margin-bottom: 1em;">
<div style="flex-grow: 3;">
Normalized (percentage of the average time), lower is better.
</div>
</div>
<div style="display: grid; grid-template-columns: 50% 50%; gap: 1em;">
{{ range $graphs }}
<div style="display: flex; flex-direction: column; gap: 1em;">
<div style="align-self: center;">
<a href="/graph/{{ .id }}">{{ .title }}</a>
</div>
<div id="{{ .id }}">
</div>
</div>
{{ end }}
</div>
<h2>Latest benchmark runs</h2>
<ul>
{{ range first 100 .Pages.Reverse }}
<li><a href="{{ .Permalink }}">{{ index (split (path.BaseName .Permalink) "_") 0 }}
<code>{{ slicestr .Params.engine.version_hash 0 9 }}</code></a></li>
{{ range sort $benchmarks ".date" "asc" | first 100}}
<li>
<a href="/benchmark/{{ .date }}_{{ .commit }}">{{ .date }}<code>{{ slicestr .engine.version_hash 0 9 }}</code></a>
</li>
{{ end }}
</ul>
<hr>
<h2>Benchmarking machine</h2>
@@ -73,6 +94,15 @@
{{ end }}
{{ define "javascript" }}
<script language="javascript" type="text/javascript" src="{{ "/graphs.js" | urlize | relURL }}"></script>
<script>
// Update all graphs.
function updateGraphs() {
let selectTag = document.querySelector('#metric');
Database.graphs.forEach((g) => {
displayGraph(`#${g.id}`, g.id, "compact")
})
}
updateGraphs()
</script>
{{ end }}

77
web/src-data/graphs.json Normal file
View File

@@ -0,0 +1,77 @@
[
{
"id": "core-callables",
"title": "Callables",
"benchmark-path-prefix": "Core/Callable"
},
{
"id": "core-crypto",
"title": "Crypto",
"benchmark-path-prefix": "Core/Crypto"
},
{
"id": "core-rng",
"title": "Random Number Generator",
"benchmark-path-prefix": "Core/Random Number Generator"
},
{
"id": "core-signal",
"title": "Signals",
"benchmark-path-prefix": "Core/Signal"
},
{
"id": "gdscript-allocations",
"title": "GDscript allocations",
"benchmark-path-prefix": "Gdscript/Alloc"
},
{
"id": "gdscript-arrays",
"title": "GDscript arrays",
"benchmark-path-prefix": "Gdscript/Array"
},
{
"id": "gdscript-string-checksum",
"title": "GDscript String Checksums",
"benchmark-path-prefix": "Gdscript/String Checksum"
},
{
"id": "gdscript-string-format",
"title": "GDscript String Format",
"benchmark-path-prefix": "Gdscript/String Format"
},
{
"id": "gdscript-string-manipulations",
"title": "GDscript String Manipulations",
"benchmark-path-prefix": "Gdscript/String Manipulation"
},
{
"id": "physics",
"title": "Rigid Body 3D",
"benchmark-path-prefix": "Physics/Rigid Body 3d"
},
{
"id": "rendering-culling",
"title": "Culling",
"benchmark-path-prefix": "Rendering/Culling"
},
{
"id": "rendering-hlod",
"title": "Hlod",
"benchmark-path-prefix": "Rendering/Hlod"
},
{
"id": "rendering-labels",
"title": "Rendering labels",
"benchmark-path-prefix": "Rendering/Hlod"
},
{
"id": "rendering-light-and-meches",
"title": "Lights and Meshes",
"benchmark-path-prefix": "Rendering/Lights And Meshes"
},
{
"id": "rendering-polygon-sprite-2d",
"title": "Polygon Sprite 2d",
"benchmark-path-prefix": "Rendering/Polygon Sprite 2d"
}
]

209
web/static/graphs.js Normal file
View File

@@ -0,0 +1,209 @@
function getAllowedMetrics() {
const allowedMetrics = new Set();
Database.benchmarks.forEach((benchmark) => {
benchmark.benchmarks.forEach((instance) => {
Object.entries(instance.results).forEach(([key, value]) => {
allowedMetrics.add(key);
});
});
});
return allowedMetrics;
}
function displayGraph(targetDivID, graphID, type = "full", filter = "") {
if (!["full", "compact"].includes(type)) {
throw Error("Unknown chart type");
}
// Include benchmark data JSON to generate graphs.
const allBenchmarks = Database.benchmarks.sort(
(a, b) => `${a.date}.${a.commit}` > `${b.date}.${b.commit}`,
);
const graph = Database.graphs.find((g) => g.id == graphID);
if (!graph) {
throw new Error("Invalid graph ID");
}
// Group by series.
const xAxis = [];
const series = new Map();
const processResult = (path, data, process) => {
Object.entries(data).forEach(([key, value]) => {
if (typeof value === "object") {
processResult(path + "/" + key, value, process);
} else {
// Number
process(path + "/" + key, value);
}
});
};
// Get list all series and fill it in.
allBenchmarks.forEach((benchmark, count) => {
// Process a day/commit
xAxis.push(benchmark.date + "." + benchmark.commit);
// Get all series.
benchmark.benchmarks.forEach((instance) => {
let instanceKey = instance.path.join("/");
if (!instanceKey.startsWith(graph["benchmark-path-prefix"])) {
return;
}
instanceKey = instanceKey.slice(
graph["benchmark-path-prefix"].length + 1,
);
processResult(instanceKey, instance.results, (path, value) => {
// Filter out paths that do not fit the filter
if (filter && !path.includes(filter)) {
return;
}
if (!series.has(path)) {
series.set(path, Array(count).fill(null));
}
series.get(path).push(value);
});
});
});
let customColor = undefined;
if (type === "compact") {
// Kind of "normalize" the series, dividing by the average.
series.forEach((serie, key) => {
let count = 0;
let mean = 0.0;
serie.forEach((el) => {
if (el != null) {
mean += el;
count += 1;
}
});
mean = mean / count;
//const std = Math.sqrt(input.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n)
series.set(
key,
serie.map((v) => {
if (v != null) {
return v / mean; // Devide by the mean.
}
return null;
}),
);
});
// Combine all into a single, averaged serie.
const outputSerie = [];
for (let i = 0; i < allBenchmarks.length; i++) {
let count = 0;
let sum = 0;
series.forEach((serie, key) => {
if (serie[i] != null) {
count += 1;
sum += serie[i];
}
});
let point = null;
if (count >= 1) {
point = Math.round((sum * 1000) / count) / 10; // Round to 3 decimals.
}
outputSerie.push(point);
}
series.clear();
series.set("Average", outputSerie);
// Detect whether we went down or not on the last 10 benchmarks.
const lastElementsCount = 3;
const totalConsideredCount = 10;
const lastElements = outputSerie.slice(-lastElementsCount);
const comparedTo = outputSerie.slice(
-totalConsideredCount,
-lastElementsCount,
);
const avgLast = lastElements.reduce((a, b) => a + b) / lastElements.length;
const avgComparedTo =
comparedTo.reduce((a, b) => a + b) / comparedTo.length;
const trend = avgLast - avgComparedTo;
if (trend > 10) {
customColor = "#E20000";
} else if (trend < -10) {
customColor = "#00E200";
}
}
var options = {
series: Array.from(series.entries()).map(([key, value]) => ({
name: key,
data: value,
})),
chart: {
foreColor: "var(--text-bright)",
background: "var(--background)",
height: type === "compact" ? 200 : 600,
type: "line",
zoom: {
enabled: false,
},
toolbar: {
show: false,
},
animations: {
enabled: false,
},
},
tooltip: {
theme: "dark",
y: {
formatter: (value, opts) => (type === "compact" ? value + "%" : value),
},
},
dataLabels: {
enabled: false,
},
stroke: {
curve: "straight",
width: 2,
},
theme: {
palette: "palette4",
},
fill:
type === "compact"
? {
type: "gradient",
gradient: {
shade: "dark",
gradientToColors: ["#4ecdc4"],
shadeIntensity: 1,
type: "horizontal",
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100],
},
}
: {},
colors:
type === "compact"
? customColor
? [customColor]
: ["#4ecdc4"]
: undefined,
xaxis: {
categories: xAxis,
labels: {
show: type !== "compact",
},
},
yaxis: {
tickAmount: 4,
min: type === "compact" ? 0 : undefined,
max: type === "compact" ? 200 : undefined,
},
legend: {
show: type !== "compact",
},
};
var chart = new ApexCharts(document.querySelector(targetDivID), options);
chart.render();
}