Files
soothe2-re/scripts/perfbp.py
T

218 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""perfbp.py — hardware data-breakpoint via perf_event_open (no ptrace).
Watches READS of the live FIR kernel word across all host threads during
offline render; collects sampler IPs -> the consumer."""
import ctypes
import glob
import mmap
import os
import struct
import subprocess
import sys
import time
PERF_TYPE_BREAKPOINT = 5
PERF_SAMPLE_IP = 1 << 0
HW_BREAKPOINT_R = 2
PERF_RECORD_SAMPLE = 9
SYS_perf_event_open = 298
IOCTL_ENABLE = 0x2400 # PERF_EVENT_IOC_ENABLE
IOC_FLAG_GROUP = 0
class PerfAttr(ctypes.Structure):
_fields_ = [
('type', ctypes.c_uint32),
('size', ctypes.c_uint32),
('config', ctypes.uint64 if hasattr(ctypes, 'uint64') else ctypes.c_uint64),
('sample_period', ctypes.c_uint64),
('sample_type', ctypes.c_uint64),
('read_format', ctypes.c_uint64),
('flags', ctypes.c_uint64), # bitfield packed: disabled=bit0 ...
('wakeup_events', ctypes.c_uint32),
('bp_type', ctypes.c_uint32),
('bp_addr', ctypes.c_uint64),
('bp_len', ctypes.c_uint64),
]
libc = ctypes.CDLL('libc.so.6', use_errno=True)
def find_host():
for p in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(p))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'soothe2' in maps and 'reaper' not in cmd:
return pid
return None
def perf_open(tid, addr):
a = PerfAttr()
a.type = PERF_TYPE_BREAKPOINT
a.size = ctypes.sizeof(a)
a.config = 0
a.sample_period = 1
a.sample_type = PERF_SAMPLE_IP
a.read_format = 0
a.flags = 1 | (1 << 5) # disabled=1, exclude_kernel=1
a.wakeup_events = 1
a.bp_type = HW_BREAKPOINT_R
a.bp_addr = addr
a.bp_len = 4
libc.syscall.restype = ctypes.c_long
r = libc.syscall(ctypes.c_long(SYS_perf_event_open), ctypes.byref(a),
ctypes.c_int(tid), ctypes.c_int(-1), ctypes.c_uint(-1),
ctypes.c_void_p(0))
if r == -1:
e = ctypes.get_errno()
return None, e
return r, 0
def main():
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
wav = '/home/m/soothe-bt/dual_b1q_0.5.wav'
wt0 = os.path.getmtime(wav) if os.path.exists(wav) else 0
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', '/home/m/soothe-bt/dual_b1q_0.5.rpp'],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
host = None
t0 = time.time()
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.002)
if not host:
print('NO HOST')
return 1
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
def rd(a, n):
try:
return os.pread(fd, n, a)
except OSError:
return None
# wait for instance
ctx = firptr = None
vt = struct.pack('<Q', 0x1824AC210)
while time.time() - t0 < 25 and firptr is None:
for line in open(f'/proc/{host}/maps'):
parts = line.split()
if 'rw' not in parts[1]:
continue
lo, hi = (int(x, 16) for x in parts[0].split('-'))
CH = 16 * 1024 * 1024
a = lo
while a < hi and firptr is None:
d = rd(a, min(CH + 4096, hi - a))
if not d:
break
j = d.find(vt)
while j >= 0:
cand = a + j
sb = rd(cand + 0x540870, 4)
if sb and struct.unpack('<f', sb)[0] > 100:
ctx = cand
firptr = struct.unpack('<Q', rd(cand + 0x540668, 8))[0]
break
j = d.find(vt, j + 1)
a += CH
if firptr:
break
print('host %d ctx %#x fir %#x at %.2fs'
% (host, ctx or 0, firptr or 0, time.time() - t0), flush=True)
if not firptr:
return 1
watch = firptr + 43 * 8 # bin43 re word
# attach to all current threads; remember to attach newborns too
events = {}
errs = {}
def attach(tid):
fde, e = perf_open(tid, watch)
if fde is None or fde < 0:
errs[e] = errs.get(e, 0) + 1
return None
events[fde] = tid
libc.mmap.restype = ctypes.c_void_p
m = libc.mmap(None, 2 * 4096, 1 | 2, 2, fde, 0) # PROT_R|W, MAP_PRIVATE
if m in (None, ctypes.c_void_p(-1).value):
return None
bufs[fde] = (m, (ctypes.c_char * (2 * 4096)).from_address(m))
libc.ioctl.argtypes = [ctypes.c_int] * 3 + [ctypes.c_void_p]
libc.ioctl(fde, IOCTL_ENABLE, 0)
return fde
bufs = {}
# pick top-CPU thread only (minimise BP resource demand)
import time as _t
def tcpu(tid):
try:
f = open(f'/proc/{host}/task/{tid}/stat').read().split()
return int(f[13]) + int(f[14])
except Exception:
return -1
tids = [int(os.path.basename(p)) for p in glob.glob(f'/proc/{host}/task/*')]
c0 = {t: tcpu(t) for t in tids}
_t.sleep(0.4)
deltas = sorted(((tcpu(t) - c0.get(t, 0), t) for t in tids), reverse=True)
target = deltas[0][1] if deltas else host
print('target tid=%d cpuΔ=%s all=%s' % (target, deltas[0], deltas[:6]), flush=True)
fde = attach(target)
n_ok = 1 if fde is not None else 0
print('attached %d fds; errno hist=%s' % (n_ok, errs), flush=True)
# let render run; drain buffers periodically
ips = {}
DUR = float(os.environ.get('BP_DUR', '12'))
t_end = time.time() + DUR
fresh_t = None
DATA = 4096
while time.time() < t_end:
time.sleep(0.05)
if fresh_t is None and os.path.exists(wav) and os.path.getmtime(wav) > wt0:
fresh_t = time.time() - t0
print('wav fresh at %.2fs' % fresh_t, flush=True)
for fde, (m, arr) in list(bufs.items()):
head, tail = struct.unpack_from('<QQ', bytes(arr[:16]), 0)
n = head - tail
if n == 0:
continue
blob = bytes(arr[4096:8192])
p = tail % DATA
consumed = 0
while consumed < n:
if p + 8 > DATA:
p = 0
typ, misc, recsz = struct.unpack_from('<IHH', blob, p)
if recsz < 8:
break
rec = blob[p:p + recsz]
if typ == PERF_RECORD_SAMPLE and recsz >= 16:
ip = struct.unpack_from('<Q', rec, 8)[0]
if 0x180001000 <= ip < 0x181baa000:
key = ip >> 4 << 4
ips[key] = ips.get(key, 0) + 1
adv = max(recsz, 8)
consumed += adv
p = (p + adv) % DATA
struct.pack_into('<Q', arr, 8, head)
print('distinct ips:', len(ips), 'total:', sum(ips.values()))
for a_, c in sorted(ips.items(), key=lambda x: -x[1])[:20]:
print('%#x %d' % (a_, c))
proc.kill()
return 0
if __name__ == '__main__':
main()