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
14 changes: 14 additions & 0 deletions docs/builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ python3 tools/booth/sessions_report.py booth/ # se
python3 tools/booth/sessions_report.py booth-a/ booth-b/ --csv sessions.csv # combine machines; one CSV row per session
```

## Soak test and frame rate

Before a show, let the build run unattended on the booth machine for a full show day:

```sh
python3 tools/booth/soak.py --minutes 600 -- "/Applications/Hoppy’s Adventure Arcade.app/Contents/MacOS/<executable>" -- --booth
python3 tools/booth/soak.py --minutes 10 --godot "$GODOT" # a quick run of this checkout's project
```

- **The soak.** The game runs with `--soak=<minutes>` (wall-clock time). It visits every route in turn and stays `--soak-dwell=<seconds>` (default 30) on each with nobody playing, counted from when the route has opened. `--soak-routes=hub,heroes_vs_villains` keeps it to those routes; the hub is always included. `--soak-cycles=<n>` ends it after n full passes, if that comes first: `--soak=30 --soak-cycles=1` is a quick frame-rate probe of every route. A soak turns off the idle return and the game-over card, ignores `--route`, and writes no session log, so its visits never count as booth sessions.
- **Frame rate.** Every 5 seconds the game appends a CSV row for the current route: frames per second, median, 99th-percentile and worst frame times, and the engine's memory and object counts. At the end it prints a table per route: the share of frames slower than 16.7 ms (60 fps) and 33.3 ms (30 fps), percentiles capped at the worst frame, and the worst loading hitch. Run it fullscreen (`--booth`) on the booth hardware to measure at the booth resolution; headless frame times mean nothing.
- **Leak check.** At the end of each hub visit the game records its object, node, orphan-node and resource counts and prints `SOAK hub <n>`. It compares the second visit with the last, since the first warms caches. Debug builds also compare static memory, which release builds don't report. Growth beyond small limits makes the game exit 1. A route that won't open, or results that can't be written, make it exit 2.
- **Resident memory.** `soak.py` samples the process's memory with `ps` every 30 seconds (`--rss-every`) into `rss.csv`, and again at each `SOAK hub <n>` line. It compares the second hub visit with the last, once there are three, and fails above 25% growth (`--max-rss-growth`). It also fails a game that exits without a verdict, and stops a run that outlasts its time by 10 minutes (`--grace`). Each run gets its own folder under `soak-results/` (`--out`), holding the game's CSV and summary, `game.log` and `rss.csv`. macOS and Linux.

## Build locally

```sh
Expand Down
30 changes: 22 additions & 8 deletions hoppys-adventure-arcade/app/app_router.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ extends CanvasLayer
const BoothLog := preload("res://app/booth_log.gd")
const BoothHeartbeat := preload("res://app/booth_heartbeat.gd")
const NextPlayerCard := preload("res://app/next_player_card.gd")
const BoothSoak := preload("res://app/booth_soak.gd")
const ROUTES := {
"heroes_vs_villains": "res://scenes/beatem_up/reinvent/heroes_vs_villains.tscn",
"code_city_brawl": "res://scenes/beatem_up/code_city_brawl.tscn",
Expand Down Expand Up @@ -61,6 +62,8 @@ var retry_limit := 0
var game_overs := 0
var next_player: NextPlayerCard
var next_player_left := 0.0
## The soak test (app/booth_soak.gd), when the game was launched with `--soak=<minutes>`.
var soak: BoothSoak
func _ready() -> void:
# The veil tween must keep running while a microgame has the tree paused.
process_mode = Node.PROCESS_MODE_ALWAYS
Expand All @@ -81,22 +84,32 @@ func _ready() -> void:
set_booth(BOOTH_FLAG in args)
if booth:
print("AppRouter: booth attract route %s, %d retries per player" % [attract, retry_limit])
var log_path := session_log_path(args, booth)
if log_path != "":
start_session_log(log_path)
var beat_path := heartbeat_path(args)
if beat_path != "":
heartbeat = BoothHeartbeat.new(beat_path)
add_child(heartbeat)
print("AppRouter: heartbeat at %s" % ProjectSettings.globalize_path(beat_path))
var soak_minutes := BoothSoak.minutes_from(args)
if soak_minutes > 0.0:
# The soak changes routes itself: no idle return, no launch route, no next-player card,
# and no session log, so its visits never count as booth sessions.
idle_limit = INF
soak = BoothSoak.new(soak_minutes, args)
add_child(soak)
return
var log_path := session_log_path(args, booth)
if log_path != "":
start_session_log(log_path)
var start := launch_route(args)
if start != "":
_launch.call_deferred(start)
## `reason` is how a game session in progress ends, for the session log: "menu" unless the router
## itself is leaving the game (booth mode's idle return passes "idle").
func go(route: String, reason := "menu") -> void:
## Returns true once the route's scene has opened, false when the change was refused or failed;
## callers that don't need to know can ignore it.
func go(route: String, reason := "menu") -> bool:
if busy or not ROUTES.has(route) or not ResourceLoader.exists(ROUTES[route]):
return
return false
busy = true
veil.mouse_filter = Control.MOUSE_FILTER_STOP
AudioService.play_ui(2)
Expand All @@ -118,6 +131,7 @@ func go(route: String, reason := "menu") -> void:
await create_tween().tween_property(veil, "modulate:a", 0.0, 0.25).finished
veil.mouse_filter = Control.MOUSE_FILTER_IGNORE
busy = false
return error == OK
## Visitors can't quit a booth from the menu; staff close the window instead (force = true).
## A forced quit wins even mid-transition: auto_accept_quit is off, so nothing else would close.
func quit_game(force := false) -> void:
Expand Down Expand Up @@ -195,13 +209,13 @@ static func retry_count(args: PackedStringArray) -> int:
## Games call this when the player loses, before showing their own retry screen. Returns true when
## the booth takes over: the player has used their retries, so the game shows nothing and the
## router thanks them and sends the booth to the attract route. Always false outside booth mode,
## and while a route change is already under way (it takes the player elsewhere anyway). The
## session log counts every game over, booth or not.
## while a route change is already under way (it takes the player elsewhere anyway), and during a
## soak, which changes routes itself. The session log counts every game over, booth or not.
## `score_line` is shown on the card, in the game's own style ("SCORE 01234").
func booth_game_over(score_line := "") -> bool:
if session_log:
session_log.count_game_over()
if not booth or busy:
if not booth or busy or soak != null:
return false
game_overs += 1
if game_overs <= retry_limit:
Expand Down
Loading
Loading