Add web interface for tracking benchmark results over time (#59)

This provides a web interface generated with Hugo for tracking
benchmark results over time. Automatic dark theme support is
provided using water.css.

Benchmarks are run daily on a dedicated server with the `master` branch
of Godot compiled from source on that server, so that build time and
memory usage during the build can also be tracked over time.
Binary size as well as startup/shutdown time and memory usage
from an empty project are also measured.
This commit is contained in:
Hugo Locurcio
2024-04-18 15:53:55 +02:00
committed by GitHub
parent 5c91a3eb71
commit dd91e37bac
30 changed files with 3179 additions and 35 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
web/static/thirdparty/* linguist-vendored

3
.gitignore vendored
View File

@@ -15,3 +15,6 @@ mono_crash.*.json
# System/tool-specific ignores # System/tool-specific ignores
.directory .directory
*~ *~
# Godot Git repository clone (run-benchmarks.sh)
/godot/

View File

@@ -5,5 +5,6 @@
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework> <TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading> <EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>GodotBenchmarks</RootNamespace> <RootNamespace>GodotBenchmarks</RootNamespace>
<DefaultItemExcludes>$(DefaultItemExcludes);godot/**</DefaultItemExcludes>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -47,6 +47,10 @@ You can save the results JSON to a file using `--save-json="path/to/file.json"`
godot -- --run-benchmarks godot -- --run-benchmarks
``` ```
`--json-results-prefix=<string>` can be used to nest individual results within a
dictionary that has the name `<string>`. This can be used for easier merging of
separate result runs with `jq`.
#### Run a single benchmark #### Run a single benchmark
The `--include-benchmarks` CLI argument can be used to specify the name. The `--include-benchmarks` CLI argument can be used to specify the name.

View File

@@ -17,18 +17,6 @@ func benchmark_generate_1g_random_bytes_1k_at_a_time() -> void:
crypto.generate_random_bytes(1000) crypto.generate_random_bytes(1000)
func benchmark_generate_1g_random_bytes_1m_at_a_time() -> void:
var iterations = BYTES / 1_000_000
for i in iterations:
crypto.generate_random_bytes(1_000_000)
func benchmark_generate_1g_random_bytes_at_once() -> void:
var iterations = BYTES / 1_000_000_000
for i in iterations:
crypto.generate_random_bytes(1_000_000_000)
func benchmark_generate_rsa_2048() -> void: func benchmark_generate_rsa_2048() -> void:
crypto.generate_rsa(2048) crypto.generate_rsa(2048)

View File

@@ -6,6 +6,7 @@ var items := []
var arg_include_benchmarks := "" var arg_include_benchmarks := ""
var arg_exclude_benchmarks := "" var arg_exclude_benchmarks := ""
var arg_save_json := "" var arg_save_json := ""
var arg_json_results_prefix := ""
var arg_run_benchmarks := false var arg_run_benchmarks := false
@onready var tree := $Tree as Tree @onready var tree := $Tree as Tree
@@ -90,6 +91,7 @@ func _ready() -> void:
if arg_save_json: if arg_save_json:
Manager.save_json_to_path = arg_save_json Manager.save_json_to_path = arg_save_json
Manager.json_results_prefix = arg_json_results_prefix
if arg_run_benchmarks: if arg_run_benchmarks:
_on_Run_pressed() _on_Run_pressed()

View File

@@ -37,7 +37,7 @@ func test_ids_from_path(path: String) -> Array[TestID]:
for method in bench_script.get_method_list(): for method in bench_script.get_method_list():
if not method.name.begins_with(languages[extension]["test_prefix"]): if not method.name.begins_with(languages[extension]["test_prefix"]):
continue continue
# This method is a runnable test. Push it onto the result # This method is a runnable test. Push it onto the result
var test_id := TestID.new() var test_id := TestID.new()
test_id.name = method.name.trim_prefix(languages[extension]["test_prefix"]) test_id.name = method.name.trim_prefix(languages[extension]["test_prefix"])
@@ -45,7 +45,7 @@ func test_ids_from_path(path: String) -> Array[TestID]:
test_id.language = extension test_id.language = extension
rv.push_back(test_id) rv.push_back(test_id)
return rv return rv
# List of supported languages and their styles. # List of supported languages and their styles.
@@ -55,6 +55,7 @@ var languages := {".gd": {"test_prefix": "benchmark_"}}
var test_results := {} var test_results := {}
var save_json_to_path := "" var save_json_to_path := ""
var json_results_prefix := ""
## Recursively walks the given directory and returns all files found ## Recursively walks the given directory and returns all files found
@@ -114,18 +115,21 @@ func benchmark(test_ids: Array[TestID], return_path: String) -> void:
print("Results JSON:") print("Results JSON:")
print("----------------") print("----------------")
print(JSON.stringify(get_results_dict())) print(JSON.stringify(get_results_dict(json_results_prefix)))
print("----------------") print("----------------")
if not save_json_to_path.is_empty(): if not save_json_to_path.is_empty():
print("Saving JSON output to: %s" % save_json_to_path) print("Saving JSON output to: %s" % save_json_to_path)
print("Using prefix for results: %s" % json_results_prefix)
var file := FileAccess.open(save_json_to_path, FileAccess.WRITE) var file := FileAccess.open(save_json_to_path, FileAccess.WRITE)
file.store_string(JSON.stringify(get_results_dict())) file.store_string(JSON.stringify(get_results_dict(json_results_prefix)))
if return_path: if return_path:
get_tree().change_scene_to_file(return_path) get_tree().change_scene_to_file(return_path)
else: else:
get_tree().queue_delete(get_tree()) # FIXME: The line below crashes the engine. Commenting it results in a
# "ObjectDB instances leaked at exit" warning (but no crash).
#get_tree().queue_delete(get_tree())
get_tree().quit() get_tree().quit()
@@ -198,9 +202,13 @@ func get_result_as_string(test_id: TestID) -> String:
return JSON.stringify(rd) return JSON.stringify(rd)
func get_test_result_as_dict(test_id: TestID) -> Dictionary: func get_test_result_as_dict(test_id: TestID, results_prefix: String = "") -> Dictionary:
var result : Results = test_results[test_id] var result : Results = test_results[test_id]
var rv := {} var rv := {}
if not results_prefix.is_empty():
# Nest the results dictionary with a prefix for easier merging of multiple unrelated runs with `jq`.
# For example, this is used on the benchmarks server to merge runs on several GPU vendors into a single JSON file.
rv = { results_prefix: {} }
if not result: if not result:
return rv return rv
@@ -208,11 +216,16 @@ func get_test_result_as_dict(test_id: TestID) -> Dictionary:
if metric.type == TYPE_FLOAT: if metric.type == TYPE_FLOAT:
var m : float = result.get(metric.name) var m : float = result.get(metric.name)
const sig_figs = 4 const sig_figs = 4
rv[metric.name] = snapped(m, pow(10,floor(log(m)/log(10))-sig_figs+1)) if not is_zero_approx(m):
# Only store metrics if not 0 to reduce JSON size.
if not results_prefix.is_empty():
rv[results_prefix][metric.name] = snapped(m, pow(10,floor(log(m)/log(10))-sig_figs+1))
else:
rv[metric.name] = snapped(m, pow(10,floor(log(m)/log(10))-sig_figs+1))
return rv return rv
func get_results_dict() -> Dictionary: func get_results_dict(results_prefix: String = "") -> Dictionary:
var version_info := Engine.get_version_info() var version_info := Engine.get_version_info()
var version_string: String var version_string: String
if version_info.patch >= 1: if version_info.patch >= 1:
@@ -220,17 +233,13 @@ func get_results_dict() -> Dictionary:
else: else:
version_string = "v%d.%d.%s.%s" % [version_info.major, version_info.minor, version_info.status, version_info.build] version_string = "v%d.%d.%s.%s" % [version_info.major, version_info.minor, version_info.status, version_info.build]
var engine_binary := FileAccess.open(OS.get_executable_path(), FileAccess.READ) # Only list information that doesn't change across benchmark runs on different GPUs,
# as JSON files are merged together. Otherwise, the fields would overwrite each other
# with different information.
var dict := { var dict := {
engine = { engine = {
version = version_string, version = version_string,
version_hash = version_info.hash, version_hash = version_info.hash,
build_type = (
"editor" if OS.has_feature("editor")
else "template_debug" if OS.is_debug_build()
else "template_release"
),
binary_size = engine_binary.get_length(),
}, },
system = { system = {
os = OS.get_name(), os = OS.get_name(),
@@ -243,18 +252,25 @@ func get_results_dict() -> Dictionary:
else "unknown" else "unknown"
), ),
cpu_count = OS.get_processor_count(), cpu_count = OS.get_processor_count(),
gpu_name = RenderingServer.get_video_adapter_name(),
gpu_vendor = RenderingServer.get_video_adapter_vendor(),
} }
} }
var benchmarks := [] var benchmarks := []
for test_id in get_test_ids(): for test_id in get_test_ids():
benchmarks.push_back({ var result_dict := get_test_result_as_dict(test_id, results_prefix)
category = test_id.pretty_category(), # Only write a dictionary if a benchmark was run for it.
name = test_id.pretty_name(), var should_write_dict := false
results = get_test_result_as_dict(test_id), if results_prefix.is_empty():
}) should_write_dict = not result_dict.is_empty()
else:
should_write_dict = not result_dict[results_prefix].is_empty()
if should_write_dict:
benchmarks.push_back({
category = test_id.pretty_category(),
name = test_id.pretty_name(),
results = result_dict,
})
dict.benchmarks = benchmarks dict.benchmarks = benchmarks

53
merge_json.gd Normal file
View File

@@ -0,0 +1,53 @@
# Usage: godot -s merge_json.gd -- json1.md json2.md [...] --output-path output.md
extends SceneTree
func _init() -> void:
if OS.get_cmdline_user_args().is_empty():
print("Usage: godot -s merge_json.gd -- json1.md json2.md [...] --output-path output.md")
quit(1)
var output_path_idx := OS.get_cmdline_user_args().find("--output-path") + 1
if output_path_idx == OS.get_cmdline_user_args().size():
push_error("`--output-path` requires an argument. Aborting.")
quit(1)
var output_path := OS.get_cmdline_user_args()[output_path_idx]
if "--output-path" not in OS.get_cmdline_user_args():
push_error("`--output-path path/to/output.md` must be used at the end of the command line (preceded by paths of individual JSON benchmark run files). Aborting.")
quit(1)
var jsons: Array[Dictionary] = []
for file_idx in OS.get_cmdline_user_args().size():
if file_idx < output_path_idx - 1:
jsons.push_back(JSON.parse_string(FileAccess.get_file_as_string(OS.get_cmdline_user_args()[file_idx])))
print("Saving merged JSON to: %s" % output_path)
var benchmarks_list := []
# Gather list of all benchmarks, without any results for now.
for json in jsons:
for benchmark in json.benchmarks:
var new_dict := {
category = benchmark.category,
name = benchmark.name,
results = {}, # We'll add results later once we've gathered all benchmark names.
}
if not benchmarks_list.has(new_dict):
benchmarks_list.push_back(new_dict)
# Populate results in all benchmarks.
for benchmark_idx in benchmarks_list.size():
var benchmark: Dictionary = benchmarks_list[benchmark_idx]
for json in jsons:
for json_benchmark in json.benchmarks:
if json_benchmark.category == benchmark.category and json_benchmark.name == benchmark.name:
benchmark.results.merge(json_benchmark.results)
var result: Dictionary = jsons[0]
result.benchmarks = benchmarks_list
var file_access := FileAccess.open(output_path, FileAccess.WRITE)
file_access.store_string(JSON.stringify(result))
quit()

225
run-benchmarks.sh Executable file
View File

@@ -0,0 +1,225 @@
#!/usr/bin/env bash
# Run all benchmarks on the server in various configurations.
#
# NOTE: This script is tailored for the dedicated benchmarking server.
# It is not meant for local usage or experimentation.
# `sudo` must be able to work non-interactively for the script to succeed.
set -euo pipefail
IFS=$'\n\t'
export DIR
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Reduce log spam and avoid issues with self-compiled builds crashing:
# https://github.com/godotengine/godot/issues/75409
export MANGOHUD=0
# Set X11 display for headless usage (with a X server running separately).
export DISPLAY=":0"
# Make the command line argument optional without tripping up `set -u`.
ARG1="${1:-''}"
if ! command -v git &> /dev/null; then
echo "ERROR: git must be installed and in PATH."
exit 1
fi
if [[ "$ARG1" == "--help" || "$ARG1" == "-h" ]]; then
echo "Usage: $0 [--skip-build]"
exit
fi
GODOT_REPO_DIR="$DIR/godot"
if [[ ! -d "$GODOT_REPO_DIR/.git" ]]; then
git clone https://github.com/godotengine/godot.git "$GODOT_REPO_DIR"
fi
GODOT_EMPTY_PROJECT_DIR="$DIR/web/godot-empty-project"
if [[ "$ARG1" != "--skip-build" ]]; then
cd "$GODOT_REPO_DIR"
git reset --hard
git clean -qdfx --exclude bin
git pull
if command -v ccache &> /dev/null; then
# Clear ccache to avoid skewing the build time results.
ccache --clear
fi
touch .gdignore
# Measure clean build times for debug and release builds (in milliseconds).
# Also create a `.gdignore` file to prevent Godot from importing resources
# within the Godot Git clone.
# WARNING: Any untracked and ignored files included in the repository will be removed!
BEGIN="$(date +%s%3N)"
PEAK_MEMORY_BUILD_DEBUG=$( (/usr/bin/time -f "%M" scons platform=linuxbsd target=editor optimize=debug module_mono_enabled=no progress=no debug_symbols=yes -j$(nproc) 2>&1 || true) | tail -1)
END="$(date +%s%3N)"
TIME_TO_BUILD_DEBUG="$((END - BEGIN))"
git clean -qdfx --exclude bin
if command -v ccache &> /dev/null; then
# Clear ccache to avoid skewing the build time results.
ccache --clear
fi
touch .gdignore
BEGIN="$(date +%s%3N)"
PEAK_MEMORY_BUILD_RELEASE=$( (/usr/bin/time -f "%M" scons platform=linuxbsd target=template_release optimize=speed lto=full module_mono_enabled=no progress=no debug_symbols=yes -j$(nproc) 2>&1 || true) | tail -1)
END="$(date +%s%3N)"
TIME_TO_BUILD_RELEASE="$((END - BEGIN))"
# FIXME: C# is disabled because the engine crashes on exit after running benchmarks.
#
# Generate Mono glue for C# build to work.
# echo "Generating .NET glue."
# bin/godot.linuxbsd.editor.x86_64.mono --headless --generate-mono-glue modules/mono/glue
# echo "Building .NET assemblies."
# # https://docs.godotengine.org/en/stable/contributing/development/compiling/compiling_with_dotnet.html#nuget-packages
# mkdir -p "$HOME/MyLocalNugetSource"
# # Source may already exist, so allow failure for the command below.
# dotnet nuget add source "$HOME/MyLocalNugetSource" --name MyLocalNugetSource || true
# modules/mono/build_scripts/build_assemblies.py --godot-output-dir=./bin --push-nupkgs-local "$HOME/MyLocalNugetSource"
cd "$DIR"
else
echo "run-benchmarks: Skipping engine build as requested on the command line."
TIME_TO_BUILD_DEBUG=1
TIME_TO_BUILD_RELEASE=1
PEAK_MEMORY_BUILD_DEBUG=1
PEAK_MEMORY_BUILD_RELEASE=1
fi
# Path to the Godot debug binary to run. Used for CPU debug benchmarks.
GODOT_DEBUG="$GODOT_REPO_DIR/bin/godot.linuxbsd.editor.x86_64"
# Path to the Godot release binary to run. Used for CPU release and GPU benchmarks.
# The release binary is assumed to be the same commit as the debug build.
# Things will break if this is not the case.
GODOT_RELEASE="$GODOT_REPO_DIR/bin/godot.linuxbsd.template_release.x86_64"
COMMIT_HASH="$($GODOT_DEBUG --version | rev | cut --delimiter="." --field="1" | rev)"
DATE="$(date +'%Y-%m-%d')"
# Measure average engine startup + shutdown times over 20 runs (in milliseconds),
# as well as peak memory usage.
# Perform a warmup run first.
echo "Performing debug warmup run."
$GODOT_DEBUG --audio-driver Dummy --path "$GODOT_EMPTY_PROJECT_DIR" --quit || true
TOTAL=0
for _ in {0..19}; do
BEGIN="$(date +%s%3N)"
echo "Performing benchmark debug startup/shutdown run."
$GODOT_DEBUG --audio-driver Dummy --path "$GODOT_EMPTY_PROJECT_DIR" --quit || true
END="$(date +%s%3N)"
TOTAL="$((TOTAL + END - BEGIN))"
done
TIME_TO_STARTUP_SHUTDOWN_DEBUG="$((TOTAL / 20))"
echo "Performing benchmark debug peak memory usage run."
PEAK_MEMORY_STARTUP_SHUTDOWN_DEBUG=$(/usr/bin/time -f "%M" "$GODOT_DEBUG" --audio-driver Dummy --path "$GODOT_EMPTY_PROJECT_DIR" --quit 2>&1 | tail -1)
# Perform a warmup run first.
echo "Performing release warmup run."
$GODOT_RELEASE --audio-driver Dummy --path "$GODOT_EMPTY_PROJECT_DIR" --quit || true
TOTAL=0
for _ in {0..19}; do
BEGIN="$(date +%s%3N)"
echo "Performing benchmark release startup/shutdown run."
$GODOT_RELEASE --audio-driver Dummy --path "$GODOT_EMPTY_PROJECT_DIR" --quit || true
END="$(date +%s%3N)"
TOTAL="$((TOTAL + END - BEGIN))"
done
TIME_TO_STARTUP_SHUTDOWN_RELEASE="$((TOTAL / 20))"
echo "Performing benchmark release peak memory usage run."
PEAK_MEMORY_STARTUP_SHUTDOWN_RELEASE=$(/usr/bin/time -f "%M" "$GODOT_RELEASE" --audio-driver Dummy --path "$GODOT_EMPTY_PROJECT_DIR" --quit 2>&1 | tail -1)
# Import resources and build C# solutions in the project (required to run it).
echo "Performing resource importing and C# solution building."
$GODOT_DEBUG --headless --editor --build-solutions --quit-after 2
# Run CPU benchmarks.
echo "Running CPU benchmarks."
$GODOT_DEBUG --audio-driver Dummy -- --run-benchmarks --exclude-benchmarks="rendering/*" --save-json="/tmp/cpu_debug.md" --json-results-prefix="cpu_debug"
$GODOT_RELEASE --audio-driver Dummy -- --run-benchmarks --exclude-benchmarks="rendering/*" --save-json="/tmp/cpu_release.md" --json-results-prefix="cpu_release"
# Run GPU benchmarks.
# TODO: Run on different GPUs.
echo "Running GPU benchmarks."
$GODOT_RELEASE --audio-driver Dummy -- --run-benchmarks --include-benchmarks="rendering/*" --save-json="/tmp/amd.md" --json-results-prefix="amd"
#$GODOT_RELEASE --audio-driver Dummy -- --run-benchmarks --include-benchmarks="rendering/*" --save-json="/tmp/intel.md" --json-results-prefix="intel"
#$GODOT_RELEASE --audio-driver Dummy -- --run-benchmarks --include-benchmarks="rendering/*" --save-json="/tmp/nvidia.md" --json-results-prefix="nvidia"
rm -rf /tmp/godot-benchmarks-results/
# Clone a copy of the repository so we can push the new JSON files to it.
# The website build is performed by GitHub Actions on the `main` branch of the repository below,
# so we only push files to it and do nothing else.
git clone git@github.com:godotengine/godot-benchmarks-results.git /tmp/godot-benchmarks-results/
cd /tmp/godot-benchmarks-results/
# Merge benchmark run JSONs together.
# Use editor build as release build errors due to missing PCK file.
echo "Merging JSON files together."
$GODOT_DEBUG --path "$DIR" --script merge_json.gd -- /tmp/cpu_debug.md /tmp/cpu_release.md /tmp/amd.md --output-path /tmp/merged.md
#$GODOT_DEBUG --path "$DIR" --script merge_json.gd -- /tmp/cpu_debug.md /tmp/cpu_release.md /tmp/amd.md /tmp/intel.md /tmp/nvidia.md --output-path /tmp/merged.md
OUTPUT_PATH="/tmp/godot-benchmarks-results/${DATE}_${COMMIT_HASH}.md"
rm -f "$OUTPUT_PATH"
# Strip debugging symbols for fair binary size comparison.
# Do this after Godot is run so we can have useful crash backtraces
# if the engine crashes while running benchmarks.
strip "$GODOT_DEBUG" "$GODOT_RELEASE"
BINARY_SIZE_DEBUG="$(stat --printf="%s" "$GODOT_DEBUG")"
BINARY_SIZE_RELEASE="$(stat --printf="%s" "$GODOT_RELEASE")"
# Add extra JSON at the end of the merged JSON. We assume the merged JSON has no
# newline at the end of file, as Godot writes it. To append more data to the
# JSON dictionary, we remove the last `}` character and add a `,` instead.
echo "Appending extra JSON at the end of the merged JSON."
EXTRA_JSON=$(cat << EOF
"binary_size": {
"debug": $BINARY_SIZE_DEBUG,
"release": $BINARY_SIZE_RELEASE
},
"build_time": {
"debug": $TIME_TO_BUILD_DEBUG,
"release": $TIME_TO_BUILD_RELEASE
},
"build_peak_memory_usage": {
"debug": $PEAK_MEMORY_BUILD_DEBUG,
"release": $PEAK_MEMORY_BUILD_RELEASE
},
"empty_project_startup_shutdown_time": {
"debug": $TIME_TO_STARTUP_SHUTDOWN_DEBUG,
"release": $TIME_TO_STARTUP_SHUTDOWN_RELEASE
},
"empty_project_startup_shutdown_peak_memory_usage": {
"debug": $PEAK_MEMORY_STARTUP_SHUTDOWN_DEBUG,
"release": $PEAK_MEMORY_STARTUP_SHUTDOWN_RELEASE
}
EOF
)
echo "$(head -c -1 /tmp/merged.md),$EXTRA_JSON}" > "$OUTPUT_PATH"
# Build website files after running all benchmarks, so that benchmarks
# appear on the web interface.
echo "Pushing results to godot-benchmarks repository."
git add .
git config --local user.name "Godot Benchmarks"
git config --local user.email "godot-benchmarks@example.com"
git commit --no-gpg-sign --message "Deploy benchmark results of $COMMIT_HASH (master at $DATE)
https://github.com/godotengine/godot/commit/$COMMIT_HASH"
git push
cd "$DIR"
echo "Success."

4
server/README.md Normal file
View File

@@ -0,0 +1,4 @@
This folder contains configuration files used on the benchmarking server. This
can be used for reference purposes or to set up a new benchmarking server.
A Linux username of `godot` on the system is assumed in these scripts.

View File

@@ -0,0 +1,27 @@
Section "ServerLayout"
Identifier "default-layout"
Screen 0 "screen"
EndSection
Section "Device"
Identifier "amd"
Driver "amdgpu"
BusID "PCI:1:0:0"
EndSection
Section "Monitor"
Identifier "monitor"
Modeline "1920x1200_60.00" 193.25 1920 2056 2256 2592 1200 1203 1209 1245 -hsync +vsync
Option "PreferredMode" "1920x1080_60.00"
EndSection
Section "Screen"
Identifier "screen"
Device "amd"
Monitor "monitor"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1920x1080"
EndSubSection
EndSection

View File

@@ -0,0 +1,8 @@
[Service]
ExecStartPre=/usr/bin/git -C /home/godot/godot-benchmarks/ reset --hard
ExecStartPre=/usr/bin/git -C /home/godot/godot-benchmarks/ clean -qdfx
ExecStartPre=/usr/bin/git -C /home/godot/godot-benchmarks/ pull
ExecStart=/home/godot/godot-benchmarks/run-benchmarks.sh
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Run godot-benchmarks
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,6 @@
[Service]
ExecStart=xinit
Restart=always
[Install]
WantedBy=default.target

0
web/.gdignore Normal file
View File

1
web/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
static/thirdparty/* linguist-vendored

10
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Output HTML files
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

38
web/README.md Normal file
View File

@@ -0,0 +1,38 @@
# Web interface for benchmark results
This website is built with the [Hugo](https://gohugo.io/) static site generator.
It's designed to work **exclusively** with the [`run-benchmarks.sh` script](../run-benchmarks.sh),
which runs benchmarks on a dedicated server with various GPU models.
## How it works
- Using the [`run-benchmarks.sh` script](../run-benchmarks.sh), benchmark data
is collected and saved to a single JSON file for each engine commit, with 5 runs:
- CPU (debug template)
- CPU (release template)
- GPU (AMD, release template)
<!--
- GPU (Intel, release template)
- GPU (NVIDIA, release template)
-->
- [Hugo](https://gohugo.io/) is used to create a homepage listing recent
benchmarked commits, plus a single page per engine commit. This allows linking
to individual benchmarked commit in a future-proof way.
- [ApexCharts](https://apexcharts.com/) is used to draw graphs on the homepage.
- [Water.css](https://watercss.kognise.dev/) is used to provide styling,
including automatic dark theme support.
### Building
## Development
- Create JSON data or fetch existing JSON data from the live website.
- Save this JSON data to the `content` folder with the following naming convention:
`YYYY-MM-DD_hash.json` where `hash` is a 9-character Git commit hash of the
Godot build used (truncated from a full commit hash).
- Run `hugo server`.
## Production
- Follow the same steps as in the **Development** section above.
- Run `hugo --minify`.

0
web/content/_index.md Normal file
View File

View File

@@ -0,0 +1,14 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
run/main_scene="res://test.tscn"
config/features=PackedStringArray("4.2")

View File

@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://drtld8p78sdv6"]
[node name="Node" type="Node"]

6
web/hugo.toml Normal file
View File

@@ -0,0 +1,6 @@
baseURL = 'https://benchmarks.godotengine.org/'
languageCode = 'en-us'
title = 'Godot Benchmarks'
# Disable generation of pages not relevant for godot-benchmarks.
disableKinds = ['taxonomy', 'term', 'sitemap', 'RSS']

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" con tent="width=device-width, initial-scale=1">
<meta name="theme-color" content="#3d8fcc">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
<link rel="canonical" href="{{ .Permalink }}">
<link rel="stylesheet" href="{{ .Site.BaseURL }}thirdparty/apexcharts/apexcharts.css">
<link rel="stylesheet" href="{{ .Site.BaseURL }}thirdparty/water.css">
<link rel="stylesheet" href="{{ .Site.BaseURL }}main.css">
<link rel="manifest" href="{{ .Site.BaseURL }}site.webmanifest">
</head>
<body>
<header>
<a class="site-title" href="{{ .Site.BaseURL }}">Godot Benchmarks</a>
</header>
{{ block "main" . }}
{{ end }}
<footer>
© 2022-present Godot Engine contributors <a href="https://github.com/godotengine/godot-benchmarks/tree/main/web">Website source code</a>
</footer>
<script src="{{ .Site.BaseURL }}thirdparty/apexcharts/apexcharts.min.js"></script>
<script>
if (window.location.href.includes("://godotengine.github.io/godot-benchmarks-results/")) {
// Perform redirect.
window.location.href = window.location.href.replace('://godotengine.github.io/godot-benchmarks-results/','://benchmarks.godotengine.org/');
}
</script>
{{ block "javascript" . }}{{end}}
</body>
</html>

View File

@@ -0,0 +1,170 @@
{{ define "main" }}
<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>
<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>
{{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>
{{end}}
</div>
</div>
<h2>System information</h2>
<table class="table-first-column-align-right">
<tr>
<td>CPU</td>
<td>Intel Core i5-12400</td>
</tr>
<tr>
<td>GPUs</td>
<td>
🔴 AMD Radeon RX 550
<!-- 🔵 Intel UHD Graphics 730 🔴 AMD Radeon RX 550 🟢 NVIDIA GeForce GT 1030 -->
</td>
</tr>
<tr>
<td>Operating system</td>
<td>Fedora 38 64-bit</td>
</tr>
</table>
<h2>Engine information</h2>
<table class="table-first-column-align-right">
<thead>
<td>Build type</td>
<td>
<span style="opacity: 0.65"><sub><abbr title="SCons flags: target=editor optimize=debug">Debug</abbr></sub> Debug editor<br></span>
<sub><abbr title="SCons flags: target=template_release optimize=speed lto=full">Release</abbr></sub> Release export template
</td>
</thead>
<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
</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
</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
</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
</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
</td>
</tr>
</table>
<h2>Benchmark results</h2>
<em>For all values, lower is better.</em>
<details open>
<summary><strong>CPU</strong></summary>
<table class="table-first-column-align-right">
<thead>
<tr>
<td>Name</td>
<td>Idle</td>
<td>Physics</td>
<td><abbr title="Time spent setting up the scene or executing the script">Main Thread Time</abbr></td>
</tr>
</thead>
<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 }}
{{ if gt .results.cpu_debug.time 0 }}
<tr>
<td><sub style="opacity: 0.65">{{ .category }}</sub><br><strong>{{ .name }}</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>
<sub>Release</sub> {{ .results.cpu_release.idle }} <sub>mspf</sub>
{{ end }}
</td>
<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>
{{ end }}
</td>
<td>
{{ if gt .results.cpu_debug.time 0 }}
<span style="opacity: 0.65"><sub>Debug</sub> {{ .results.cpu_debug.time }} <sub>ms</sub><br></span>
<sub>Release</sub> {{ .results.cpu_release.time }} <sub>ms</sub>
{{ end }}
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</details>
<details open>
<summary><strong>GPU</strong></summary>
<table class="table-first-column-align-right">
<thead>
<tr>
<td>Name</td>
<td>Render CPU</td>
<td>Render GPU</td>
</tr>
</thead>
<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 }}
{{ if gt .results.amd.render_cpu 0 }}
<tr>
<td><sub style="opacity: 0.65">{{ .category }}</sub><br><strong>{{ .name }}</strong></td>
<td>
{{ if gt .results.amd.render_cpu 0 }}
<!-- <span title="Intel HD Graphics">🔵</span> {{ .results.intel.render_cpu }} <sub>mspf</sub><br> -->
<span title="AMD Radeon RX 550">🔴</span> {{ .results.amd.render_cpu }} <sub>mspf</sub><!-- &nbsp; -->
<!-- span title="NVIDIA GeForce GT 1030">🟢</span> {{ .results.nvidia.render_cpu }} <sub>mspf</sub> -->
{{ end }}
</td>
<td>
{{ if gt .results.amd.render_gpu 0 }}
<!-- <span title="Intel HD Graphics">🔵</span> {{ .results.intel.render_gpu }} <sub>mspf</sub><br> -->
<span title="AMD Radeon RX 550">🔴</span> {{ .results.amd.render_gpu }} <sub>mspf</sub><!-- &nbsp; -->
<!-- <span title="NVIDIA GeForce GT 1030">🟢</span> {{ .results.nvidia.render_gpu }} <sub>mspf</sub> -->
{{ end }}
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</details>
{{ end }}

78
web/layouts/index.html Normal file
View File

@@ -0,0 +1,78 @@
{{ define "main" }}
<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>.
Benchmarks are run on a daily basis to track performance improvements and
regressions over time.
</p>
<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>
{{ end }}
</ul>
<hr>
<h2>Benchmarking machine</h2>
<p>
To allow for direct GPU access and avoid performance fluctuations due to the
use of a shared host, benchmarks are run on a dedicated server with the
following hardware:
</p>
<table class="table-first-column-align-right">
<tr>
<td>CPU</td>
<td>Intel Core i5-12400</td>
</tr>
<tr>
<td>RAM</td>
<td>16 GB (2×8 GB DDR4-3000 CL16)</td>
</tr>
<tr>
<td>SSD</td>
<td>Samsung 980 500 GB (M.2)</td>
</tr>
<!--
<tr>
<td>GPU 1 (integrated)</td>
<td>🔵 Intel UHD Graphics 730</td>
</tr>
-->
<tr>
<td>GPU (dedicated)</td>
<td>🔴 AMD Radeon RX 550</td>
</tr>
<!--
<tr>
<td>GPU 3 (dedicated)</td>
<td>🟢 NVIDIA GeForce GT 1030</td>
</tr>
-->
<tr>
<td>Operating system</td>
<td>Fedora 39 x86_64</td>
</tr>
</table>
<p>
All core and memory clocks are kept at stock frequencies. When running
benchmarks (except for compiling the engine), The CPU governor is set to
<code>performance</code> to improve result consistency. The OS is kept in a
default configuration as closely as posible. The machine is not running any
other tasks in parallel this page is served from GitHub Pages.
</p>
<p>
Benchmarks that make use of the GPU are run 3 times (once on each GPU with a release build).
Benchmarks that don't make use of the GPU are run twice (once with a debug build, once with a release build).
</p>
</main>
{{ end }}
{{ define "javascript" }}
<script>
</script>
{{ end }}

39
web/static/main.css Normal file
View File

@@ -0,0 +1,39 @@
/* See `thirdparty/water.css` for CSS variables that can be used here. */
p, li {
line-height: 1.6;
}
footer {
text-align: center;
}
header {
background-color: var(--background);
border-radius: 0 0 6px 6px;
padding: .75rem 2rem;
margin-top: -1.25rem;
margin-bottom: 2rem;
}
.site-title {
color: var(--text-bright);
font-weight: bold;
font-size: 1.25rem;
border-radius: 6px;
}
/* Automatically resize the left column to take as little space as required. */
.table-first-column-align-right {
width: auto;
}
/* Align first column to the right to improve readability in some situations. */
.table-first-column-align-right tr td:first-of-type {
text-align: right;
width: 1%;
white-space: nowrap;
/* Style the first column as headings. */
font-weight: bold;
}

View File

@@ -0,0 +1,12 @@
{
"short_name": "Godot Benchmarks",
"name": "Godot Benchmarks",
"icons": [{
"src": "icon.png",
"type": "image/png",
"sizes": "192x192"
}],
"start_url": "/godot-benchmarks/",
"background_color": "#223",
"theme_color": "#3d8fcc"
}

View File

@@ -0,0 +1,688 @@
.apexcharts-canvas {
position: relative;
user-select: none;
/* cannot give overflow: hidden as it will crop tooltips which overflow outside chart area */
}
/* scrollbar is not visible by default for legend, hence forcing the visibility */
.apexcharts-canvas ::-webkit-scrollbar {
-webkit-appearance: none;
width: 6px;
}
.apexcharts-canvas ::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, .5);
box-shadow: 0 0 1px rgba(255, 255, 255, .5);
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5);
}
.apexcharts-inner {
position: relative;
}
.apexcharts-text tspan {
font-family: inherit;
}
.legend-mouseover-inactive {
transition: 0.15s ease all;
opacity: 0.20;
}
.apexcharts-series-collapsed {
opacity: 0;
}
.apexcharts-tooltip {
border-radius: 5px;
box-shadow: 2px 2px 6px -4px #999;
cursor: default;
font-size: 14px;
left: 62px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
white-space: nowrap;
z-index: 12;
transition: 0.15s ease all;
}
.apexcharts-tooltip.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-tooltip.apexcharts-theme-light {
border: 1px solid #e3e3e3;
background: rgba(255, 255, 255, 0.96);
}
.apexcharts-tooltip.apexcharts-theme-dark {
color: #fff;
background: rgba(30, 30, 30, 0.8);
}
.apexcharts-tooltip * {
font-family: inherit;
}
.apexcharts-tooltip-title {
padding: 6px;
font-size: 15px;
margin-bottom: 4px;
}
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
background: #ECEFF1;
border-bottom: 1px solid #ddd;
}
.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {
background: rgba(0, 0, 0, 0.7);
border-bottom: 1px solid #333;
}
.apexcharts-tooltip-text-y-value,
.apexcharts-tooltip-text-goals-value,
.apexcharts-tooltip-text-z-value {
display: inline-block;
font-weight: 600;
margin-left: 5px;
}
.apexcharts-tooltip-title:empty,
.apexcharts-tooltip-text-y-label:empty,
.apexcharts-tooltip-text-y-value:empty,
.apexcharts-tooltip-text-goals-label:empty,
.apexcharts-tooltip-text-goals-value:empty,
.apexcharts-tooltip-text-z-value:empty {
display: none;
}
.apexcharts-tooltip-text-y-value,
.apexcharts-tooltip-text-goals-value,
.apexcharts-tooltip-text-z-value {
font-weight: 600;
}
.apexcharts-tooltip-text-goals-label,
.apexcharts-tooltip-text-goals-value {
padding: 6px 0 5px;
}
.apexcharts-tooltip-goals-group,
.apexcharts-tooltip-text-goals-label,
.apexcharts-tooltip-text-goals-value {
display: flex;
}
.apexcharts-tooltip-text-goals-label:not(:empty),
.apexcharts-tooltip-text-goals-value:not(:empty) {
margin-top: -6px;
}
.apexcharts-tooltip-marker {
width: 12px;
height: 12px;
position: relative;
top: 0px;
margin-right: 10px;
border-radius: 50%;
}
.apexcharts-tooltip-series-group {
padding: 0 10px;
display: none;
text-align: left;
justify-content: left;
align-items: center;
}
.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {
opacity: 1;
}
.apexcharts-tooltip-series-group.apexcharts-active,
.apexcharts-tooltip-series-group:last-child {
padding-bottom: 4px;
}
.apexcharts-tooltip-series-group-hidden {
opacity: 0;
height: 0;
line-height: 0;
padding: 0 !important;
}
.apexcharts-tooltip-y-group {
padding: 6px 0 5px;
}
.apexcharts-tooltip-box, .apexcharts-custom-tooltip {
padding: 4px 8px;
}
.apexcharts-tooltip-boxPlot {
display: flex;
flex-direction: column-reverse;
}
.apexcharts-tooltip-box>div {
margin: 4px 0;
}
.apexcharts-tooltip-box span.value {
font-weight: bold;
}
.apexcharts-tooltip-rangebar {
padding: 5px 8px;
}
.apexcharts-tooltip-rangebar .category {
font-weight: 600;
color: #777;
}
.apexcharts-tooltip-rangebar .series-name {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
.apexcharts-xaxistooltip {
opacity: 0;
padding: 9px 10px;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #ECEFF1;
border: 1px solid #90A4AE;
transition: 0.15s ease all;
}
.apexcharts-xaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.5);
color: #fff;
}
.apexcharts-xaxistooltip:after,
.apexcharts-xaxistooltip:before {
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.apexcharts-xaxistooltip:after {
border-color: rgba(236, 239, 241, 0);
border-width: 6px;
margin-left: -6px;
}
.apexcharts-xaxistooltip:before {
border-color: rgba(144, 164, 174, 0);
border-width: 7px;
margin-left: -7px;
}
.apexcharts-xaxistooltip-bottom:after,
.apexcharts-xaxistooltip-bottom:before {
bottom: 100%;
}
.apexcharts-xaxistooltip-top:after,
.apexcharts-xaxistooltip-top:before {
top: 100%;
}
.apexcharts-xaxistooltip-bottom:after {
border-bottom-color: #ECEFF1;
}
.apexcharts-xaxistooltip-bottom:before {
border-bottom-color: #90A4AE;
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after {
border-bottom-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {
border-bottom-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-top:after {
border-top-color: #ECEFF1
}
.apexcharts-xaxistooltip-top:before {
border-top-color: #90A4AE;
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after {
border-top-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {
border-top-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-yaxistooltip {
opacity: 0;
padding: 4px 10px;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #ECEFF1;
border: 1px solid #90A4AE;
}
.apexcharts-yaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.5);
color: #fff;
}
.apexcharts-yaxistooltip:after,
.apexcharts-yaxistooltip:before {
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.apexcharts-yaxistooltip:after {
border-color: rgba(236, 239, 241, 0);
border-width: 6px;
margin-top: -6px;
}
.apexcharts-yaxistooltip:before {
border-color: rgba(144, 164, 174, 0);
border-width: 7px;
margin-top: -7px;
}
.apexcharts-yaxistooltip-left:after,
.apexcharts-yaxistooltip-left:before {
left: 100%;
}
.apexcharts-yaxistooltip-right:after,
.apexcharts-yaxistooltip-right:before {
right: 100%;
}
.apexcharts-yaxistooltip-left:after {
border-left-color: #ECEFF1;
}
.apexcharts-yaxistooltip-left:before {
border-left-color: #90A4AE;
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after {
border-left-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {
border-left-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-right:after {
border-right-color: #ECEFF1;
}
.apexcharts-yaxistooltip-right:before {
border-right-color: #90A4AE;
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after {
border-right-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {
border-right-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip.apexcharts-active {
opacity: 1;
}
.apexcharts-yaxistooltip-hidden {
display: none;
}
.apexcharts-xcrosshairs,
.apexcharts-ycrosshairs {
pointer-events: none;
opacity: 0;
transition: 0.15s ease all;
}
.apexcharts-xcrosshairs.apexcharts-active,
.apexcharts-ycrosshairs.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-ycrosshairs-hidden {
opacity: 0;
}
.apexcharts-selection-rect {
cursor: move;
}
.svg_select_boundingRect, .svg_select_points_rot {
pointer-events: none;
opacity: 0;
visibility: hidden;
}
.apexcharts-selection-rect + g .svg_select_boundingRect,
.apexcharts-selection-rect + g .svg_select_points_rot {
opacity: 0;
visibility: hidden;
}
.apexcharts-selection-rect + g .svg_select_points_l,
.apexcharts-selection-rect + g .svg_select_points_r {
cursor: ew-resize;
opacity: 1;
visibility: visible;
}
.svg_select_points {
fill: #efefef;
stroke: #333;
rx: 2;
}
.apexcharts-svg.apexcharts-zoomable.hovering-zoom {
cursor: crosshair
}
.apexcharts-svg.apexcharts-zoomable.hovering-pan {
cursor: move
}
.apexcharts-zoom-icon,
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon,
.apexcharts-reset-icon,
.apexcharts-pan-icon,
.apexcharts-selection-icon,
.apexcharts-menu-icon,
.apexcharts-toolbar-custom-icon {
cursor: pointer;
width: 20px;
height: 20px;
line-height: 24px;
color: #6E8192;
text-align: center;
}
.apexcharts-zoom-icon svg,
.apexcharts-zoomin-icon svg,
.apexcharts-zoomout-icon svg,
.apexcharts-reset-icon svg,
.apexcharts-menu-icon svg {
fill: #6E8192;
}
.apexcharts-selection-icon svg {
fill: #444;
transform: scale(0.76)
}
.apexcharts-theme-dark .apexcharts-zoom-icon svg,
.apexcharts-theme-dark .apexcharts-zoomin-icon svg,
.apexcharts-theme-dark .apexcharts-zoomout-icon svg,
.apexcharts-theme-dark .apexcharts-reset-icon svg,
.apexcharts-theme-dark .apexcharts-pan-icon svg,
.apexcharts-theme-dark .apexcharts-selection-icon svg,
.apexcharts-theme-dark .apexcharts-menu-icon svg,
.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg {
fill: #f3f4f5;
}
.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg {
fill: #008FFB;
}
.apexcharts-theme-light .apexcharts-selection-icon:not(.apexcharts-selected):hover svg,
.apexcharts-theme-light .apexcharts-zoom-icon:not(.apexcharts-selected):hover svg,
.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,
.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg,
.apexcharts-theme-light .apexcharts-reset-icon:hover svg,
.apexcharts-theme-light .apexcharts-menu-icon:hover svg {
fill: #333;
}
.apexcharts-selection-icon,
.apexcharts-menu-icon {
position: relative;
}
.apexcharts-reset-icon {
margin-left: 5px;
}
.apexcharts-zoom-icon,
.apexcharts-reset-icon,
.apexcharts-menu-icon {
transform: scale(0.85);
}
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon {
transform: scale(0.7)
}
.apexcharts-zoomout-icon {
margin-right: 3px;
}
.apexcharts-pan-icon {
transform: scale(0.62);
position: relative;
left: 1px;
top: 0px;
}
.apexcharts-pan-icon svg {
fill: #fff;
stroke: #6E8192;
stroke-width: 2;
}
.apexcharts-pan-icon.apexcharts-selected svg {
stroke: #008FFB;
}
.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {
stroke: #333;
}
.apexcharts-toolbar {
position: absolute;
z-index: 11;
max-width: 176px;
text-align: right;
border-radius: 3px;
padding: 0px 6px 2px 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.apexcharts-menu {
background: #fff;
position: absolute;
top: 100%;
border: 1px solid #ddd;
border-radius: 3px;
padding: 3px;
right: 10px;
opacity: 0;
min-width: 110px;
transition: 0.15s ease all;
pointer-events: none;
}
.apexcharts-menu.apexcharts-menu-open {
opacity: 1;
pointer-events: all;
transition: 0.15s ease all;
}
.apexcharts-menu-item {
padding: 6px 7px;
font-size: 12px;
cursor: pointer;
}
.apexcharts-theme-light .apexcharts-menu-item:hover {
background: #eee;
}
.apexcharts-theme-dark .apexcharts-menu {
background: rgba(0, 0, 0, 0.7);
color: #fff;
}
@media screen and (min-width: 768px) {
.apexcharts-canvas:hover .apexcharts-toolbar {
opacity: 1;
}
}
.apexcharts-datalabel.apexcharts-element-hidden {
opacity: 0;
}
.apexcharts-pie-label,
.apexcharts-datalabels,
.apexcharts-datalabel,
.apexcharts-datalabel-label,
.apexcharts-datalabel-value {
cursor: default;
pointer-events: none;
}
.apexcharts-pie-label-delay {
opacity: 0;
animation-name: opaque;
animation-duration: 0.3s;
animation-fill-mode: forwards;
animation-timing-function: ease;
}
.apexcharts-canvas .apexcharts-element-hidden {
opacity: 0;
}
.apexcharts-hide .apexcharts-series-points {
opacity: 0;
}
.apexcharts-gridline,
.apexcharts-annotation-rect,
.apexcharts-tooltip .apexcharts-marker,
.apexcharts-area-series .apexcharts-area,
.apexcharts-line,
.apexcharts-zoom-rect,
.apexcharts-toolbar svg,
.apexcharts-area-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,
.apexcharts-line-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,
.apexcharts-radar-series path,
.apexcharts-radar-series polygon {
pointer-events: none;
}
/* markers */
.apexcharts-marker {
transition: 0.15s ease all;
}
@keyframes opaque {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Resize generated styles */
@keyframes resizeanim {
from {
opacity: 0;
}
to {
opacity: 0;
}
}
.resize-triggers {
animation: 1ms resizeanim;
visibility: hidden;
opacity: 0;
}
.resize-triggers,
.resize-triggers>div,
.contract-trigger:before {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.resize-triggers>div {
background: #eee;
overflow: auto;
}
.contract-trigger:before {
width: 200%;
height: 200%;
}

File diff suppressed because one or more lines are too long

1689
web/static/thirdparty/water.css vendored Normal file

File diff suppressed because it is too large Load Diff