Skip to content

Instantly share code, notes, and snippets.

@j-mao
Created August 22, 2021 01:45
Show Gist options
  • Save j-mao/1d833c66fc72c773c28c6ecf272e4d02 to your computer and use it in GitHub Desktop.
Save j-mao/1d833c66fc72c773c28c6ecf272e4d02 to your computer and use it in GitHub Desktop.
POC Battlecode Python engine without bytecode rewriting
#!/usr/bin/env python
import importlib
import sys
import threading
EXCEPTION_BYTECODE_PENALTY = 500
class Unit:
def __init__(self, module, bytecode_limit):
self.module = module
self.bytecode_limit = bytecode_limit
self._bytecodes_used = 0
self._thread = threading.Thread(target=self._run, daemon=True)
self._turn_start = threading.Condition()
self._turn_end = threading.Condition()
self._turn_running = False
self._age = 0
def step(self):
with self._turn_start:
self._turn_running = True
if self._thread.is_alive():
self._turn_start.notify()
else:
self._thread.start()
with self._turn_end:
while self._turn_running:
self._turn_end.wait()
return self._bytecodes_used
def kill(self):
# TODO: how to do this? possibly need multiprocess instead of thread
# can we multiprocess without terrible message-passing
pass
def _run(self):
if self.module in sys.modules:
# FIXME: Very likely insecure and hackable
del sys.modules[self.module] # force reimport
try:
sys.settrace(self._instrument)
player = importlib.import_module(self.module)
player.run(self._end_turn)
# FIXME: instrumenter includes overhead due to importlib
# NOTE: even a naked import statement costs >2000 bytecode, is this ok?
# FIXME: pycache makes the import not have constant cost
finally:
sys.settrace(None)
def _instrument(self, frame, event, arg):
if event == 'call':
# TODO: how to instrument C functions? eg. builtins, the math library, etc
# Likely need to use sys.setprofile to get 'c_call' event
# Would then need to enumerate the bytecode costs for all C functions
if frame.f_code is self._end_turn.__code__:
return None # Do not instrument the _end_turn function
frame.f_trace_opcodes = True
if event == 'opcode':
self._bytecodes_used += 1
if event == 'exception':
# FIXME: This might multi-count an exception if it jumps over multiple frames
self._bytecodes_used += EXCEPTION_BYTECODE_PENALTY
while self._bytecodes_used >= self.bytecode_limit:
self._end_turn()
return self._instrument
def _end_turn(self):
sys.settrace(None)
self._age += 1
with self._turn_end:
self._turn_running = False
self._turn_end.notify()
with self._turn_start:
while not self._turn_running:
self._turn_start.wait()
self._bytecodes_used = max(self._bytecodes_used-self.bytecode_limit, 0)
sys.settrace(self._instrument)
def run_match(players, turn_count, bytecode_limit):
runners = [Unit(m, bytecode_limit) for m in players]
for i in range(turn_count):
for r in runners:
bytecodes = r.step()
print(">>> Ending", r._age, bytecodes)
if __name__ == '__main__':
run_match(["player", "player"], 20, 10000)
a = 0
def run(end):
global a
while True:
a += sum(i for i in range(10000)) # something expensive
print("the value is", a)
end()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment