Build and Push Docker Container / build-and-push (push) Successful in 3m52s
- 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.
91 lines
2.9 KiB
Python
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()
|