Skip to content

Commit c0345cd

Browse files
author
Jyri Sarha
committed
tools: debug-stream.py: debug_stream record receiving and decoding
Python implementation for receiving and decoding debug_stream records from debug window slot transportation. Opens SOF debugfs file "debug_stream" reads and decodes the records from the circular buffer documented in soc_debug_window_slot.h. This initial version only knows of DEBUG_STREAM_RECORD_ID_THREAD_INFO records. Signed-off-by: Jyri Sarha <jyri.sarha@linux.intel.com>
1 parent 3f955ef commit c0345cd

2 files changed

Lines changed: 399 additions & 217 deletions

File tree

tools/debugstream/debug-stream.py

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
#
4+
# Copyright (c) 2024, Intel Corporation.
5+
6+
import argparse
7+
import mmap
8+
import ctypes
9+
import time
10+
import sys
11+
12+
import logging
13+
logging.basicConfig(format="%(filename)s:%(lineno)s %(funcName)s: %(message)s",
14+
level=logging.WARNING)
15+
16+
DEBUG_STREAM_PAYLOAD_MAGIC = 0x1ED15EED
17+
18+
""" Generic Debug-stream header """
19+
class DebugStreamHdr(ctypes.Structure):
20+
_fields_ = [
21+
("magic", ctypes.c_uint),
22+
("hdr_size", ctypes.c_uint),
23+
]
24+
25+
""" Debug Stream record for passing debug data """
26+
class DebugStreamRecord(ctypes.Structure):
27+
_fields_ = [
28+
("id", ctypes.c_uint),
29+
("serial", ctypes.c_uint),
30+
("size", ctypes.c_uint),
31+
]
32+
33+
""" Thread Info record header """
34+
class CPUInfo(ctypes.Structure):
35+
_pack_ = 1
36+
_fields_ = [
37+
("hdr", DebugStreamRecord),
38+
("load", ctypes.c_ubyte),
39+
("thread_count", ctypes.c_ubyte),
40+
]
41+
42+
""" Thread specific data-record, the thread name string starts after name_len """
43+
class ThreadInfo(ctypes.Structure):
44+
_pack_ = 1
45+
_fields_ = [
46+
("stack_usage", ctypes.c_ubyte),
47+
("cpu_load", ctypes.c_ubyte),
48+
("name_len", ctypes.c_ubyte),
49+
]
50+
51+
WSIZE = ctypes.sizeof(ctypes.c_uint)
52+
53+
""" Debug Stream record decoder and printer class """
54+
class RecordPrinter:
55+
RECORD_ID_UNINITIALIZED = 0
56+
RECORD_ID_THREAD_INFO = 1
57+
58+
def print_record(self, record, cpu):
59+
recp = ctypes.cast(record, ctypes.POINTER(DebugStreamRecord))
60+
logging.debug(f"rec: {recp.contents.id} {recp.contents.serial} {recp.contents.size}")
61+
if recp.contents.id == self.RECORD_ID_THREAD_INFO:
62+
return self.print_thread_info(record, cpu)
63+
else:
64+
logging.warning(f"cpu {cpu}: Unsupported recodrd type {rec.contents.id}")
65+
return True
66+
67+
def print_thread_info(self, record, cpu):
68+
remlen = len(record) - ctypes.sizeof(CPUInfo)
69+
if remlen < 0:
70+
logging.info(f"Buffer end reached, parsing failed")
71+
return False
72+
cpup = ctypes.cast(record, ctypes.
73+
POINTER(CPUInfo))
74+
print("CPU %u: Load: %02.1f%% %u threads (serial %u)" %
75+
(cpu, cpup.contents.load / 2.55,
76+
cpup.contents.thread_count, cpup.contents.hdr.serial))
77+
remain = (ctypes.c_ubyte *
78+
(len(record) - ctypes.sizeof(CPUInfo)
79+
)).from_address(ctypes.addressof(record)
80+
+ ctypes.sizeof(CPUInfo))
81+
for i in range(cpup.contents.thread_count):
82+
remlen = remlen - ctypes.sizeof(ThreadInfo)
83+
if remlen < 0:
84+
logging.info(f"Buffer end reached, parsing failed")
85+
return False
86+
threadp = ctypes.cast(remain, ctypes.POINTER(ThreadInfo))
87+
remain = (ctypes.c_ubyte *
88+
(len(remain) - ctypes.sizeof(ThreadInfo)
89+
)).from_address(ctypes.addressof(remain)
90+
+ ctypes.sizeof(ThreadInfo))
91+
remlen = remlen - threadp.contents.name_len
92+
if remlen < 0:
93+
logging.info(f"Buffer end reached, parsing failed")
94+
return False
95+
name = bytearray(remain[:threadp.contents.name_len]).decode("utf-8")
96+
remain = (ctypes.c_ubyte *
97+
(len(remain) - threadp.contents.name_len
98+
)).from_address(ctypes.addressof(remain)
99+
+ threadp.contents.name_len)
100+
print(" %-20s stack %02.1f%%\tload %02.1f%%" %
101+
(name, threadp.contents.stack_usage / 2.55,
102+
threadp.contents.cpu_load / 2.55))
103+
return True
104+
105+
""" Describes CPU specific circular buffers """
106+
class DebugStreamSectionDescriptor(ctypes.Structure):
107+
_fields_ = [
108+
("core_id", ctypes.c_uint),
109+
("buf_words", ctypes.c_uint),
110+
("offset", ctypes.c_uint),
111+
# This is for cacheline alignment
112+
("padding", ctypes.c_ubyte * (64 - 3 * WSIZE))
113+
]
114+
115+
""" Debug Slot transport specific Debug Stream header, padded to cache line """
116+
class DebugStreamSlotHdr(ctypes.Structure):
117+
_fields_ = [
118+
("hdr", DebugStreamHdr),
119+
("total_size", ctypes.c_uint),
120+
("num_sections", ctypes.c_uint),
121+
# This is for cacheline alignment
122+
("padding", ctypes.c_ubyte * (64 - 2 * WSIZE -
123+
ctypes.sizeof(DebugStreamHdr)))
124+
]
125+
126+
""" Live data header for CPU specific circular buffer """
127+
class CircularBufHdr(ctypes.Structure):
128+
_fields_ = [
129+
("next_serial", ctypes.c_uint),
130+
("w_ptr", ctypes.c_uint),
131+
]
132+
133+
""" Class for extracting records from circular buffer """
134+
class CircularBufferDecoder:
135+
desc = None
136+
boffset = None
137+
buf_words = None
138+
cpu = None
139+
printer = None
140+
prev_w_ptr = 0
141+
prev_serial = None
142+
error_count = 0
143+
def __init__(self, desc, cpu, printer):
144+
self.desc = desc
145+
self.boffset = desc.offset + ctypes.sizeof(CircularBufHdr)
146+
self.buf_words = desc.buf_words
147+
self.cpu = cpu
148+
self.printer = printer
149+
logging.debug(f"boffset {self.boffset} buf_words {self.buf_words} cpu {self.cpu}")
150+
151+
def get_hdr(self, slot, pos):
152+
if pos >= self.buf_words:
153+
logging.warning(f"Bad position {pos}")
154+
return None
155+
hdr_size = ctypes.sizeof(DebugStreamRecord)
156+
hdr_words = hdr_size // WSIZE
157+
if pos + hdr_words > self.buf_words:
158+
hdr = (ctypes.c_ubyte * hdr_size)()
159+
size1 = (self.buf_words - pos) * WSIZE
160+
size2 = hdr_size - size1
161+
pos1 = self.boffset + pos * WSIZE
162+
pos2 = self.boffset
163+
logging.debug(f"Wrapped header {pos} {hdr_words} {self.buf_words} {size1}")
164+
165+
hdr[0:size1] = slot[pos1:pos1 + size1]
166+
hdr[size1:hdr_size] = slot[pos2:pos2 + size2]
167+
header = ctypes.cast(hdr, ctypes.POINTER(DebugStreamRecord)).contents
168+
else:
169+
header = ctypes.cast(slot[self.boffset + pos * WSIZE:],
170+
ctypes.POINTER(DebugStreamRecord)).contents
171+
if header.id > 100 or header.size >= self.buf_words:
172+
logging.warning(f"Broken record id {header.id} serial {header.serial} size {header.size}")
173+
return None
174+
return header
175+
176+
def get_record(self, slot, pos, serial):
177+
rec = self.get_hdr(slot, pos)
178+
if rec == None or rec.size == 0:
179+
return None
180+
logging.debug(f"got header at pos {pos} rec {rec.id} {rec.serial} {rec.size}")
181+
if serial != None and rec.serial != serial:
182+
logging.warning(f"Record serial mismatch {rec.serial} != {serial}, pos {pos} size {rec.size}")
183+
self.error_count = self.error_count + 1
184+
return None
185+
rwords = rec.size
186+
rsize = rec.size * WSIZE
187+
if pos + rwords > self.buf_words:
188+
record = (ctypes.c_ubyte * rsize)()
189+
size1 = (self.buf_words - pos) * WSIZE
190+
size2 = rsize - size1
191+
pos1 = self.boffset + pos * WSIZE
192+
pos2 = self.boffset
193+
logging.debug(f"Wrapped record {pos} {rsize} {self.buf_words} {size1}")
194+
195+
record[0:size1] = slot[pos1:pos1 + size1]
196+
record[size1:rsize] = slot[pos2:pos2 + size2]
197+
else:
198+
record = (ctypes.c_ubyte * rsize
199+
).from_buffer_copy(slot, self.boffset + pos * WSIZE)
200+
logging.info(f"got {rec.serial}")
201+
self.error_count = 0
202+
return record
203+
204+
def catch_up(self, slot):
205+
circ = CircularBufHdr.from_buffer_copy(
206+
slot, self.desc.offset)
207+
if circ.next_serial == 0 or circ.w_ptr >= self.buf_words:
208+
return
209+
self.decode_past_records(slot, circ.w_ptr, circ.next_serial)
210+
self.prev_w_ptr = circ.w_ptr
211+
self.prev_serial = circ.next_serial - 1
212+
logging.info(f"serial {self.prev_serial} w_ptr {self.prev_w_ptr}")
213+
214+
def decode_past_records(self, slot, pos, serial):
215+
if self.prev_serial != None and self.prev_serial >= serial - 1:
216+
return
217+
if pos == 0:
218+
spos = self.buf_words - 1
219+
else:
220+
spos = pos - 1
221+
bsize = ctypes.cast(slot[self.boffset + spos*4:],
222+
ctypes.POINTER(ctypes.c_uint)).contents.value
223+
bpos = pos - bsize
224+
if bpos < 0:
225+
bpos = self.buf_words + pos - bsize
226+
rec = self.get_hdr(slot, bpos)
227+
if bsize != rec.size:
228+
return
229+
if serial != None:
230+
if rec.serial != serial - 1:
231+
return
232+
else:
233+
serial = rec.serial + 1
234+
235+
self.decode_past_records(slot, bpos, serial - 1)
236+
237+
record = self.get_record(slot, bpos, serial - 1)
238+
if record != None:
239+
if not self.printer.print_record(record, self.cpu):
240+
logging.info(f"Parse failed on record {serial - 1}")
241+
logging.info(f"Printing {serial - 1} success")
242+
else:
243+
logging.info(f"Broken record {serial - 1}")
244+
245+
def get_next_record(self, slot):
246+
if self.prev_serial != None:
247+
record = self.get_record(slot, self.prev_w_ptr, self.prev_serial + 1)
248+
else:
249+
record = self.get_record(slot, self.prev_w_ptr, None)
250+
if record != None:
251+
success = self.printer.print_record(record, self.cpu)
252+
if success:
253+
recp = ctypes.cast(record, ctypes.POINTER(DebugStreamRecord))
254+
self.prev_w_ptr = (self.prev_w_ptr + recp.contents.size
255+
) % self.buf_words
256+
self.prev_serial = recp.contents.serial
257+
else:
258+
logging.info(f"Parse failed on record {self.prev_serial + 1}")
259+
return success
260+
self.error_count = self.error_count + 1
261+
logging.info(f"Record decoding failed {self.error_count}")
262+
return False
263+
264+
def poll_buffer(self, slot):
265+
circ = CircularBufHdr.from_buffer_copy(
266+
slot, self.desc.offset)
267+
if self.prev_w_ptr == circ.w_ptr:
268+
return False
269+
success = True
270+
while self.prev_w_ptr != circ.w_ptr and success:
271+
success = self.get_next_record(slot)
272+
return True
273+
274+
def check_error_count(self):
275+
if self.error_count > 3:
276+
return True
277+
return False
278+
279+
class DebugStreamDecoder:
280+
"""
281+
Class for finding thread analyzer chuck and initializing CoreData objects.
282+
"""
283+
file_size = 4096 # ADSP debug slot size
284+
f = None
285+
slot = None
286+
descs = []
287+
circdec = []
288+
rec_printer = RecordPrinter()
289+
290+
def set_file(self, f):
291+
self.f = f
292+
293+
def update_slot(self):
294+
self.f.seek(0)
295+
self.slot = self.f.read(self.file_size)
296+
297+
def get_descriptors(self):
298+
if self.slot == None:
299+
return
300+
hdr = ctypes.cast(self.slot, ctypes.
301+
POINTER(DebugStreamSlotHdr))
302+
if hdr.contents.hdr.magic != DEBUG_STREAM_PAYLOAD_MAGIC:
303+
logging.warning("Debug Slot has bad magic 0x%08x" %
304+
hdr.contents.hdr.magic)
305+
return False
306+
num_sections = hdr.contents.num_sections
307+
if num_sections == len(self.descs):
308+
return True
309+
hsize = ctypes.sizeof(DebugStreamSlotHdr)
310+
dsize = ctypes.sizeof(DebugStreamSectionDescriptor)
311+
self.descs = (DebugStreamSectionDescriptor * num_sections
312+
).from_buffer_copy(self.slot, hsize)
313+
self.circdec = [CircularBufferDecoder(self.descs[i], i,
314+
self.rec_printer)
315+
for i in range(len(self.descs))]
316+
logging.info(f"Descriptors {hdr.contents.hdr.hdr_size} {hdr.contents.total_size} {hdr.contents.num_sections}")
317+
return True
318+
319+
def catch_up_all(self):
320+
if len(self.descs) == 0 or self.slot == None:
321+
return
322+
for i in range(len(self.descs)):
323+
self.circdec[i].catch_up(self.slot)
324+
325+
def poll(self):
326+
if len(self.descs) == 0 or self.slot == None:
327+
return
328+
sleep = True
329+
for i in range(len(self.descs)):
330+
if self.circdec[i].poll_buffer(self.slot):
331+
sleep = False
332+
return sleep
333+
334+
def check_slot(self):
335+
hdr = ctypes.cast(self.slot, ctypes.
336+
POINTER(DebugStreamSlotHdr))
337+
if hdr.contents.hdr.magic != DEBUG_STREAM_PAYLOAD_MAGIC:
338+
self.slot = None
339+
return False
340+
if hdr.contents.num_sections != len(self.descs):
341+
self.slot = None
342+
return False
343+
for i in range(len(self.descs)):
344+
if self.circdec[i].check_error_count():
345+
self.circdec[i] = CircularBufferDecoder(self.descs[i], i,
346+
self.rec_printer)
347+
return True
348+
349+
def reset(self):
350+
self.f = None
351+
self.slot = None
352+
353+
def main_f(args):
354+
"""
355+
Open debug stream slot file and pass it to decoder
356+
"""
357+
decoder = DebugStreamDecoder()
358+
prev_error = None
359+
while True:
360+
try:
361+
with open(args.debugstream_file, "rb") as f:
362+
decoder.set_file(f)
363+
decoder.update_slot()
364+
if not decoder.get_descriptors():
365+
break
366+
decoder.catch_up_all()
367+
while True:
368+
if decoder.poll():
369+
time.sleep(args.update_interval)
370+
decoder.update_slot()
371+
if not decoder.check_slot():
372+
break
373+
374+
except FileNotFoundError:
375+
print(f"File {args.debugstream_file} not found!")
376+
break
377+
except OSError as e:
378+
if str(e) != prev_error:
379+
print(f"Open {args.debugstream_file} failed '{e}'")
380+
prev_error = str(e)
381+
decoder.reset()
382+
time.sleep(args.update_interval)
383+
384+
def parse_params():
385+
""" Parses parameters
386+
"""
387+
parser = argparse.ArgumentParser(description=
388+
"SOF DebugStream thread info client. ")
389+
parser.add_argument('-t', '--update-interval', type=float,
390+
help='Telemetry2 window polling interval in seconds, default 1',
391+
default=0.01)
392+
parser.add_argument('-f', '--debugstream-file',
393+
help='File to read the DebugStream data from, default /sys/kernel/debug/sof/debug_stream',
394+
default="/sys/kernel/debug/sof/debug_stream")
395+
parsed_args = parser.parse_args()
396+
return parsed_args
397+
398+
args = parse_params()
399+
main_f(args)

0 commit comments

Comments
 (0)