80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
# Author: Joeri Exelmans
|
|
|
|
class QueueEntry:
|
|
__slots__ = ('timestamp', 'raise_method', 'value', 'canceled', 'event_name') # For MAXIMUM performance :)
|
|
|
|
def __init__(self, timestamp, raise_method, value, event_name):
|
|
self.timestamp = timestamp
|
|
self.raise_method = raise_method
|
|
self.value = value
|
|
self.event_name = event_name # name of the event - only needed for debugging
|
|
self.canceled = False
|
|
|
|
def __repr__(self):
|
|
return f"({self.timestamp}, {self.event_name}, {self.value})"
|
|
|
|
# The main primitive for discrete event simulation.
|
|
# An event queue / event loop, using virtualized (simulated) time, independent of wall clock time.
|
|
class Controller:
|
|
def __init__(self):
|
|
self.event_queue = []
|
|
self.simulated_time = 0
|
|
self.input_tracers = []
|
|
|
|
# timestamp = absolute value, in simulated time (since beginning of simulation)
|
|
def add_input(self, sc, event_name, timestamp, value=None):
|
|
if '.' in event_name:
|
|
interface, short_event_name = event_name.split('.')
|
|
raise_method = getattr(getattr(sc, interface), 'raise_' + short_event_name)
|
|
else:
|
|
raise_method = getattr(sc, 'raise_' + event_name)
|
|
self.add_input_lowlevel(timestamp, raise_method, value, event_name)
|
|
|
|
# time_offset = relative to current simulated time
|
|
def add_input_relative(self, sc, event_name, time_offset=0, value=None):
|
|
timestamp = self.simulated_time + time_offset
|
|
return self.add_input(sc, event_name, timestamp, value)
|
|
|
|
def add_input_lowlevel(self, timestamp, raise_method, value, event_name):
|
|
e = QueueEntry(timestamp, raise_method, value, event_name)
|
|
self.event_queue.append(e)
|
|
# important to use a stable sorting algorithm here,
|
|
# so the order between equally-timestamped events is preserved:
|
|
self.event_queue.sort(key = lambda entry: entry.timestamp)
|
|
return e
|
|
|
|
# difference here is that the added event will occur BEFORE equally-timestamped events that were already in the queue
|
|
def add_input_lowlevel_interrupt(self, timestamp, raise_method, value, event_name):
|
|
e = QueueEntry(timestamp, raise_method, value, event_name)
|
|
self.event_queue.insert(0, e)
|
|
self.event_queue.sort(key = lambda entry: entry.timestamp)
|
|
return e
|
|
|
|
# Runs simulation as-fast-as-possible, until 'until'-timestamp (in simulated time)
|
|
# blocking, synchronous function
|
|
def run_until(self, until):
|
|
# print('running until', pretty_time(until))
|
|
while self.have_event() and self.get_earliest() <= until:
|
|
e = self.event_queue[0]
|
|
for sc, tracer in self.input_tracers:
|
|
if sc == e.raise_method.__self__:
|
|
tracer(e.timestamp, e.event_name, e.value)
|
|
# e = self.event_queue.pop();
|
|
self.event_queue = self.event_queue[1:]
|
|
if not e.canceled:
|
|
self.simulated_time = e.timestamp
|
|
if e.value == None:
|
|
e.raise_method()
|
|
else:
|
|
e.raise_method(e.value)
|
|
|
|
def have_event(self):
|
|
return len(self.event_queue) > 0
|
|
|
|
def get_earliest(self):
|
|
# return self.event_queue[-1].timestamp
|
|
return self.event_queue[0].timestamp
|
|
|
|
|
|
def pretty_time(time_ns):
|
|
return f'{round(time_ns / 1000000000, 3)} s'
|