From d3dcd24b4d0a68c294f7696b2124820eb2378b37 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Wed, 29 May 2024 00:56:31 +0200 Subject: [PATCH] add profiler script --- .../prog8/codegen/cpu6502/AssemblyProgram.kt | 7 +- docs/source/todo.rst | 8 +- scripts/profiler.py | 164 ++++++++++++++++++ 3 files changed, 172 insertions(+), 7 deletions(-) create mode 100755 scripts/profiler.py diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/AssemblyProgram.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/AssemblyProgram.kt index 7dac8a19f..d471b87b7 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/AssemblyProgram.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/AssemblyProgram.kt @@ -28,7 +28,7 @@ internal class AssemblyProgram( // add "-Wlong-branch" to see warnings about conversion of branch instructions to jumps (default = do this silently) val command = mutableListOf("64tass", "--ascii", "--case-sensitive", "--long-branch", "-Wall", // "-Wno-strict-bool", "-Werror", - "--dump-labels", "--vice-labels", "--labels=$viceMonListFile", "--no-monitor" + "--dump-labels", "--vice-labels", "--labels=$viceMonListFile" ) if(options.warnSymbolShadowing) @@ -39,8 +39,9 @@ internal class AssemblyProgram( if(options.asmQuiet) command.add("--quiet") - if(options.asmListfile) - command.add("--list=$listFile") + if(options.asmListfile) { + command.addAll(listOf("--list=$listFile", "--no-monitor")) + } val outFile = when (options.output) { OutputType.PRG -> { diff --git a/docs/source/todo.rst b/docs/source/todo.rst index ffbd5f823..3ee5a5508 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -4,6 +4,8 @@ TODO See https://github.com/irmen/prog8/issues/134 + any other issues that got reported. +Document scripts/profiler.py in manual? + ... @@ -68,10 +70,8 @@ Optimizations: those checks should probably be removed, or be made permanent - optimizeCommonSubExpressions: currently only looks in expressions on a single line, could search across multiple expressions -STRUCTS again? --------------- - -What if we were to re-introduce Structs in prog8? Some thoughts: +STRUCTS? +-------- - can contain only numeric types (byte,word,float) - no nested structs, no reference types (strings, arrays) inside structs - only as a reference type (uword pointer). This removes a lot of the problems related to introducing a variable length value type. diff --git a/scripts/profiler.py b/scripts/profiler.py new file mode 100755 index 000000000..65c9ab835 --- /dev/null +++ b/scripts/profiler.py @@ -0,0 +1,164 @@ +#!/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() + 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") + 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") + 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") + 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)