diff --git a/amitools/vamos/cfg/__init__.py b/amitools/vamos/cfg/__init__.py index 98b4e7a3..7109621b 100644 --- a/amitools/vamos/cfg/__init__.py +++ b/amitools/vamos/cfg/__init__.py @@ -5,4 +5,5 @@ from .machine import MachineParser from .proc import ProcessParser from .profile import ProfileParser +from .schedule import ScheduleParser from .vamos import VamosMainParser diff --git a/amitools/vamos/cfg/machine.py b/amitools/vamos/cfg/machine.py index 84addf75..e29f9d85 100644 --- a/amitools/vamos/cfg/machine.py +++ b/amitools/vamos/cfg/machine.py @@ -21,8 +21,6 @@ def __init__(self, ini_prefix=None): def_cfg = { "machine": { "cpu": Value(str, "68000", enum=cpus), - "max_cycles": 0, - "cycles_per_run": 1000, "ram_size": 1024, }, "memmap": { @@ -38,18 +36,6 @@ def __init__(self, ini_prefix=None): action="store", help="Set type of CPU to emulate (68000, 68020 or 68040)", ), - "max_cycles": Argument( - "--max-cycles", - action="store", - type=int, - help="maximum number of cycles to execute", - ), - "cycles_per_run": Argument( - "--cycles-per-block", - action="store", - type=int, - help="cycles per block", - ), "ram_size": Argument( "-m", "--ram-size", @@ -75,8 +61,6 @@ def __init__(self, ini_prefix=None): ini_trafo = { "machine": { "cpu": "cpu", - "max_cycles": "max_cycles", - "cycles_per_run": "cycles_per_run", "ram_size": "ram_size", }, "memmap": {"hw_access": "hw_access", "old_dos_guard": "old_dos_guard"}, diff --git a/amitools/vamos/cfg/schedule.py b/amitools/vamos/cfg/schedule.py new file mode 100644 index 00000000..3a108d16 --- /dev/null +++ b/amitools/vamos/cfg/schedule.py @@ -0,0 +1,35 @@ +from amitools.vamos.cfgcore import * + + +class ScheduleParser(Parser): + def __init__(self, ini_prefix=None): + def_cfg = { + "schedule": { + "slice_cycles": 1000, + } + } + arg_cfg = { + "schedule": { + "slice_cycles": Argument( + "--slice-cycles", + action="store", + type=int, + help="duration of one one scheduler slice in m68k cycles", + ) + } + } + ini_trafo = { + "schedule": { + "slice_cycles": "slice_cycles", + } + } + Parser.__init__( + self, + "schedule", + def_cfg, + arg_cfg, + "schedule", + "scheduler options", + ini_trafo, + ini_prefix, + ) diff --git a/amitools/vamos/cfg/vamos.py b/amitools/vamos/cfg/vamos.py index 766f2192..68ba2749 100644 --- a/amitools/vamos/cfg/vamos.py +++ b/amitools/vamos/cfg/vamos.py @@ -26,6 +26,9 @@ def __init__(self, debug=None, *args, **kwargs): # profile self.profile = ProfileParser() self.add_parser(self.profile) + # schedule + self.schedule = ScheduleParser() + self.add_parser(self.schedule) def get_log_dict(self): return self.log.get_cfg_dict() @@ -47,3 +50,6 @@ def get_proc_dict(self): def get_profile_dict(self): return self.profile.get_cfg_dict() + + def get_schedule_dict(self): + return self.schedule.get_cfg_dict() diff --git a/amitools/vamos/log.py b/amitools/vamos/log.py index 4b9e20f3..ae5cd650 100644 --- a/amitools/vamos/log.py +++ b/amitools/vamos/log.py @@ -12,6 +12,7 @@ log_mem_int = logging.getLogger("mem_int") log_instr = logging.getLogger("instr") log_machine = logging.getLogger("machine") +log_schedule = logging.getLogger("schedule") log_lib = logging.getLogger("lib") log_libmgr = logging.getLogger("libmgr") @@ -56,6 +57,7 @@ log_hw, log_math, log_machine, + log_schedule, ] preset = {log_prof: logging.INFO} diff --git a/amitools/vamos/machine/runtime.py b/amitools/vamos/machine/runtime.py index 68d1ddc6..0edb9e2d 100644 --- a/amitools/vamos/machine/runtime.py +++ b/amitools/vamos/machine/runtime.py @@ -60,14 +60,17 @@ def reset_slice(self): """allow to reset slice reporting""" self.left_cycles = self.slice_cycles - def run(self, pc, sp, set_regs=None, get_regs=None, name=None) -> RunState: + def is_running(self): + return self.running + + def run(self, pc, sp=None, set_regs=None, get_regs=None, name=None) -> RunState: """convenience method to dispatch either to start or nested_run""" if not self.running: return self.start(pc, sp, set_regs, get_regs, name) else: return self.nested_run(pc, sp, set_regs, get_regs, name) - def start(self, pc, sp, set_regs=None, get_regs=None, name=None) -> RunState: + def start(self, pc, sp=None, set_regs=None, get_regs=None, name=None) -> RunState: """start the main run at given pc with stack and return result""" if self.running: raise RuntimeError("start() only allowed inside idle runtime.") diff --git a/amitools/vamos/main.py b/amitools/vamos/main.py index 0bc1f4f9..df260fc5 100644 --- a/amitools/vamos/main.py +++ b/amitools/vamos/main.py @@ -77,7 +77,8 @@ def main(cfg_files=None, args=None, cfg_dict=None, profile=False): return RET_CODE_CONFIG_ERROR # setup scheduler - scheduler = Scheduler(machine) + schedule_cfg = mp.get_schedule_dict().schedule + scheduler = Scheduler.from_cfg(machine, schedule_cfg) # a default runtime for m68k code execution after scheduling default_runtime = Runtime(machine, machine.scratch_end) @@ -118,20 +119,18 @@ def runner(*args, **kw_args): log_main.error("main proc setup failed!") return RET_CODE_CONFIG_ERROR - # main loop + # add main task task = main_proc.get_task() scheduler.add_task(task) - scheduler.schedule() - # check proc result - run_state = task.get_result() + # main loop + scheduler.schedule() # return code is limited to 0-255 - exit_code = run_state.regs[REG_D0] & 0xFF + exit_code = task.get_exit_code() & 0xFF + run_state = task.get_run_result() log_main.info("done. exit code=%d", exit_code) - log_main.info( - "cycles: main=%d total=%d", run_state.cycles, run_state.total_cycles - ) + log_main.debug("run result: %r", run_state) # shutdown main proc main_proc.free() diff --git a/amitools/vamos/schedule/scheduler.py b/amitools/vamos/schedule/scheduler.py index 88e5884d..3dbeebf0 100644 --- a/amitools/vamos/schedule/scheduler.py +++ b/amitools/vamos/schedule/scheduler.py @@ -1,11 +1,29 @@ +import greenlet + +from amitools.vamos.log import log_schedule +from amitools.vamos.schedule.task import TaskState + + class Scheduler(object): """handle the execution of multiple tasks""" - def __init__(self, machine): + def __init__(self, machine, slice_cycles=1000): self.machine = machine - self.tasks = [] + self.slice_cycles = slice_cycles + # state + self.added_tasks = [] + self.ready_tasks = [] + self.waiting_tasks = [] self.cur_task_hook = None self.cur_task = None + self.num_tasks = 0 + self.main_glet = greenlet.getcurrent() + self.num_switch_same = 0 + self.num_switch_other = 0 + + @classmethod + def from_cfg(cls, machine, schedule_cfg): + return cls(machine, schedule_cfg.slice_cycles) def get_machine(self): return self.machine @@ -14,7 +32,11 @@ def set_cur_task_callback(self, func): self.cur_task_hook = func def get_num_tasks(self): - return len(self.tasks) + """count the active tasks""" + sum = len(self.ready_tasks) + len(self.waiting_tasks) + len(self.added_tasks) + if self.cur_task: + sum += 1 + return sum def get_cur_task(self): return self.cur_task @@ -22,48 +44,119 @@ def get_cur_task(self): def schedule(self): """main work call for scheduler. at least one task must be added. terminates if there are no more tasks to schedule or if a task - failed. - - return result of last task + fails and an uncaught exception is thrown. """ + log_schedule.info("schedule(): start") + # check that we have at least one task to run - if len(self.tasks) == 0: + if len(self.added_tasks) == 0: raise RuntimeError("no tasks to schedule!") - # currently we are single task - # so for now simply run a single task - task = self.tasks[0] + # main loop + while True: + # nothing to do anymore? + if self.get_num_tasks() == 0: + break + + # find a task to run + task = self._find_run_task() + if task is None: + log_schedule.error("schedule(): no task to run?!") + return False + + # current tasks stays the same? + # no context switch required. simply switch to it + if task == self.cur_task: + self.num_switch_same += 1 + log_schedule.debug("run: current %s", task.name) + task.switch() + else: + self.num_switch_other += 1 + # switch out old + old_task = self.cur_task + if old_task: + log_schedule.debug("run: switch out %s", old_task.name) + old_task.set_state(TaskState.TS_READY) + self.ready_tasks.append(old_task) + + old_task.save_ctx() + + # switch in new + self.cur_task = task + self._make_current(task) + task.set_state(TaskState.TS_RUN) + log_schedule.debug("run: switch in %s", task.name) + + task.restore_ctx() + task.switch() + + self._make_current(None) + log_schedule.info( + "schedule(): done (switches: same=%d, other=%d)", + self.num_switch_same, + self.num_switch_other, + ) + return True - # report this task + def _find_run_task(self): + # if added tasks are available take this one + if len(self.added_tasks) > 0: + task = self.added_tasks.pop(0) + log_schedule.debug("take: added task %s", task.name) + return task + + # if a ready task is available + if len(self.ready_tasks): + task = self.ready_tasks.pop(0) + log_schedule.debug("take: ready task %s", task.name) + return task + + # keep current task + task = self.cur_task + log_schedule.debug("take: current task %s", task.name) + return task + + def _make_current(self, task): self.cur_task = task if self.cur_task_hook: self.cur_task_hook(task) - # start task - task.start() - - # cleanup task - task.free() - - # no more cur task - self.cur_task = None - if self.cur_task_hook: - self.cur_task_hook(None) - def add_task(self, task): """add a new task and prepare for execution. returns True if task was added """ - self.tasks.append(task) - # assign myself - task.set_scheduler(self) - # return regs + self.added_tasks.append(task) + task.set_state(TaskState.TS_ADDED) + # configure task + task.config(self, self.slice_cycles) + log_schedule.info("add_task: %s", task.name) return True def rem_task(self, task): - raise NotImplementedError + # find task: is it current? + if self.cur_task == task: + self.cur_task = None + # in ready list? + elif task in self.ready_tasks: + self.ready_tasks.remove(task) + # in waiting list? + elif task in self.waiting_tasks: + self.waiting_tasks.remove(task) + # in added list? + elif task in self.added_tasks: + self.added_tasks.remove(task) + # not found + else: + log_schedule.warn("rem_task: unknown task %s", task.name) + return False + # mark as removed + task.set_state(TaskState.TS_REMOVED) + log_schedule.info("rem_task: %s", task.name) + # finally free task + task.free() + return True def reschedule(self, task): """callback from tasks to reschedule""" - pass + self.main_glet.switch() diff --git a/amitools/vamos/schedule/task.py b/amitools/vamos/schedule/task.py index fe571b94..82599d03 100644 --- a/amitools/vamos/schedule/task.py +++ b/amitools/vamos/schedule/task.py @@ -1,6 +1,7 @@ -import abc from enum import Enum +import greenlet +from amitools.vamos.log import log_schedule from amitools.vamos.machine import Runtime, REG_D0 @@ -14,21 +15,32 @@ class TaskState(Enum): TS_REMOVED = 6 -class TaskBase(abc.ABC): +class TaskBase: """basic structure for both native and Pyhton tasks""" def __init__(self, name, machine): - self.state = TaskState.TS_INVALID self.name = name + self.machine = machine self.runtime = Runtime(machine) + # state + self.state = TaskState.TS_INVALID self.scheduler = None - self.result = None - # trigger reschedule on max cycles - self.runtime.set_max_cycle_hook(self._max_cycles_hook) + self.exit_code = None + self.glet = None + self.cpu_ctx = None + + def __repr__(self): + return "TaskBase(%r, %s)" % (self.name, self.state) - def set_scheduler(self, scheduler): - """the scheduler adds its own ref""" + def config(self, scheduler, slice_cycles): + """the scheduler configs the task""" self.scheduler = scheduler + self.glet = greenlet.greenlet(self.start) + + def slice_hook(run_state): + self.scheduler.reschedule(self) + + self.runtime.set_slice_hook(slice_cycles, slice_hook) def set_state(self, state): """the scheduler assigns a new state""" @@ -41,29 +53,37 @@ def get_run_state(self): """if task is running then you can get the current run state""" return self.runtime.get_current_run_state() - def get_result(self): - return self.result - - def run(self, *args, **kw_args): - """execute m68k code in your task""" - return self.runtime.nested_run(*args, **kw_args) + def get_exit_code(self): + return self.exit_code def reschedule(self): """give up this tasks execution and allow the scheduler to run another task""" self.scheduler.reschedule(self) - @abc.abstractmethod def free(self): """clean up task resources, e.g. stack""" pass - @abc.abstractmethod def start(self): """run the task until it ends. it might be interrupted by reschedule() calls""" pass - def _max_cycles_hook(self, cur_cycles, run_cycles, run_nesting): - self.reschedule() + def run(self, *args, **kw_args): + """execute m68k code in your task""" + return self.runtime.run(*args, **kw_args) + + def switch(self): + self.glet.switch() + + def save_ctx(self): + if self.runtime.is_running(): + log_schedule.debug("%s: save cpu context", self) + self.cpu_ctx = self.machine.cpu.get_context() + + def restore_ctx(self): + if self.runtime.is_running(): + log_schedule.debug("%s: restore cpu context", self) + self.machine.cpu.set_context(self.cpu_ctx) class NativeTask(TaskBase): @@ -81,6 +101,7 @@ def __init__( return_regs = [REG_D0] self.start_regs = start_regs self.return_regs = return_regs + self.run_state = None def __repr__(self): return ( @@ -103,9 +124,13 @@ def start(self): sp = self.get_init_sp() set_regs = self.start_regs get_regs = self.return_regs - run_state = self.runtime.start(pc, sp, set_regs=set_regs, get_regs=get_regs) - # native tasks return the run state as a result - self.result = run_state + self.run_state = self.runtime.start( + pc, sp, set_regs=set_regs, get_regs=get_regs + ) + self.exit_code = self.run_state.regs[REG_D0] + # at the end of execution remove myself + if self.scheduler: + self.scheduler.rem_task(self) def get_init_pc(self): return self.init_pc @@ -121,3 +146,6 @@ def get_start_regs(self): def get_return_regs(self): return self.return_regs + + def get_run_result(self): + return self.run_state diff --git a/test/unit/cfg_machine.py b/test/unit/cfg_machine.py index b64c6f46..7c7a9f7b 100644 --- a/test/unit/cfg_machine.py +++ b/test/unit/cfg_machine.py @@ -7,8 +7,6 @@ def cfg_machine_dict_test(): input_dict = { "machine": { "cpu": "68020", - "max_cycles": 23, - "cycles_per_run": 42, "ram_size": 512, }, "memmap": {"hw_access": "abort", "old_dos_guard": True}, @@ -22,8 +20,6 @@ def cfg_machine_ini_test(): ini_dict = { "vamos": { "cpu": "68020", - "max_cycles": 23, - "cycles_per_run": 42, "ram_size": 512, "hw_access": "abort", "old_dos_guard": True, @@ -33,8 +29,6 @@ def cfg_machine_ini_test(): assert lp.get_cfg_dict() == { "machine": { "cpu": "68020", - "max_cycles": 23, - "cycles_per_run": 42, "ram_size": 512, }, "memmap": {"hw_access": "abort", "old_dos_guard": True}, @@ -49,10 +43,6 @@ def cfg_machine_args_test(): [ "-C", "68020", - "--max-cycles", - "23", - "--cycles-per-block", - "42", "--old-dos-guard", "-m", "512", @@ -64,8 +54,6 @@ def cfg_machine_args_test(): assert lp.get_cfg_dict() == { "machine": { "cpu": "68020", - "max_cycles": 23, - "cycles_per_run": 42, "ram_size": 512, }, "memmap": {"hw_access": "abort", "old_dos_guard": True}, diff --git a/test/unit/cfg_schedule.py b/test/unit/cfg_schedule.py new file mode 100644 index 00000000..195d895c --- /dev/null +++ b/test/unit/cfg_schedule.py @@ -0,0 +1,42 @@ +from amitools.vamos.cfg import ScheduleParser +import argparse + + +def cfg_schedule_dict_test(): + lp = ScheduleParser() + input_dict = { + "schedule": { + "slice_cycles": 2000, + } + } + lp.parse_config(input_dict, "dict") + assert lp.get_cfg_dict() == input_dict + + +def cfg_schedule_ini_test(): + lp = ScheduleParser("vamos") + ini_dict = {"vamos": {"slice_cycles": 2000}} + lp.parse_config(ini_dict, "ini") + assert lp.get_cfg_dict() == { + "schedule": { + "slice_cycles": 2000, + } + } + + +def cfg_schedule_args_test(): + lp = ScheduleParser() + ap = argparse.ArgumentParser() + lp.setup_args(ap) + args = ap.parse_args( + [ + "--slice-cycles", + "2000", + ] + ) + lp.parse_args(args) + assert lp.get_cfg_dict() == { + "schedule": { + "slice_cycles": 2000, + } + } diff --git a/test/unit/schedule_scheduler.py b/test/unit/schedule_scheduler.py index 7355a1b2..57c6fbb4 100644 --- a/test/unit/schedule_scheduler.py +++ b/test/unit/schedule_scheduler.py @@ -33,8 +33,8 @@ def schedule_scheduler_native_task_simple_test(): assert sched.add_task(task) # run scheduler sched.schedule() - result = task.get_result() - assert result.regs == {REG_D0: 42} + exit_code = task.get_exit_code() + assert exit_code == 42 assert alloc.is_all_free() machine.cleanup() @@ -59,8 +59,8 @@ def cb(task): assert sched.get_cur_task() is None # run scheduler sched.schedule() - result = task.get_result() - assert result.regs == {REG_D0: 42} + exit_code = task.get_exit_code() + assert exit_code == 42 assert alloc.is_all_free() machine.cleanup() assert tasks == [task, None] @@ -99,8 +99,8 @@ def trap(op, pc): assert sched.add_task(task) # run scheduler sched.schedule() - result = task.get_result() - assert result.regs == {REG_D0: 42} + exit_code = task.get_exit_code() + assert exit_code == 42 assert alloc.is_all_free() machine.cleanup() assert tasks == [task, None] diff --git a/test/unit/schedule_task.py b/test/unit/schedule_task.py index f6ec0f8c..8378de42 100644 --- a/test/unit/schedule_task.py +++ b/test/unit/schedule_task.py @@ -28,6 +28,6 @@ def schedule_task_native_simple_test(): task = create_native_task(machine, alloc, pc, {REG_D0: 42}) result = task.start() task.free() - assert task.get_result().regs == {REG_D0: 42} + assert task.get_exit_code() == 42 assert alloc.is_all_free() machine.cleanup()