mirror of
https://github.com/irmen/prog8.git
synced 2025-02-04 02:30:19 +00:00
add profiler script
This commit is contained in:
parent
fd1e6796ef
commit
d3dcd24b4d
@ -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 -> {
|
||||
|
@ -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.
|
||||
|
164
scripts/profiler.py
Executable file
164
scripts/profiler.py
Executable file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user