#!/usr/bin/env python3 """Bounded, isolated real-engine benchmark. Requires Docker and two Linux binaries. python3 resource_benchmark.py --engine /path/to/butternut --swarm /path/to/swarm \ --output /path/shared/with/docker/results.json No published ports, no external network, no customer worlds. Limits: 2 CPUs, 3 GiB. The Docker-shared output directory holds a disposable world until cleanup. """ import argparse import datetime import hashlib import json import pathlib import re import subprocess import tempfile import time import uuid def command(*args, **kwargs): return subprocess.run(args, check=True, capture_output=True, text=True, timeout=40, **kwargs).stdout def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--engine', required=True, type=pathlib.Path) parser.add_argument('--swarm', required=True, type=pathlib.Path) parser.add_argument('--output', required=True, type=pathlib.Path) parser.add_argument('--phase-seconds', type=int, default=90) args = parser.parse_args() if not 30 <= args.phase_seconds <= 300: parser.error('phase-seconds must be between 30 and 300') engine, swarm = args.engine.resolve(strict=True), args.swarm.resolve(strict=True) output = args.output.resolve() output.parent.mkdir(parents=True, exist_ok=True) image = 'rust:1-bookworm' result = { 'measured_at_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'engine_sha256': hashlib.sha256(engine.read_bytes()).hexdigest(), 'swarm_sha256': hashlib.sha256(swarm.read_bytes()).hexdigest(), 'image_id': command('docker', 'image', 'inspect', image, '--format', '{{.Id}}').strip(), 'docker_vm': json.loads(command('docker', 'info', '--format', '{{json .}}')), 'settings': {'cpu_limit': 2, 'memory_limit_gib': 3, 'seed': 42, 'view_distance': 8, 'simulation_distance': 6, 'gamemode': 'Creative', 'difficulty': 'Peaceful', 'network': 'none (loopback only)', 'plugins': False, 'bedrock': False, 'phase_seconds': args.phase_seconds, 'stagger_ms': 400, 'leash_blocks': 28}, 'startup_engine_reported_ms': [], 'phases': [] } # Publish only environment facts, not Docker paths, registry credentials or labels. info = result['docker_vm'] result['docker_vm'] = {key: info.get(key) for key in ('Architecture', 'NCPU', 'MemTotal', 'KernelVersion', 'OperatingSystem', 'ServerVersion')} config = '''seed = "42" white_list = false enforce_whitelist = false default_gamemode = "Creative" default_difficulty = "Peaceful" spawn_protection = 0 [networking.java] address = "127.0.0.1:25777" max_players = 16 online_mode = false encryption = false view_distance = 8 simulation_distance = 6 [networking.bedrock] enabled = false [networking.query] enabled = false [networking.rcon] enabled = false [networking.lan_broadcast] enabled = false [telemetry] enabled = false [plugins] enabled = false [commands] use_tty = false use_console = false [logging] color = false ''' result['config'] = config with tempfile.TemporaryDirectory(prefix='engine-bench-', dir=output.parent) as temporary: work = pathlib.Path(temporary) for run in range(3): folder = work / str(run) folder.mkdir() (folder / 'butternut.toml').write_text(config) name = 'jetrift-bench-' + uuid.uuid4().hex[:12] try: command('docker', 'run', '-d', '--name', name, '--network', 'none', '--cpus', '2', '--memory', '3g', '--memory-swap', '3g', '--pids-limit', '256', '-v', f'{engine}:/opt/butternut:ro', '-v', f'{swarm}:/opt/swarm:ro', '-v', f'{folder}:/work', '-w', '/work', '--entrypoint', '/opt/butternut', image) deadline = time.monotonic() + 60 while time.monotonic() < deadline: log = command('docker', 'logs', name) match = re.search(r'Started server; took (\d+)ms', log) if match: result['startup_engine_reported_ms'].append(int(match[1])) break time.sleep(.25) else: raise RuntimeError('engine did not become ready: ' + log[-3000:]) if run < 2: continue result['startup_log'] = log for bots, duration in ((0, 30), (4, args.phase_seconds), (8, args.phase_seconds)): phase = {'bots_target': bots, 'duration_seconds': duration, 'samples': []} bot_process = None try: if bots: bot_process = subprocess.Popen(['docker', 'exec', name, '/opt/swarm', '--addr', '127.0.0.1:25777', '--bots', str(bots), '--duration-secs', str(duration), '--stagger-ms', '400', '--seed', '42', '--leash', '28', '--json'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) end = time.monotonic() + duration seen = set() while time.monotonic() < end: path = folder / 'telemetry/stats.json' if path.exists(): try: report = json.loads(path.read_text()) stamp = report['generated_unix_ms'] if stamp not in seen: seen.add(stamp) phase['samples'].append({ 'generated_unix_ms': stamp, 'uptime_s': report['uptime_s'], 'engine': report['engine'], 'rss_bytes': report['process']['rss_bytes'], 'cpu': report['cpu'], 'players': report['census']['players'], 'chunks': report['census']['chunks_loaded_total'], 'entities': report['census']['entities_total']}) except (json.JSONDecodeError, FileNotFoundError): pass time.sleep(1) if bot_process: stdout, stderr = bot_process.communicate(timeout=20) if bot_process.returncode: raise RuntimeError('swarm failed: ' + stderr[-2000:]) phase['swarm'] = json.loads(stdout.strip().splitlines()[-1]) if not phase['samples']: raise RuntimeError('no engine telemetry collected') result['phases'].append(phase) output.write_text(json.dumps(result, indent=2) + '\n') print(json.dumps({'phase': bots, 'samples': len(phase['samples']), 'swarm': phase.get('swarm')}), flush=True) finally: if bot_process and bot_process.poll() is None: bot_process.terminate() try: bot_process.wait(timeout=5) except subprocess.TimeoutExpired: bot_process.kill() finally: # The only container removed is this run's unique, unpublished container. subprocess.run(['docker', 'stop', '-t', '15', name], capture_output=True, timeout=25) subprocess.run(['docker', 'rm', '-f', name], capture_output=True, timeout=10) output.write_text(json.dumps(result, indent=2) + '\n') if __name__ == '__main__': main()