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
101 changes: 101 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Benchmark

on:
push:
branches: [ dev ]
workflow_dispatch:
inputs:
duration:
description: Benchmark duration per preset (seconds)
required: false
default: '30'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: main

jobs:
benchmark:
permissions:
contents: read
statuses: write
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 90
steps:
- uses: actions/checkout@v4

- name: Setup Nix
uses: ./.github/actions/setup-nix

- name: Start headless Wayland compositor
run: |
mkdir -p /tmp/runtime-runner
chmod 700 /tmp/runtime-runner
export XDG_RUNTIME_DIR=/tmp/runtime-runner
nix develop --command weston --socket=headless --backend=headless-backend.so --width=1280 --height=720 &
{
echo "WAYLAND_DISPLAY=headless"
echo "XDG_RUNTIME_DIR=/tmp/runtime-runner"
} >> "$GITHUB_ENV"
sleep 5

- name: Run benchmark suite
id: run_benchmark
env:
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache-global
XDG_RUNTIME_DIR: /tmp/runtime-runner
WAYLAND_DISPLAY: headless
ZIGCRAFT_SAFE_MODE: '1'
BENCHMARK_DURATION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.duration || '30' }}
run: |
LVP_PATH=$(nix build --no-link --print-out-paths nixpkgs#mesa.drivers)/share/vulkan/icd.d/lvp_icd.x86_64.json
LAYER_PATH=$(nix build --no-link --print-out-paths nixpkgs#vulkan-validation-layers)/share/vulkan/explicit_layer.d
export VK_ICD_FILENAMES=$LVP_PATH
export VK_LAYER_PATH=$LAYER_PATH
nix develop --command bash scripts/run_benchmark.sh --duration "$BENCHMARK_DURATION" --presets low,medium,high --output-dir benchmark-results

- name: Compare against baseline
id: compare
run: |
for preset in low medium high; do
bash scripts/compare_benchmarks.sh docs/benchmarks/baseline.json "benchmark-results/${preset}.json" --preset "$preset"
done

- name: Upload benchmark artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: benchmark-results
retention-days: 30

- name: Publish commit status
if: always()
uses: actions/github-script@v7
env:
BENCHMARK_RUN_OUTCOME: ${{ steps.run_benchmark.outcome }}
BENCHMARK_COMPARE_OUTCOME: ${{ steps.compare.outcome }}
with:
script: |
const runOutcome = process.env.BENCHMARK_RUN_OUTCOME;
const compareOutcome = process.env.BENCHMARK_COMPARE_OUTCOME;
const failed = runOutcome === 'failure' || compareOutcome === 'failure';
const description = failed
? 'Benchmark regression or runtime failure'
: compareOutcome === 'skipped'
? 'Benchmark completed (baseline placeholder)'
: 'Benchmark completed';

await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: failed ? 'failure' : 'success',
context: 'performance/benchmark',
description,
});
56 changes: 56 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ pub fn build(b: *std.Build) void {
const screenshot_path = b.option([]const u8, "screenshot-path", "Capture a PPM screenshot after N frames and exit (menu mode)") orelse "";
options.addOption([]const u8, "screenshot_path", screenshot_path);

const benchmark = b.option(bool, "benchmark", "Enable benchmark mode") orelse false;
options.addOption(bool, "benchmark", benchmark);

const benchmark_preset = b.option([]const u8, "benchmark-preset", "Graphics preset to benchmark (low, medium, high, ultra, extreme)") orelse "medium";
options.addOption([]const u8, "benchmark_preset", benchmark_preset);

const benchmark_duration = b.option(u32, "benchmark-duration", "Benchmark duration in seconds") orelse 60;
options.addOption(u32, "benchmark_duration", benchmark_duration);

const benchmark_output = b.option([]const u8, "benchmark-output", "Benchmark JSON output path") orelse "benchmark_results.json";
options.addOption([]const u8, "benchmark_output", benchmark_output);

const zig_math = b.createModule(.{
.root_source_file = b.path("libs/zig-math/math.zig"),
.target = target,
Expand Down Expand Up @@ -69,6 +81,50 @@ pub fn build(b: *std.Build) void {
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);

const benchmark_options = b.addOptions();
benchmark_options.addOption(bool, "debug_shadows", enable_debug_shadows);
benchmark_options.addOption(bool, "smoke_test", false);
benchmark_options.addOption(bool, "skip_present", true);
benchmark_options.addOption([]const u8, "screenshot_path", "");
benchmark_options.addOption(bool, "benchmark", true);
benchmark_options.addOption([]const u8, "benchmark_preset", benchmark_preset);
benchmark_options.addOption(u32, "benchmark_duration", benchmark_duration);
benchmark_options.addOption([]const u8, "benchmark_output", benchmark_output);

const benchmark_root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
benchmark_root_module.addImport("zig-math", zig_math);
benchmark_root_module.addImport("zig-noise", zig_noise);
benchmark_root_module.addOptions("build_options", benchmark_options);
benchmark_root_module.addIncludePath(b.path("libs/stb"));

const benchmark_exe = b.addExecutable(.{
.name = "benchmark",
.root_module = benchmark_root_module,
});

benchmark_exe.linkLibC();
benchmark_exe.addCSourceFile(.{
.file = b.path("libs/stb/stb_image_impl.c"),
.flags = &.{"-std=c99"},
});

benchmark_exe.linkSystemLibrary("sdl3");
benchmark_exe.linkSystemLibrary("vulkan");

b.installArtifact(benchmark_exe);

const benchmark_run_cmd = b.addRunArtifact(benchmark_exe);
benchmark_run_cmd.step.dependOn(b.getInstallStep());
benchmark_run_cmd.step.dependOn(&shader_cmd.step);
benchmark_run_cmd.setCwd(b.path("."));

const benchmark_step = b.step("benchmark", "Run benchmark harness");
benchmark_step.dependOn(&benchmark_run_cmd.step);

const test_root_module = b.createModule(.{
.root_source_file = b.path("src/tests.zig"),
.target = target,
Expand Down
81 changes: 81 additions & 0 deletions docs/benchmarks/baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"generated": false,
"presets": {
"low": {
"preset": "low",
"render_distance": 0,
"frames": 0,
"duration_s": 0,
"fps": {
"min": 0,
"avg": 0,
"max": 0,
"p1": 0,
"p5": 0,
"p50": 0,
"p95": 0,
"p99": 0
},
"cpu_ms_avg": 0,
"gpu_ms": {
"shadow_avg": 0,
"opaque_avg": 0,
"total_avg": 0
},
"draw_calls_avg": 0,
"vertices_avg": 0,
"chunks_rendered_avg": 0
},
"medium": {
"preset": "medium",
"render_distance": 0,
"frames": 0,
"duration_s": 0,
"fps": {
"min": 0,
"avg": 0,
"max": 0,
"p1": 0,
"p5": 0,
"p50": 0,
"p95": 0,
"p99": 0
},
"cpu_ms_avg": 0,
"gpu_ms": {
"shadow_avg": 0,
"opaque_avg": 0,
"total_avg": 0
},
"draw_calls_avg": 0,
"vertices_avg": 0,
"chunks_rendered_avg": 0
},
"high": {
"preset": "high",
"render_distance": 0,
"frames": 0,
"duration_s": 0,
"fps": {
"min": 0,
"avg": 0,
"max": 0,
"p1": 0,
"p5": 0,
"p50": 0,
"p95": 0,
"p99": 0
},
"cpu_ms_avg": 0,
"gpu_ms": {
"shadow_avg": 0,
"opaque_avg": 0,
"total_avg": 0
},
"draw_calls_avg": 0,
"vertices_avg": 0,
"chunks_rendered_avg": 0
}
},
"note": "Populate this file with real benchmark baselines before enabling regression gating."
}
94 changes: 94 additions & 0 deletions scripts/compare_benchmarks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail

warn_threshold=10
fail_threshold=20
preset=""

if [[ $# -lt 2 ]]; then
printf 'Usage: %s baseline.json new.json\n' "$0" >&2
exit 2
fi

baseline="$1"
new="$2"
shift 2

while [[ $# -gt 0 ]]; do
case "$1" in
--preset)
preset="$2"
shift 2
;;
--warn)
warn_threshold="$2"
shift 2
;;
--fail)
fail_threshold="$2"
shift 2
;;
*)
printf 'Unknown argument: %s\n' "$1" >&2
exit 2
;;
esac
done

read_json() {
jq -r "$1" "$2"
}

baseline_source="$baseline"
baseline_generated=$(read_json '.generated' "$baseline")
if [[ "$baseline_generated" != "true" ]]; then
printf 'Baseline placeholder detected, skipping comparison.\n'
exit 0
fi

if [[ -n "$preset" ]]; then
baseline_source=$(mktemp)
jq -c --arg preset "$preset" '.presets[$preset]' "$baseline" > "$baseline_source"
fi

cleanup() {
if [[ -n "${baseline_source:-}" && "$baseline_source" == /tmp/* && -f "$baseline_source" ]]; then
rm -f "$baseline_source"
fi
}

trap cleanup EXIT

pct_change() {
awk -v old="$1" -v new="$2" 'BEGIN {
if (old == 0 && new == 0) { print 0; exit }
if (old == 0) { print 100; exit }
printf "%.2f", ((new - old) / old) * 100.0
}'
}

baseline_fps=$(read_json '.fps.avg' "$baseline_source")
new_fps=$(read_json '.fps.avg' "$new")
baseline_gpu=$(read_json '.gpu_ms.total_avg' "$baseline_source")
new_gpu=$(read_json '.gpu_ms.total_avg' "$new")
baseline_draws=$(read_json '.draw_calls_avg' "$baseline_source")
new_draws=$(read_json '.draw_calls_avg' "$new")

fps_change=$(pct_change "$baseline_fps" "$new_fps")
gpu_change=$(pct_change "$baseline_gpu" "$new_gpu")
draw_change=$(pct_change "$baseline_draws" "$new_draws")

printf 'FPS avg: %s -> %s (%s%%)\n' "$baseline_fps" "$new_fps" "$fps_change"
printf 'GPU total avg: %s -> %s (%s%%)\n' "$baseline_gpu" "$new_gpu" "$gpu_change"
printf 'Draw calls avg: %s -> %s (%s%%)\n' "$baseline_draws" "$new_draws" "$draw_change"

fps_drop=$(awk -v change="$fps_change" 'BEGIN { if (change < 0) printf "%.2f", -change; else print 0 }')

if awk -v drop="$fps_drop" -v warn="$warn_threshold" 'BEGIN { exit !(drop >= warn) }'; then
printf 'Warning: FPS regressed by %s%%\n' "$fps_drop"
fi

if awk -v drop="$fps_drop" -v fail="$fail_threshold" 'BEGIN { exit !(drop >= fail) }'; then
printf 'Regression exceeds failure threshold (%s%% >= %s%%)\n' "$fps_drop" "$fail_threshold" >&2
exit 1
fi
37 changes: 37 additions & 0 deletions scripts/run_benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail

duration=60
presets="low,medium,high"
output_dir="docs/benchmarks/results"

while [[ $# -gt 0 ]]; do
case "$1" in
--duration)
duration="$2"
shift 2
;;
--presets)
presets="$2"
shift 2
;;
--output-dir)
output_dir="$2"
shift 2
;;
*)
printf 'Unknown argument: %s\n' "$1" >&2
exit 2
;;
esac
done

mkdir -p "$output_dir"

IFS=',' read -r -a preset_list <<< "$presets"

for preset in "${preset_list[@]}"; do
output_file="$output_dir/$preset.json"
printf 'Running benchmark preset %s -> %s\n' "$preset" "$output_file"
ZIGCRAFT_SAFE_MODE=1 nix develop --command zig build benchmark -Dbenchmark-preset="$preset" -Dbenchmark-duration="$duration" -Dbenchmark-output="$output_file"
done
Loading
Loading