mirror of
https://github.com/irmen/prog8.git
synced 2024-11-22 00:31:56 +00:00
176 lines
7.2 KiB
Python
Executable File
176 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""
|
|
This is a simple run-time profiler tool for X16 assembly programs.
|
|
It takes an assembly list file (as produced by 64tass/turbo assembler) and
|
|
a memory access statistics dump file (produced by the emulator's -memorystats option)
|
|
and prints out what assembly lines and variables were read from and written to the most.
|
|
These may indicate hot paths or even bottlenecks in your program,
|
|
and what variables in system ram might be better placed in Zeropage.
|
|
|
|
The -memorystats option in the emulator is work in progress at the time of writing.
|
|
"""
|
|
|
|
|
|
import sys
|
|
import argparse
|
|
import operator
|
|
from typing import Tuple
|
|
|
|
|
|
class AsmList:
|
|
"""parses a l64tass Turbo Assembler Macro listing file"""
|
|
|
|
def __init__(self, filename: str) -> None:
|
|
self.lines = []
|
|
symbols = {}
|
|
self.check_format(filename)
|
|
for index, line in enumerate(open(filename, "rt"), 1):
|
|
if not line or line == '\n' or line[0] == ';':
|
|
continue
|
|
if line[0] == '=':
|
|
value, symbol, _ = line.split(maxsplit=2)
|
|
value = value[1:]
|
|
if value:
|
|
if value[0] == '$':
|
|
address = int(value[1:], 16)
|
|
else:
|
|
address = int(value)
|
|
symbols[symbol] = (address, index)
|
|
elif line[0] == '>':
|
|
value, rest = line.split(maxsplit=1)
|
|
address = int(value[1:], 16)
|
|
self.lines.append((address, rest.strip(), index))
|
|
elif line[0] == '.':
|
|
value, rest = line.split(maxsplit=1)
|
|
address = int(value[1:], 16)
|
|
self.lines.append((address, rest.strip(), index))
|
|
else:
|
|
raise ValueError("invalid syntax: " + line)
|
|
for name, (address, index) in symbols.items():
|
|
self.lines.append((address, name, index))
|
|
self.lines.sort()
|
|
|
|
def check_format(self, filename: str) -> None:
|
|
with open(filename, "rt") as inf:
|
|
firstline = inf.readline()
|
|
if firstline.startswith(';') and "listing file" in firstline:
|
|
pass
|
|
else:
|
|
secondline = inf.readline()
|
|
if secondline.startswith(';') and "listing file" in secondline:
|
|
pass
|
|
else:
|
|
raise IOError("listing file is not in recognised 64tass / turbo assembler format")
|
|
|
|
def print_info(self) -> None:
|
|
print("number of actual lines in the assembly listing:", len(self.lines))
|
|
|
|
def find(self, address: int) -> list[Tuple[int, str, int]]:
|
|
exact_result = [(line_addr, name, index) for line_addr, name, index in self.lines if line_addr == address]
|
|
if exact_result:
|
|
return exact_result
|
|
fuzzy_result = [(line_addr, name, index) for line_addr, name, index in self.lines if line_addr == address - 1]
|
|
if fuzzy_result:
|
|
return fuzzy_result
|
|
fuzzy_result = [(line_addr, name, index) for line_addr, name, index in self.lines if line_addr == address + 1]
|
|
if fuzzy_result:
|
|
return fuzzy_result
|
|
return []
|
|
|
|
|
|
class MemoryStats:
|
|
"""parses the read and write counts in a x16emulator memory statistics file"""
|
|
|
|
def __init__(self, filename: str) -> None:
|
|
self.check_format(filename)
|
|
self.reads = []
|
|
self.writes = []
|
|
|
|
def parse(rest: str) -> Tuple[int, int, int]:
|
|
if ':' in rest:
|
|
bank = int(rest[:2], 16)
|
|
address = int(rest[3:7], 16)
|
|
count = int(rest[8:])
|
|
else:
|
|
bank = 0 # regular system RAM, bank is irrellevant.
|
|
address = int(rest[:4], 16)
|
|
count = int(rest[5:])
|
|
return bank, address, count
|
|
|
|
for line in open(filename, "rt"):
|
|
if line.startswith("r "):
|
|
bank, address, count = parse(line[2:])
|
|
self.reads.append(((bank, address), count))
|
|
elif line.startswith("w "):
|
|
bank, address, count = parse(line[2:])
|
|
self.writes.append(((bank, address), count))
|
|
self.reads.sort(reverse=True, key=operator.itemgetter(1))
|
|
self.writes.sort(reverse=True, key=operator.itemgetter(1))
|
|
|
|
def check_format(self, filename: str) -> None:
|
|
with open(filename, "rt") as inf:
|
|
firstline = inf.readline()
|
|
if not firstline.startswith("Usage counts "):
|
|
raise IOError("memory statistics file is not recognised as a X16 emulator memorystats file")
|
|
|
|
def print_info(self) -> None:
|
|
print("number of distinct addresses read from :", len(self.reads))
|
|
print("number of distinct addresses written to :", len(self.writes))
|
|
counts = sum(c for _, c in self.reads)
|
|
print(f"total number of reads : {counts} ({counts//1_000_000}M)")
|
|
counts = sum(c for _, c in self.writes)
|
|
print(f"total number of writes : {counts} ({counts//1_000_000}M)")
|
|
|
|
|
|
def profile(number_of_lines: int, asmlist: str, memstats: str) -> None:
|
|
"""performs profiling analysis of the given assembly listing file based on the given memory stats file"""
|
|
asm = AsmList(asmlist)
|
|
stats = MemoryStats(memstats)
|
|
asm.print_info()
|
|
stats.print_info()
|
|
|
|
def print_unknown(address: int) -> None:
|
|
if address < 0x100:
|
|
print("unknown zp")
|
|
elif address < 0x200:
|
|
print("cpu stack")
|
|
elif address in range(0x9f00, 0xa000):
|
|
print("io")
|
|
else:
|
|
print("unknown")
|
|
|
|
print(f"\ntop {number_of_lines} most reads:")
|
|
for (bank, address), count in stats.reads[:number_of_lines]:
|
|
print(f"${address:04x} ({count}) : ", end="")
|
|
if bank == 0 and address < 0xa000:
|
|
result = asm.find(address)
|
|
if result:
|
|
lines = [f"${address:04x} '{line}' (line {line_number})" for address, line, line_number in result]
|
|
print(", ".join(lines))
|
|
else:
|
|
print_unknown(address)
|
|
else:
|
|
print(f"banked memory: {bank:02x}:{address:04x}")
|
|
print(f"\ntop {number_of_lines} most writes:")
|
|
for (bank, address), count in stats.writes[:number_of_lines]:
|
|
print(f"${address:04x} ({count}) : ", end="")
|
|
if bank == 0 and address < 0xa000:
|
|
result = asm.find(address)
|
|
if result:
|
|
lines = [f"${address:04x} '{line}' (line {line_number})" for address, line, line_number in result]
|
|
print(", ".join(lines))
|
|
else:
|
|
print_unknown(address)
|
|
else:
|
|
print(f"banked memory: {bank:02x}:{address:04x}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="simple X16 assembly run time profiler")
|
|
parser.add_argument("-n", dest="number", type=int, default=20, help="amount of reads and writes to print (default 20)")
|
|
parser.add_argument("asmlistfile", type=str, help="the 64tass/turbo assembler listing file to read")
|
|
parser.add_argument("memorystatsfile", type=str, help="the X16 emulator memstats dump file to read")
|
|
args = parser.parse_args()
|
|
profile(args.number, args.asmlistfile, args.memorystatsfile)
|