Skip to content

Commit c33652a

Browse files
author
Jyri Sarha
committed
tools: sof-ipc-timer.py: Timings of module init and config IPC loggings
The sof-ipc-timer collects module initialization and configuration timings from 'journalctl -k -o short-precise' output if SOF IPC debug is enabled. Signed-off-by: Jyri Sarha <jyri.sarha@linux.intel.com>
1 parent 77dd64c commit c33652a

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

tools/sof-ipc-timer.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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

Comments
 (0)