1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-18 04:18:10 +00:00

feat(core/tools): improve alloc.py for seeing allocations

[no changelog]
This commit is contained in:
grdddj 2023-03-28 17:18:55 +02:00 committed by Jiří Musil
parent 44178514f3
commit e8f32755ba

View File

@ -1,14 +1,40 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, Protocol, TextIO
# for python37 support, is not present in typing there
from typing_extensions import TypedDict
import click import click
from dominate import document
from dominate.tags import *
from dominate.util import raw
HERE = Path(__file__).resolve().parent HERE = Path(__file__).resolve().parent
def parse_alloc_data(alloc_data): if TYPE_CHECKING:
parsed_data = {}
class LineAllocData(TypedDict):
total_allocs: int
total_calls: int
avg_allocs: float
# {filename:{lineno:LineAllocData}}
alloc_data_dict = Dict[str, Dict[int, LineAllocData]]
class SharedObject(Protocol):
data: alloc_data_dict
type: str
def parse_alloc_data(
alloc_data: TextIO,
) -> alloc_data_dict:
parsed_data: alloc_data_dict = {}
for line in alloc_data: for line in alloc_data:
ident, allocs, calls = line.strip().split(" ") ident, allocs, calls = line.strip().split(" ")
allocs = int(allocs) allocs = int(allocs)
@ -29,11 +55,14 @@ def parse_alloc_data(alloc_data):
@click.pass_context @click.pass_context
@click.option("-a", "--alloc-data", type=click.File(), default="src/alloc_data.txt") @click.option("-a", "--alloc-data", type=click.File(), default="src/alloc_data.txt")
@click.option("-t", "--type", type=click.Choice(("total", "avg")), default="avg") @click.option("-t", "--type", type=click.Choice(("total", "avg")), default="avg")
def cli(ctx, alloc_data, type): def cli(ctx: click.Context, alloc_data: TextIO, type: str):
ctx.obj = SimpleNamespace(data=parse_alloc_data(alloc_data), type=type) shared_obj: SharedObject = SimpleNamespace() # type: ignore
shared_obj.data = parse_alloc_data(alloc_data)
shared_obj.type = type
ctx.obj = shared_obj
def _normalize_filename(filename): def _normalize_filename(filename: str) -> str:
if filename.startswith("src/"): if filename.startswith("src/"):
return filename[4:] return filename[4:]
return filename return filename
@ -42,13 +71,18 @@ def _normalize_filename(filename):
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
@click.argument("filename") @click.argument("filename")
def annotate(obj, filename): def annotate(obj: SharedObject, filename: str):
filename = _normalize_filename(filename) filename = _normalize_filename(filename)
if obj.type == "total": if obj.type == "total":
alloc_str = lambda line: str(line["total_allocs"])
def alloc_str(line: LineAllocData) -> str:
return str(line["total_allocs"])
else: else:
alloc_str = lambda line: f"{line['avg_allocs']:.2f}"
def alloc_str(line: LineAllocData) -> str:
return f"{line['avg_allocs']:.2f}"
filedata = obj.data[filename] filedata = obj.data[filename]
@ -62,7 +96,9 @@ def annotate(obj, filename):
print(f"{linecount:>{maxlen}} {line}", end="") print(f"{linecount:>{maxlen}} {line}", end="")
def _list(obj, sort_by="avg_allocs", reverse=False): def _list(
obj: SharedObject, sort_by: str = "avg_allocs", reverse: bool = False
) -> list[tuple[str, float, int]]:
return sorted( return sorted(
( (
( (
@ -77,16 +113,21 @@ def _list(obj, sort_by="avg_allocs", reverse=False):
) )
@cli.command() @cli.command(name="list")
@click.pass_obj @click.pass_obj
@click.option("-r", "--reverse", is_flag=True) @click.option("-r", "--reverse", is_flag=True)
def list(obj, reverse): def list_function(obj: SharedObject, reverse: bool):
if obj.type == "total": if obj.type == "total":
field = "total_allocs" field = "total_allocs"
format_num = lambda l: f"{l[2]}"
def format_num(l: tuple[str, float, int]) -> str:
return f"{l[2]}"
else: else:
field = "avg_allocs" field = "avg_allocs"
format_num = lambda l: f"{l[1]:.2f}"
def format_num(l: tuple[str, float, int]) -> str:
return f"{l[1]:.2f}"
file_sums = _list(obj, field, reverse) file_sums = _list(obj, field, reverse)
@ -97,77 +138,139 @@ def list(obj, reverse):
print(f"{num_str:>{maxlen}} {filename}") print(f"{num_str:>{maxlen}} {filename}")
class HtmlTable: def get_biggest_line_allocations(
def __init__(self, f): obj: SharedObject, biggest_n: int
self.f = f ) -> list[tuple[str, float]]:
all_allocs: dict[str, float] = {}
for file, line_stats in obj.data.items():
for line, stats in line_stats.items():
all_allocs[f"{file}:{line}"] = stats["avg_allocs"]
def __enter__(self): return sorted(all_allocs.items(), key=lambda x: x[1], reverse=True)[:biggest_n]
self.f.write("<table>")
return self
def __exit__(self, type, value, traceback):
self.f.write("</table>")
def tr(self, *tds): def get_biggest_n_lines_for_each_file(
self.f.write("<tr>") obj: SharedObject, biggest_n: int
for td in tds: ) -> dict[str, list[int]]:
if isinstance(td, tuple): biggest_file_allocs: dict[str, list[int]] = {}
self.f.write(f"<td {td[0]}><tt>{td[1]}</tt></td>") for file, line_stats in obj.data.items():
else: biggest = sorted(
self.f.write(f"<td><tt>{td}</tt></td>") line_stats.items(), key=lambda x: x[1]["avg_allocs"], reverse=True
self.f.write("</tr>") )[:biggest_n]
biggest_file_allocs[file] = [line for line, _stats in biggest]
return biggest_file_allocs
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
@click.argument("htmldir") @click.argument("htmldir")
def html(obj, htmldir): def html(obj: SharedObject, htmldir: str):
file_sums = _list(obj, "total_allocs", reverse=True) file_sums = _list(obj, "total_allocs", reverse=True)
style_grey = "style='color: grey'" style_grey = "color: grey"
style_right = "style='text-align: right'" style_red = "color: red;"
style_blue = "color: blue;"
with open(f"{htmldir}/index.html", "w") as f: style_right = "text-align: right;"
f.write("<html>") css_smaller_mono = (
f.write( "body { font-size: 80%; font-family: 'Courier New', Courier, monospace; }"
f"<h3>Total allocations: {sum(total_sum for _, _, total_sum in file_sums)}</h3>"
) )
with HtmlTable(f) as table:
table.tr((style_right, "avg"), (style_right, "total"), "") n_biggest = 50
biggest_lines = get_biggest_line_allocations(obj, n_biggest)
for location, avg_alloc in reversed(biggest_lines):
# Prepending core/src so it can be opened via alt+click in VSCode
print(f"{avg_alloc:.2f} core/src/{location}")
# Create index.html - two tables
doc = document(title="Firmware allocations")
with doc.head:
meta(charset="utf-8")
style(css_smaller_mono)
with doc:
h3(f"{n_biggest} biggest allocations")
with table():
with thead():
with tr():
th("alloc", style=style_right)
th("file:line")
with tbody():
for location, avg_alloc in biggest_lines:
filename, lineno = location.split(":")
with tr():
td(f"{avg_alloc:.2f}", style=style_right)
td(
a(
location,
href=f"{filename}.html#{lineno}",
target="_blank",
)
)
h3(f"Total allocations: {sum(total_sum for _, _, total_sum in file_sums)}")
with table():
with thead():
with tr():
th("avg", style=style_right)
th("total", style=style_right)
th("file")
with tbody():
for filename, avg_sum, total_sum in file_sums: for filename, avg_sum, total_sum in file_sums:
table.tr( with tr():
(style_right, f"{avg_sum:.2f}"), td(f"{avg_sum:.2f}", style=style_right)
(style_right, total_sum), td(total_sum, style=style_right)
f"<a href='{filename}.html'>{filename}</a>", td(
a(
filename,
href=f"{filename}.html",
target="_blank",
) )
f.write("</html>") )
with open(f"{htmldir}/index.html", "w") as f:
f.write(doc.render())
# So we can highlight biggest allocations in each file
biggest_n_lines_for_each_file = get_biggest_n_lines_for_each_file(obj, 5)
# Create HTML for each file - one table in each
for filename in file_sums: for filename in file_sums:
filename = _normalize_filename(filename[0]) filename = _normalize_filename(filename[0])
htmlfile = Path(htmldir) / filename htmlfile = Path(htmldir) / filename
htmlfile.parent.mkdir(parents=True, exist_ok=True) htmlfile.parent.mkdir(parents=True, exist_ok=True)
with open(str(htmlfile) + ".html", "w") as f: doc = document(title=filename)
filedata = obj.data[filename] with doc.head:
f.write(f"<html><title>{filename}</title>") meta(charset="utf-8")
with HtmlTable(f) as table: style(css_smaller_mono)
table.tr( with doc:
(style_grey, "#"), (style_right, "avg"), (style_right, "total"), "" with table():
) with thead():
with tr():
th("#", style=style_grey)
th("avg", style=style_right)
th("total", style=style_right)
th("")
with tbody():
lineno = 0 lineno = 0
for line in open(HERE.parent / "src" / filename): for line in open(HERE.parent / "src" / filename):
line = line.rstrip("\n").replace(" ", "&nbsp;")
lineno += 1 lineno += 1
total = filedata.get(lineno, {}).get("total_allocs", 0) line_info = obj.data[filename].get(lineno, {})
avg = filedata.get(lineno, {}).get("avg_allocs", 0) total = line_info.get("total_allocs", 0)
avg = line_info.get("avg_allocs", 0)
table.tr( if lineno in biggest_n_lines_for_each_file[filename]:
(style_grey, lineno), row_style = style_red
(style_right, f"{avg:.2f}"), elif avg > 0:
(style_right, total), row_style = style_blue
line, else:
) row_style = None
f.write("</html>")
with tr(style=row_style, id=lineno):
td(lineno, style=style_grey)
td(f"{avg:.2f}", style=style_right)
td(total, style=style_right)
# Creating nonbreaking space, otherwise prefix
# whitespace is stripped
td(raw(line.rstrip("\n").replace(" ", "&nbsp;")))
with open(str(htmlfile) + ".html", "w") as f:
f.write(doc.render())
if __name__ == "__main__": if __name__ == "__main__":