218 lines
7.3 KiB
Python
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()
|