Files
snake-python/scripts/benchmark_snakes_from_db.py
T
daniel156161 39a7a53873
Build and Push Docker Container / build-and-push (push) Successful in 3m52s
feat(database): store gameplay replays in compact form
- Add compact storage package: codec, records, hydrate, replay blobs, cache.
- Running games write game_turns and snake_states keyed by integer ids.
- Encode moves as 1-byte codes and bodies as head plus step codes.
- Store food and hazards only when they changed since the previous turn.
- Strip reasoning fields that duplicate stored values and restore on read.
- Pack finished games into one zstd blob per game in game_replays.
- Merge late turns into packed blobs during the stale-game sweep.
- Extract SQLite and PostgreSQL schemas into dedicated modules.
- Add read/import game bundle and list_game_ids to the backend template.
- Add legacy readers and converter for the old turns/snake_turns layout.
- Add migrate_gameplay_to_compact.py with in-place or cross-DB migration.
- Verify every migrated game and keep legacy rows on failure.
- Support --since, --minimum-quality and guarded --drop-legacy.
- Add just recipe migrate-gameplay-postgres.
- Rewrite merge script to copy game by game via backend bundles.
- Move benchmark state loading into benchmark_states for both backends.
- Update PostgreSQL cleanup script to the new replay tables.
- Drop sqlite-zstd build stages from the Dockerfile.
- Remove old migration scripts, normalized_turn and their tests.
- Add tests for compact storage, schema, legacy migration and PostgreSQL.
- Document the compact layout and migration workflow in the README.
2026-11-05 00:02:56 +01:00

91 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Benchmark snake move latency against sampled states from a gameplay database."""
import argparse
from pathlib import Path
from statistics import mean, median
import sys
from time import perf_counter
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from server.database.benchmark_states import load_states
lazy from server.GameBoard import GameBoard
lazy from snakes import SnakeBuilder
def percentile(values: list[float], quantile: float) -> float:
ordered = sorted(values)
index = min(len(ordered) - 1, round((len(ordered) - 1) * quantile))
return ordered[index]
def benchmark(snake_name: str, states: list[tuple[dict, dict]], repeat: int) -> dict:
durations: list[float] = []
moves = 0
for pass_number in range(repeat):
for board_data, metadata in states:
snake = SnakeBuilder.build(snake_name)
game_id = f"benchmark-{pass_number}-{metadata['game_id']}"
board = GameBoard(
game_id=game_id,
width=board_data["width"],
height=board_data["height"],
ruleset=metadata["ruleset"],
source=metadata["source"],
map=metadata["map"],
snake_class=snake,
)
state = {
"game": {
"id": game_id,
"ruleset": metadata["ruleset"],
"source": metadata["source"],
"map": metadata["map"],
"timeout": 500,
},
"turn": metadata["turn"],
"board": board_data,
"you": metadata["you"],
}
board.read_game_data(state)
started = perf_counter()
snake.choose_move(board)
durations.append((perf_counter() - started) * 1000)
moves += 1
return {
"snake": snake_name,
"moves": moves,
"mean_ms": mean(durations),
"median_ms": median(durations),
"p95_ms": percentile(durations, 0.95),
"max_ms": max(durations),
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--database", required=True,
help="SQLite path or postgresql:// DSN",
)
parser.add_argument("--snake", action="append", default=[])
parser.add_argument("--samples", type=int, default=100)
parser.add_argument("--stride", type=int, default=997)
parser.add_argument("--repeat", type=int, default=1)
args = parser.parse_args()
states = load_states(args.database, max(1, args.samples), max(1, args.stride))
if not states:
raise SystemExit("No gameplay states found")
snake_names = args.snake or ["ApexBattleSnake", "PrismBattleSnake_GPT_5_6_Sol"]
print(f"Loaded {len(states)} states from {args.database}")
for snake_name in snake_names:
result = benchmark(snake_name, states, max(1, args.repeat))
print(
f"{result['snake']}: {result['moves']} moves, "
f"mean={result['mean_ms']:.2f} ms, median={result['median_ms']:.2f} ms, "
f"p95={result['p95_ms']:.2f} ms, max={result['max_ms']:.2f} ms"
)
if __name__ == "__main__":
main()