Files
gex/scripts/prof-report.py
velzieandClaude Opus 5.5 a6e7c17f9e Restructure: hosts/, page/, ports/, site/, toolchain/, wine/, scripts/
The tree by what each part is, behind the top-level Makefile:
- shims/ -> hosts/ (the browser, kernel, native and Wayland hosts)
- web/ -> page/ (the page's source, build and serve), ports/mesa,
  ports/xwayland and ports/tint (the ports the page loads), site/ (static
  sites and the eject page)
- tools/gexcc -> toolchain/gexcc, with the sysroot and busybox builds from
  tools/wine-wasm in toolchain/sysroot and toolchain/busybox, and one
  toolchain/pins (musl, the LP64 musl, LLVM, busybox)
- tools/wine-wasm -> wine/: dist.sh (was wine64-dist.sh), wine.pins (64-bit
  Wine, and the 32-bit tree its wineserver builds from), libs.pins (the Unix
  libraries), the 32-bit-only pieces in wine/legacy32 (its own pins and
  legacy.mk, which wine/Makefile includes)
- tools/*.py and tools/*.sh -> scripts/ (the GL generators in scripts/gl)
- wine32/src.sh is scripts/fetch-pinned.sh, which takes the trees the old
  script marked as its own

References follow the moves: paths from the tree's root by the map, paths
from a script's own directory by where their target went.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_e0aa53aa95eb4b86910519b4d14812a5
2026-11-04 17:30:24 -05:00

140 lines
4.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Symbolise a GEX_PROF profile (crates/gexc/src/prof.rs).
scripts/prof-report.py PROFILE [--modules] [--top N] [--cats FILE.json] [--perfmap PATH]
--modules sums compiled wasm by module (big modules are whole-image
units, small ones tier-1's runtime translations).
PROFILE is what GEX_PROF wrote (`pid`, `samples`, then `pc count` in hex),
with PROFILE.maps beside it. Compiled wasm is named by wasmtime's perf map
(/tmp/perf-<pid>.map, from GEX_PERFMAP=1); native code by the binaries'
symbol tables (`nm`). Prints the top symbols by self samples, then the
share of each category: the first regex in --cats (a JSON list of
[name, regex] matched against "object:symbol") that matches, else "other".
"""
import bisect, collections, json, re, subprocess, sys
args = sys.argv[1:]
bymod = '--modules' in args
args = [a for a in args if a != '--modules']
top = 40
cats = None
perfmap = None
while len(args) > 1:
if args[-2] == '--top':
top = int(args[-1]); args = args[:-2]
elif args[-2] == '--cats':
cats = json.load(open(args[-1])); args = args[:-2]
elif args[-2] == '--perfmap':
perfmap = args[-1]; args = args[:-2]
else:
break
prof = args[0]
lines = open(prof).read().split('\n')
pid = int(lines[0].split()[1])
total = int(lines[1].split()[1])
hist = {}
for l in lines[2:]:
if l.strip():
pc, c = l.split()
hist[int(pc, 16)] = int(c)
maps = []
for l in open(prof.rsplit('.', 1)[0] + '.maps' if not prof.endswith('.prof') else prof[:-5] + '.maps'):
p = l.split()
lo, hi = (int(x, 16) for x in p[0].split('-'))
maps.append((lo, hi, p[1], int(p[2], 16), p[5] if len(p) > 5 else ''))
maps.sort()
starts = [m[0] for m in maps]
jit = []
# wasmtime names every module's functions `wasm[0]::function[k]`: modules
# are told apart by the perf map's order (each module's entries are written
# together, its function indices rising), and named by their size, so the
# big whole-image units stand out from tier-1's small runtime modules.
mods = [] # per jit entry: module number
sizes = collections.Counter()
try:
mod, last = -1, None
for l in open(perfmap or f'/tmp/perf-{pid}.map'):
a, n, name = l.rstrip('\n').split(' ', 2)
m = re.match(r'wasm\[\d+\]::function\[(\d+)\]', name)
if m:
k = int(m.group(1))
if last is None or k <= last:
mod += 1
last = k
sizes[mod] += 1
jit.append((int(a, 16), int(n, 16), name, mod))
except FileNotFoundError:
pass
jit.sort()
jstarts = [j[0] for j in jit]
def modname(m):
n = sizes[m]
return f'module#{m}({n} fns)' if n >= 1000 else 'small-module'
symcache = {}
def syms(path):
if path not in symcache:
out = []
try:
# file offset -> vaddr, from the LOAD headers
ph = subprocess.run(['readelf', '-lW', path], capture_output=True, text=True).stdout
loads = [(int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)) for m in
re.finditer(r'LOAD\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+0x[0-9a-f]+\s+0x([0-9a-f]+)', ph)]
nm = subprocess.run(['nm', '-n', '-C', '--defined-only', path], capture_output=True, text=True).stdout
if not nm.strip():
nm = subprocess.run(['nm', '-n', '-C', '-D', '--defined-only', path], capture_output=True, text=True).stdout
for l in nm.split('\n'):
p = l.split(' ', 2)
if len(p) == 3 and p[1] in 'tTwWiI':
out.append((int(p[0], 16), p[2]))
except Exception:
loads = []
out.sort()
symcache[path] = (loads, out, [o[0] for o in out])
return symcache[path]
def name(pc):
k = bisect.bisect_right(jstarts, pc) - 1
if k >= 0 and jit[k][0] <= pc < jit[k][0] + jit[k][1]:
if bymod and 'trampoline' not in jit[k][2]:
return 'jit:' + modname(jit[k][3])
return 'jit:' + jit[k][2]
k = bisect.bisect_right(starts, pc) - 1
if k < 0 or not (maps[k][0] <= pc < maps[k][1]):
return '?:?'
lo, hi, perm, off, path = maps[k]
if not path.startswith('/'):
return f'anon[{perm}]:?'
loads, out, ost = syms(path)
foff = pc - lo + off
va = foff
for o, v, fsz in loads:
if o <= foff < o + fsz:
va = foff - o + v
j = bisect.bisect_right(ost, va) - 1
s = out[j][1] if j >= 0 else '?'
return path.rsplit('/', 1)[-1] + ':' + s
by = collections.Counter()
for pc, c in hist.items():
by[name(pc)] += c
print(f'{total} samples')
for s, c in by.most_common(top):
print(f'{100 * c / total:6.2f}% {c:8} {s[:160]}')
if cats:
cc = collections.Counter()
for s, c in by.items():
for cat, rx in cats:
if re.search(rx, s):
cc[cat] += c
break
else:
cc['other'] += c
print()
for cat, c in cc.most_common():
print(f'{100 * c / total:6.2f}% {cat}')