|
| 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