"""Cycle model (sim/cycles.py) and GSP cycle profile (sim/gsp_profile.py) tests. All assertions here are about a *modeled* cycle count and its determinism, not about silicon timings. Everything is stdlib-only and offline: no hardware, no GPU, no QEMU, no network. The real-image tests are bounded (200k instructions), read-only, and skip cleanly when the 570.144 fwimage is absent. """ from __future__ import annotations import json import os import tempfile import unittest from greeneye.device_map import QEMU_REFERENCE from sim.cycles import ( CLASS_ALU, CLASS_DIV, CLASS_LOAD, CLASS_MUL, CYCLE_FIDELITY, DEFAULT_MODEL, NOTE, CycleModel, ) from sim.decode import ( A_ADD, A_DIV, A_MUL, A_SUB, K_AMO, K_BRANCH, K_CSR, K_EBREAK, K_ECALL, K_ILLEGAL, K_JAL, K_JALR, K_LOAD, K_OP, K_OP32, decode32, ) from sim.gsp_profile import DEFAULT_IMAGE, run_profile from sim.machine import R_EBREAK, Machine HERE = os.path.dirname(os.path.abspath(__file__)) TOOL_ROOT = os.path.normpath(os.path.join(HERE, "..")) DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") REAL_IMAGE = os.path.normpath( os.path.join( HERE, "..", "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin" ) ) BASE = 0x80000000 TOUCH_A = QEMU_REFERENCE["access_addr"] # 0x2000000000000A00 (570.144 first touch) # addi a0,a0,1 x3; ebreak PROGRAM_ALU = bytes.fromhex("13051500" * 3 + "73001000") # lui t0,0x80000; lw a0,0(t0); ebreak (one completed RAM load) PROGRAM_RAM_LOAD = bytes.fromhex("b702008003a5020073001000") # lui t0,0x20000; slli t0,t0,32; addi t0,t0,0x600; addi t0,t0,0x400; # lw a0,0(t0); sw a0,0(t0); ebreak (one unknown-MMIO read + one write) PROGRAM_MMIO = bytes.fromhex("b702002093920202938202609382024003a5020023a0a20073001000") def decoded_cost(program: bytes, model: CycleModel, pc0: int = BASE) -> int: """Instruction cost of a straight-line 4-byte-encoded program (no memory).""" total = 0 pc = pc0 while pc < pc0 + len(program): word = int.from_bytes(program[pc - pc0 : pc - pc0 + 4], "little") kind, sub, _rd, _r1, _r2, _imm, _nxt = decode32(pc, word) total += model.instruction_cost(kind, sub) pc += 4 return total class CycleTableTests(unittest.TestCase): def test_every_decoded_kind_has_a_positive_cost(self): subs = {K_OP: range(18), K_OP32: range(18)} for kind in range(18): for sub in subs.get(kind, (0,)): cost = DEFAULT_MODEL.instruction_cost(kind, sub) self.assertIsInstance(cost, int) self.assertGreaterEqual(cost, 1, f"kind={kind} sub={sub}") def test_alu_branch_and_jump_are_unit_cost(self): model = DEFAULT_MODEL self.assertEqual(model.instruction_cost(K_OP, A_ADD), model.alu) self.assertEqual(model.instruction_cost(K_OP, A_SUB), model.alu) self.assertEqual(model.instruction_cost(K_BRANCH), model.branch) self.assertEqual(model.instruction_cost(K_JAL), model.jump) self.assertEqual(model.instruction_cost(K_JALR), model.jump) def test_m_extension_is_conservative(self): model = DEFAULT_MODEL mul = model.instruction_cost(K_OP, A_MUL) div = model.instruction_cost(K_OP, A_DIV) self.assertGreater(mul, model.alu) self.assertGreater(div, mul) # Same table is used for the 32-bit M forms. self.assertEqual(model.instruction_cost(K_OP32, A_MUL), mul) self.assertEqual(model.instruction_cost(K_OP32, A_DIV), div) def test_memory_latency_is_a_parameter(self): model = DEFAULT_MODEL self.assertGreaterEqual(model.memory_cost(False), 1) self.assertGreaterEqual(model.memory_cost(True), model.memory_cost(False)) custom = CycleModel(memory_latency=9, mmio_latency=40) self.assertEqual(custom.memory_cost(False), 9) self.assertEqual(custom.memory_cost(True), 40) self.assertEqual(custom.instruction_cost(K_LOAD), model.instruction_cost(K_LOAD)) def test_csr_fence_amo_and_trap_are_costs(self): model = DEFAULT_MODEL for kind in (K_CSR, K_AMO, K_EBREAK, K_ECALL, K_ILLEGAL): self.assertGreaterEqual(model.instruction_cost(kind), 1) def test_classify_reports_classes(self): self.assertEqual(CycleModel.classify(K_OP, A_ADD), CLASS_ALU) self.assertEqual(CycleModel.classify(K_OP, A_MUL), CLASS_MUL) self.assertEqual(CycleModel.classify(K_OP, A_DIV), CLASS_DIV) self.assertEqual(CycleModel.classify(K_LOAD), CLASS_LOAD) def test_model_is_labelled_modeled_not_measured(self): self.assertIn("modeled", NOTE.lower()) self.assertIn("not measured", NOTE.lower()) self.assertIn("L2", CYCLE_FIDELITY) self.assertIn("modeled", DEFAULT_MODEL.name) described = DEFAULT_MODEL.describe() self.assertEqual(described["name"], DEFAULT_MODEL.name) self.assertEqual(described["note"], NOTE) self.assertIn("memory_latency", described) def test_invalid_parameters_rejected(self): with self.assertRaises(ValueError): CycleModel(alu=0) with self.assertRaises(ValueError): CycleModel(memory_latency=-1) with self.assertRaises(ValueError): CycleModel(div=True) class MachineCycleTests(unittest.TestCase): def _machine(self, program: bytes, model=None, **kwargs) -> Machine: machine = Machine(cycle_model=model, **kwargs) machine.load_segments([(BASE, program)], BASE) return machine def test_counting_enabled_by_default(self): machine = self._machine(PROGRAM_ALU) machine.run(3) # three addi, budget stop self.assertEqual(machine.insns, 3) self.assertEqual(machine.cycles, 3) def test_ram_load_adds_the_modelled_memory_latency(self): machine = self._machine(PROGRAM_RAM_LOAD) reason, _pc, _a, _b = machine.run(100) self.assertEqual(reason, R_EBREAK) expected = decoded_cost(PROGRAM_RAM_LOAD, DEFAULT_MODEL) + DEFAULT_MODEL.memory_latency self.assertEqual(machine.cycles, expected) def test_mmio_accesses_add_the_modelled_mmio_latency(self): machine = self._machine(PROGRAM_MMIO, unknown_mmio="lenient") reason, _pc, _a, _b = machine.run(100) self.assertEqual(reason, R_EBREAK) self.assertEqual(machine.mmio_unknown, 2) self.assertEqual(machine.mmio_log[0]["addr"], TOUCH_A) expected = decoded_cost(PROGRAM_MMIO, DEFAULT_MODEL) + 2 * DEFAULT_MODEL.mmio_latency self.assertEqual(machine.cycles, expected) def test_variable_memory_latency_changes_only_the_counter(self): slow = CycleModel(memory_latency=9) quick = self._machine(PROGRAM_RAM_LOAD, model=DEFAULT_MODEL) slower = self._machine(PROGRAM_RAM_LOAD, model=slow) quick.run(100) slower.run(100) self.assertEqual(slower.insns, quick.insns) self.assertEqual(slower.x, quick.x) self.assertEqual(slower.cycles - quick.cycles, 9 - DEFAULT_MODEL.memory_latency) def test_disabled_model_leaves_the_counter_at_zero(self): machine = self._machine(PROGRAM_ALU, model=False) machine.run(3) self.assertIsNone(machine.cycle_model) self.assertEqual(machine.insns, 3) self.assertEqual(machine.cycles, 0) def test_model_does_not_change_execution(self): without = self._machine(PROGRAM_MMIO, model=False, unknown_mmio="lenient") with_model = self._machine(PROGRAM_MMIO, unknown_mmio="lenient") result_without = without.run(100) result_with_model = with_model.run(100) self.assertEqual(result_without, result_with_model) self.assertEqual(without.insns, with_model.insns) self.assertEqual(without.x, with_model.x) self.assertEqual(without.mmio_log, with_model.mmio_log) self.assertGreater(with_model.cycles, 0) def test_double_run_identical(self): def profile(): machine = self._machine(PROGRAM_MMIO, unknown_mmio="lenient") result = machine.run(100) return result, machine.insns, machine.cycles, machine.pc, tuple(machine.x) self.assertEqual(profile(), profile()) @unittest.skipUnless(os.path.isfile(DEMO_ELF), "demo firmware absent") def test_demo_execution_identical_with_and_without_model(self): from greeneye.elf import load_elf image = load_elf(DEMO_ELF) plan = image.flat_load_plan(bias=0) entry = image.relocated_entry(bias=0) def run(model): machine = Machine(cycle_model=model) machine.load_segments(plan, entry) result = machine.run(20_000) return result, machine.insns, tuple(machine.x), machine.uart.text() self.assertEqual(run(False), run(None)) class GspProfileTests(unittest.TestCase): def test_default_image_path_matches_workspace_firmware(self): self.assertEqual( os.path.normpath(DEFAULT_IMAGE), os.path.normpath(REAL_IMAGE), ) def test_absent_image_skips_cleanly(self): with tempfile.TemporaryDirectory() as root: missing = os.path.join(root, "no-such-image.bin") result = run_profile(image=missing, instructions=1000) self.assertFalse(result.ran) self.assertIn("absent", result.reason) self.assertIsNone(result.session_dir) def test_bad_budget_rejected(self): from greeneye.errors import GreenEyeError with self.assertRaises(GreenEyeError): run_profile(image=REAL_IMAGE, instructions=0) @unittest.skipUnless(os.path.isfile(REAL_IMAGE), "TU104 570.144 fwimage absent") class RealImageCycleProfileTests(unittest.TestCase): def test_bounded_profile_first_touch_and_metadata(self): with tempfile.TemporaryDirectory() as root: result = run_profile( image=REAL_IMAGE, instructions=200_000, record=root, name="unittest-cycle0" ) self.assertTrue(result.ran, result.reason) self.assertEqual(result.instructions, 200_000) self.assertEqual(result.stop_reason, "budget") self.assertGreater(result.cycles, result.instructions) self.assertEqual(result.cycle_model, DEFAULT_MODEL.name) self.assertEqual(result.first_unknown_mmio["addr"], TOUCH_A) self.assertEqual(result.first_unknown_mmio["rw"], "r") self.assertEqual(result.first_unknown_mmio["pc"], 0x84009014) self.assertEqual(result.stop_pc, 0x84009070) self.assertEqual(result.stop_pc_fw, 0x4009070) with open(os.path.join(result.session_dir, "session.json"), "r", encoding="utf-8") as fh: meta = json.load(fh) self.assertEqual(meta["cycles"], result.cycles) self.assertEqual(meta["instructions_executed"], 200_000) self.assertEqual(meta["cycle_model"], DEFAULT_MODEL.name) self.assertEqual(meta["firmware_sha256"], result.image_sha256) self.assertIn("modeled", meta["cycle_fidelity"]) self.assertEqual(meta["first_unknown_mmio"]["addr"], TOUCH_A) self.assertEqual(meta["cycle_model_table"]["name"], DEFAULT_MODEL.name) def test_two_runs_identical_cycles(self): with tempfile.TemporaryDirectory() as root: first = run_profile( image=REAL_IMAGE, instructions=200_000, record=os.path.join(root, "a"), name="unittest-cycle-a", ) second = run_profile( image=REAL_IMAGE, instructions=200_000, record=os.path.join(root, "b"), name="unittest-cycle-b", ) self.assertTrue(first.ran and second.ran) self.assertEqual(first.instructions, second.instructions) self.assertEqual(first.cycles, second.cycles) self.assertEqual(first.stop_pc, second.stop_pc) self.assertEqual(first.image_sha256, second.image_sha256) if __name__ == "__main__": unittest.main()