|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +# SPDX-License-Identifier: BSD-3-Clause |
| 4 | +# Copyright(c) 2025 Intel Corporation. All rights reserved. |
| 5 | + |
| 6 | +'''The sof-ipc-timer collects module initialization and configuration |
| 7 | +timings from 'journalctl -k -o short-precise' output if SOF IPC debug |
| 8 | +is enabled. |
| 9 | +
|
| 10 | +''' |
| 11 | + |
| 12 | +import re |
| 13 | +import pathlib |
| 14 | +import argparse |
| 15 | +from datetime import datetime |
| 16 | + |
| 17 | +class Component: |
| 18 | + '''SOF audio component storage class''' |
| 19 | + ppln_id: str |
| 20 | + comp_id: int |
| 21 | + wname: str |
| 22 | + |
| 23 | + def __init__(self, ppln_id, comp_id, wname): |
| 24 | + self.ppln_id = ppln_id |
| 25 | + self.comp_id = comp_id |
| 26 | + self.wname = wname |
| 27 | + |
| 28 | + def __str__(self) -> str: |
| 29 | + return f'{self.ppln_id}-{self.comp_id:#08x}' |
| 30 | + |
| 31 | +def read_comp_data(f): |
| 32 | + '''Process the dmesg to get the component ID to component name mapping, |
| 33 | + they are acquired from the line that contains 'Create widget': |
| 34 | + [ 59.622645] snd_sof:sof_ipc4_widget_setup: sof-audio-pci-intel-mtl 0000:00:1f.3: |
| 35 | + Create widget host-copier.0.capture (pipe 3) - ID 4, instance 3, core 0" |
| 36 | +
|
| 37 | + By design in the kernel, pipeline ID is the instance ID of pipeline |
| 38 | + widget, so it is acquired from the line that contains 'Create pipeline': |
| 39 | + [ 59.622134] snd_sof:sof_ipc4_widget_setup: sof-audio-pci-intel-mtl 0000:00:1f.3: |
| 40 | + Create pipeline pipeline.3 (pipe 3) - instance 3, core 0 |
| 41 | +
|
| 42 | + In practice, sof-test only capture kernel message and firmware trace generated during a test |
| 43 | + case run. Mostly in manual tests, if the kernel message file contains multiple firmware runs |
| 44 | + with overlapping information, the last one wins. |
| 45 | + ''' |
| 46 | + comp_data = {} |
| 47 | + ppln_id = None |
| 48 | + for line in f: |
| 49 | + if match_obj := re.search(r"Create (widget|pipeline)", line): |
| 50 | + span_end_pos = match_obj.span()[1] |
| 51 | + line_split = line[span_end_pos + 1:].split() |
| 52 | + widget_name = line_split[0] |
| 53 | + # In the linux kernel, IDA is used to allocated pipeline widget instance ID, |
| 54 | + # this ID later is used for pipeline creation, thus becomes pipeline ID in the |
| 55 | + # firmware. Note that ppln_id variable will be assigned properly at pipeline widget |
| 56 | + # creation, because it is always the first one to be created before all other |
| 57 | + # widgets in the same pipeline and pipelines are created sequentially. |
| 58 | + if widget_name.startswith('pipeline'): |
| 59 | + # remove ending comma(,) with [:-1] |
| 60 | + ppln_id = int(line_split[5][:-1]) |
| 61 | + continue |
| 62 | + # remove ending comma(,) with [:-1] |
| 63 | + module_instance_id = int(line_split[7][:-1]) |
| 64 | + # remove ending comma(,) with [:-1] |
| 65 | + widget_id = int(line_split[5][:-1]) |
| 66 | + # final module id are composed with high16(module instance id) + low16(module id) |
| 67 | + widget_id |= module_instance_id << 16 |
| 68 | + comp_data[widget_id] = Component(ppln_id, widget_id, widget_name) |
| 69 | + return comp_data |
| 70 | + |
| 71 | +def read_ipc_data(f, comp_data, args): |
| 72 | + '''Pick ipc tx MOD_INIT_INSTANCE and MOD_LARGE_CONFIG_SET lines |
| 73 | + from log, take time stamp from the first message and the 'done' |
| 74 | + message, and store the difference of to either init_time or |
| 75 | + conf_time in the component table. |
| 76 | +
|
| 77 | + ''' |
| 78 | + mod_id = None |
| 79 | + start = None |
| 80 | + for line in f: |
| 81 | + if match_obj := re.search(r" ipc tx ( |reply|done )", line): |
| 82 | + line_split = line.split() |
| 83 | + dt_object = datetime.strptime(line_split[2], "%H:%M:%S.%f") |
| 84 | + secs = dt_object.second + dt_object.minute * 60 + dt_object.hour * 3600 |
| 85 | + usecs = secs * 1000000 + dt_object.microsecond |
| 86 | + span_end_pos = match_obj.span()[1] |
| 87 | + # msg_type shoule be either " ", "reply", or "done " |
| 88 | + msg_type = line[match_obj.span()[0] + 8 : match_obj.span()[1]] |
| 89 | + msg_part = line[span_end_pos:].split() |
| 90 | + primary = int(msg_part[1].split('|')[0], 16) |
| 91 | + extension = int(msg_part[1].split('|')[1].rstrip(":"), 16) |
| 92 | + if msg_part[2] == "MOD_INIT_INSTANCE" or msg_part[2] == "MOD_LARGE_CONFIG_SET": |
| 93 | + if msg_type == " ": |
| 94 | + mod_id = primary & 0xFFFFFF |
| 95 | + if mod_id == 0: |
| 96 | + mod_id = None |
| 97 | + continue |
| 98 | + start = usecs |
| 99 | + if mod_id == None: |
| 100 | + continue |
| 101 | + comp = comp_data[mod_id] |
| 102 | + if msg_type == "reply" and args.reply_timings: |
| 103 | + if msg_part[2] == "MOD_INIT_INSTANCE": |
| 104 | + print("%s:\tinit reply\t%d us" % (comp.wname, usecs - start)) |
| 105 | + elif msg_part[2] == "MOD_LARGE_CONFIG_SET": |
| 106 | + print("%s:\tconf reply\t%d us" % (comp.wname, usecs - start)) |
| 107 | + start = usecs |
| 108 | + elif msg_type == "done ": |
| 109 | + module_id = "" |
| 110 | + pipeline_id = "" |
| 111 | + if args.module_id: |
| 112 | + module_id = "\tmodule id: " + format(mod_id, '#08x') |
| 113 | + if args.pipeline_id: |
| 114 | + pipeline_id = "\tpipeline id: " + str(comp.ppln_id) |
| 115 | + if msg_part[2] == "MOD_INIT_INSTANCE": |
| 116 | + print("%s:\tinit done\t%d us%s%s" % |
| 117 | + (comp.wname, usecs - start, module_id, pipeline_id)) |
| 118 | + elif msg_part[2] == "MOD_LARGE_CONFIG_SET": |
| 119 | + print("%s:\tconf done\t%d us%s%s" % |
| 120 | + (comp.wname, usecs - start, module_id, pipeline_id)) |
| 121 | + mod_id = None |
| 122 | + start = None |
| 123 | + end = None |
| 124 | + |
| 125 | +def parse_args(): |
| 126 | + '''Parse command line arguments''' |
| 127 | + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, |
| 128 | + description=__doc__) |
| 129 | + parser.add_argument('filename') |
| 130 | + parser.add_argument('-r', '--reply-timings', action="store_true", default=False, |
| 131 | + help='Show time to reply message, "done" time is from "reply"') |
| 132 | + parser.add_argument('-m', '--module-id', action="store_true", default=False, |
| 133 | + help='Show module id') |
| 134 | + parser.add_argument('-p', '--pipeline-id', action="store_true", default=False, |
| 135 | + help='Show pipeline id') |
| 136 | + |
| 137 | + return parser.parse_args() |
| 138 | + |
| 139 | +def main(): |
| 140 | + args = parse_args() |
| 141 | + |
| 142 | + with open(args.filename, 'r', encoding='utf8') as file: |
| 143 | + comp_data = read_comp_data(file) |
| 144 | + |
| 145 | + file.seek(0) |
| 146 | + read_ipc_data(file, comp_data, args) |
| 147 | + |
| 148 | +if __name__ == "__main__": |
| 149 | + main() |
0 commit comments