mirror of
https://github.com/iHaiDeeZ/SSBU.git
synced 2026-10-29 04:01:44 +01:00
176 lines
6.9 KiB
Python
176 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Name the PS4 profiler's samples (port/ps4_profile.cpp) and add them up by function.
|
|
|
|
Usage (in WSL):
|
|
profile.py [--fetch] [--elf <eboot elf>] [--log <ssbu.log>] [profile_<n>.txt ...]
|
|
|
|
--fetch downloads /data/SSBU/profiles/*.txt and ssbu.log from the console into
|
|
~/ssbu-build/logs/profiles first (and then names all of them). Each profile_<n>.txt gets a
|
|
profile_<n>.named.txt beside it: per thread its busy share and its busiest functions, and the
|
|
busiest functions of all sampled threads together.
|
|
|
|
Addresses: "elf 0x.." is relative to the eboot's .text (named against the ELF of the version
|
|
that ran, from ssbu.log's first line, ~/ssbu-build/oelf/v<version>.elf); "abs 0x.." inside an NRO
|
|
library (ssbu.log's "[ro] <name>'s code: ... at 0x..") against ~/ssbu-build/build/ssbu/nro-so;
|
|
"mod <module>+0x.." stays as it is (system modules).
|
|
"""
|
|
import argparse
|
|
import collections
|
|
import glob
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
HOME = os.path.expanduser("~/ssbu-build")
|
|
OUT = os.path.join(HOME, "logs", "profiles")
|
|
NRO_SO = os.path.join(HOME, "build", "ssbu", "nro-so")
|
|
HOST = "ftp://" + os.environ.get("PS4_HOST", "192.168.0.90") + ":" + os.environ.get("PS4_FTP_PORT", "2121")
|
|
SYMBOLIZER = "llvm-symbolizer-21"
|
|
|
|
|
|
def fetch():
|
|
os.makedirs(OUT, exist_ok=True)
|
|
listing = subprocess.run(["curl", "-s", "--max-time", "30", f"{HOST}/data/SSBU/profiles/"], capture_output=True, text=True).stdout
|
|
names = re.findall(r"(profile_\d+\.txt)\s*$", listing, re.M)
|
|
for n in names + ["../ssbu.log"]:
|
|
dest = os.path.join(OUT, os.path.basename(n))
|
|
subprocess.run(["curl", "-s", "--max-time", "60", "-o", dest, f"{HOST}/data/SSBU/profiles/{n}"], check=False)
|
|
print(f"fetched {len(names)} profiles and ssbu.log into {OUT}")
|
|
return [os.path.join(OUT, n) for n in names]
|
|
|
|
|
|
def nro_bases(log):
|
|
"""(start, end, name) of each NRO library's code, from ssbu.log."""
|
|
out = []
|
|
for m in re.finditer(r"\[ro\] (\S+)'s code: (\d+) MiB at (0x[0-9a-f]+)", open(log, errors="replace").read()):
|
|
start = int(m.group(3), 16)
|
|
out.append((start, start + (int(m.group(2)) + 1) * (1 << 20), m.group(1)))
|
|
return sorted(out)
|
|
|
|
|
|
def elf_for(log):
|
|
first = open(log, errors="replace").readline()
|
|
m = re.search(r"SSBU (\d\d\.\d\d)", first)
|
|
if m and os.path.isfile(os.path.join(HOME, "oelf", f"v{m.group(1)}.elf")):
|
|
return os.path.join(HOME, "oelf", f"v{m.group(1)}.elf")
|
|
found = sorted(glob.glob(os.path.join(HOME, "oelf", "v*.elf")), key=os.path.getmtime)
|
|
return found[-1] if found else None
|
|
|
|
|
|
def text_start(elf):
|
|
out = subprocess.run(["llvm-nm-21", elf], capture_output=True, text=True).stdout
|
|
m = re.search(r"^([0-9a-f]+) \S __text_start$", out, re.M)
|
|
return int(m.group(1), 16) if m else 0
|
|
|
|
|
|
def symbolize(obj, addresses):
|
|
"""{address: function name} for addresses in one object."""
|
|
addresses = sorted(set(addresses))
|
|
if not addresses:
|
|
return {}
|
|
r = subprocess.run([SYMBOLIZER, f"--obj={obj}", "--functions=linkage", "--demangle", "--no-inlines", "--output-style=GNU"],
|
|
input="\n".join(hex(a) for a in addresses) + "\n", capture_output=True, text=True)
|
|
lines = r.stdout.splitlines()
|
|
names = {}
|
|
for i, a in enumerate(addresses): # (GNU style: two lines an address)
|
|
name = lines[2 * i] if 2 * i < len(lines) else "??"
|
|
names[a] = name if name != "??" else f"?{a:#x}"
|
|
return names
|
|
|
|
|
|
def parse(path):
|
|
"""header lines, and [(thread line, [(count, where, waiting)])]"""
|
|
header, threads, cur = [], [], None
|
|
for line in open(path, errors="replace"):
|
|
line = line.rstrip("\n")
|
|
if line.startswith("thread "):
|
|
cur = (line, [])
|
|
threads.append(cur)
|
|
elif line.startswith(" ") and cur is not None:
|
|
m = re.match(r"\s+(\d+) (elf|abs|mod) (\S+)( wait)?$", line)
|
|
if m:
|
|
cur[1].append((int(m.group(1)), m.group(2), m.group(3), bool(m.group(4))))
|
|
else:
|
|
header.append(line)
|
|
cur = None
|
|
return header, threads
|
|
|
|
|
|
def name_profile(path, elf, log):
|
|
header, threads = parse(path)
|
|
tstart = text_start(elf) if elf else 0
|
|
nros = nro_bases(log) if log else []
|
|
elf_addrs, nro_addrs = [], collections.defaultdict(list)
|
|
for _line, samples in threads:
|
|
for _n, kind, where, _w in samples:
|
|
if kind == "elf":
|
|
elf_addrs.append(tstart + int(where, 16))
|
|
elif kind == "abs":
|
|
a = int(where, 16)
|
|
for s, e, n in nros:
|
|
if s <= a < e:
|
|
nro_addrs[n].append(a - s)
|
|
names = {("elf", a): v for a, v in symbolize(elf, elf_addrs).items()} if elf else {}
|
|
for n, addrs in nro_addrs.items():
|
|
so = os.path.join(NRO_SO, n + ".so")
|
|
if os.path.isfile(so):
|
|
names.update({(n, a): f"{v} [{n}]" for a, v in symbolize(so, addrs).items()})
|
|
|
|
def fname(kind, where):
|
|
if kind == "elf":
|
|
return names.get(("elf", tstart + int(where, 16)), "elf " + where)
|
|
if kind == "abs":
|
|
a = int(where, 16)
|
|
for s, e, n in nros:
|
|
if s <= a < e:
|
|
return names.get((n, a - s), f"{n}+{a - s:#x}")
|
|
return "abs " + where
|
|
return re.sub(r"\+0x[0-9a-f]+$", "", where) # (a system module: by module)
|
|
|
|
out_path = re.sub(r"\.txt$", "", path) + ".named.txt"
|
|
total = collections.Counter()
|
|
with open(out_path, "w") as out:
|
|
out.write("\n".join(header) + "\n\n")
|
|
for line, samples in threads:
|
|
per = collections.Counter()
|
|
n = sum(c for c, *_ in samples)
|
|
for c, kind, where, waiting in samples:
|
|
key = fname(kind, where) + (" (waiting)" if waiting else "")
|
|
per[key] += c
|
|
if not waiting:
|
|
total[key] += c
|
|
out.write(line + "\n")
|
|
for key, c in per.most_common(25):
|
|
out.write(f" {100.0 * c / max(n, 1):5.1f}% {key}\n")
|
|
out.write("\n")
|
|
busy = sum(total.values())
|
|
out.write(f"All sampled threads, busy samples ({busy}) by function:\n")
|
|
for key, c in total.most_common(80):
|
|
out.write(f" {100.0 * c / max(busy, 1):5.1f}% {key}\n")
|
|
return out_path
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--fetch", action="store_true")
|
|
ap.add_argument("--elf")
|
|
ap.add_argument("--log")
|
|
ap.add_argument("profiles", nargs="*")
|
|
a = ap.parse_args()
|
|
paths = a.profiles
|
|
if a.fetch:
|
|
paths = fetch() + paths
|
|
if not paths:
|
|
sys.exit("no profiles (use --fetch, or name them)")
|
|
log = a.log or os.path.join(os.path.dirname(os.path.abspath(paths[0])), "ssbu.log")
|
|
log = log if os.path.isfile(log) else None
|
|
elf = a.elf or (elf_for(log) if log else None)
|
|
print(f"eboot symbols: {elf}; NRO places: {log}")
|
|
for p in paths:
|
|
print("->", name_profile(p, elf, log))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|