Skip to content

Commit 54df69f

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, and set pipeline state timings from 'journalctl -k -o short-precise' output if SOF IPC debug is enabled. Different kind of outputs can be generated with different flags. Adding just -s gives a summary like this: host-copier.0.playback init min 196 us max 456 us average 322 us of 100 gain.1.1 init min 192 us max 552 us average 327 us of 100 gain.1.1 conf min 197 us max 494 us average 325 us of 100 mixin.1.1 init min 193 us max 8219 us average 406 us of 100 mixout.2.1 init min 197 us max 573 us average 327 us of 100 gain.2.1 init min 197 us max 484 us average 324 us of 100 gain.2.1 conf min 193 us max 418 us average 322 us of 100 smart_amp.2.1 init min 194 us max 526 us average 326 us of 100 smart_amp.2.1 conf min 197 us max 1826 us average 393 us of 100 dai-copier.SSP.NoCodec-0.playback init min 202 us max 2135 us average 479 us of 100 pipeline.1: host-copier.0.playback, gain.1.1, mixin.1.1, pipeline.1 3 min 200 us max 2592 us average 553 us of 200 pipeline.1 4 min 228 us max 3202 us average 673 us of 100 pipeline.1 2 min 208 us max 1867 us average 565 us of 100 pipeline.2: mixout.2.1, gain.2.1, smart_amp.2.1, dai-copier.SSP.NoCodec-0.playback, pipeline.2 3 min 199 us max 2667 us average 863 us of 200 pipeline.2 4 min 205 us max 8656 us average 794 us of 100 Signed-off-by: Jyri Sarha <jyri.sarha@linux.intel.com> sof-ipc-timer.py: Fix summary only functionality
1 parent 77dd64c commit 54df69f

File tree

1 file changed

+318
-0
lines changed

1 file changed

+318
-0
lines changed

tools/sof-ipc-timer.py

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
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+
Without any flags there is no output. If you want it all, just put
11+
everything on the command line: -t -i -c -m -p -s
12+
13+
'''
14+
15+
import re
16+
import sys
17+
import argparse
18+
from datetime import datetime
19+
20+
class Component:
21+
'''SOF audio component storage class'''
22+
pipe_id: int
23+
comp_id: int
24+
wname: str
25+
init_times: list
26+
conf_times: list
27+
28+
def __init__(self, pipe_id, comp_id, wname):
29+
self.pipe_id = pipe_id
30+
self.comp_id = comp_id
31+
self.wname = wname
32+
self.init_times = []
33+
self.conf_times = []
34+
35+
def __str__(self) -> str:
36+
return f'{self.pipe_id}-{self.comp_id:#08x}'
37+
38+
class Pipeline:
39+
'''SOF audio pipeline storage class'''
40+
pipe_id: int
41+
comps: list
42+
state_times: dict
43+
44+
def __init__(self, pipe_id):
45+
self.pipe_id = pipe_id
46+
self.comps = []
47+
self.state_times = {}
48+
49+
def __str__(self) -> str:
50+
return f'pipeline.{self.pipe_id + 1}'
51+
52+
def add_state_timing(self, state, usecs):
53+
if self.state_times.get(state) is None:
54+
self.state_times[state] = []
55+
self.state_times[state].append(usecs)
56+
57+
class LogLineParser:
58+
def __init__(self, args, comp_data, pipe_data):
59+
self.args = args
60+
self.comp_data = comp_data
61+
self.pipe_data = pipe_data
62+
63+
class PipelineParser(LogLineParser):
64+
'''Parse line of form
65+
66+
Feb 25 23:17:00.598919 lnlm-rvp-sdw kernel: snd_sof:sof_ipc4_widget_setup: sof-audio-pci-intel-lnl 0000:00:1f.3: Create pipeline pipeline.1 (pipe 1) - instance 0, core 0
67+
68+
'pipe_id' from ' instance 0,'
69+
'''
70+
def __init__(self, args, comp_data, pipe_data):
71+
super().__init__(args, comp_data, pipe_data)
72+
73+
def parse_line(self, line):
74+
if match_obj := re.search(r" Create pipeline ", line):
75+
match_end_pos = match_obj.span()[1]
76+
line_split = line[match_end_pos + 1:].split()
77+
pipe_id = int(line_split[5].rstrip(","))
78+
if self.pipe_data.get(pipe_id) is None:
79+
self.pipe_data[pipe_id] = Pipeline(pipe_id)
80+
return True
81+
return False
82+
83+
class WidgetParser(LogLineParser):
84+
'''Parse line of form:
85+
86+
Feb 25 23:17:00.599838 lnlm-rvp-sdw kernel: snd_sof:sof_ipc4_widget_setup: sof-audio-pci-intel-lnl 0000:00:1f.3: Create widget host-copier.0.playback (pipe 1) - ID 4, instance 0, core 0
87+
88+
'widget_name' from 'host-copier.0.playback'
89+
'pipi_id' integer form "(pipe 1)"
90+
'widget_id' MSB integer from "instance 0," and LSB from "ID 4,"
91+
'''
92+
def __init__(self, args, comp_data, pipe_data):
93+
super().__init__(args, comp_data, pipe_data)
94+
95+
def parse_line(self, line):
96+
if match_obj := re.search(r" Create widget ", line):
97+
match_end_pos = match_obj.span()[1]
98+
line_split = line[match_end_pos:].split()
99+
widget_name = line_split[0]
100+
# pipeline instance id is one smaller than the pipeline number
101+
pipe_id = int(line_split[2].rstrip(")")) - 1
102+
module_instance_id = int(line_split[7].rstrip(","))
103+
widget_id = int(line_split[5].rstrip(","))
104+
# final module id are composed with high16(module instance id) + low16(module id)
105+
widget_id |= module_instance_id << 16
106+
# do not overwire data we have colledted, only add new items to dictionary
107+
if self.comp_data.get(widget_id) is None:
108+
self.comp_data[widget_id] = Component(pipe_id, widget_id, widget_name)
109+
if not self.pipe_data.get(pipe_id) is None:
110+
self.pipe_data[pipe_id].comps.append(self.comp_data[widget_id])
111+
return True
112+
return False
113+
114+
class IpcMsgParser(LogLineParser):
115+
'''Parse three consequtive lines of form:
116+
117+
Feb 25 23:17:00.599946 lnlm-rvp-sdw kernel: snd_sof:sof_ipc4_log_header: sof-audio-pci-intel-lnl 0000:00:1f.3: ipc tx : 0x40000004|0x15: MOD_INIT_INSTANCE [data size: 84]
118+
Feb 25 23:17:00.600048 lnlm-rvp-sdw kernel: snd_sof:sof_ipc4_log_header: sof-audio-pci-intel-lnl 0000:00:1f.3: ipc tx reply: 0x60000000|0x15: MOD_INIT_INSTANCE
119+
Feb 25 23:17:00.600185 lnlm-rvp-sdw kernel: snd_sof:sof_ipc4_log_header: sof-audio-pci-intel-lnl 0000:00:1f.3: ipc tx done : 0x40000004|0x15: MOD_INIT_INSTANCE [data size: 84]
120+
121+
'usecs' from '23:17:00.599946' (please do not run your tests over midnight)-
122+
'msg_type' from 5 letters following " ipc tx ", either " ", "reply", or "done "
123+
'msg_str' from '0x40000004|0x15'
124+
'msg_name' from 'MOD_INIT_INSTANCE' ('MOD_LARGE_CONFIG_SET' or 'GLB_SET_PIPELINE_STATE')
125+
'primary' integer from '0x40000004'
126+
'extension' integer from '0x15'
127+
'''
128+
comp_id: int
129+
pipe_id: int
130+
start: int
131+
state: int
132+
133+
def __init__(self, args, comp_data, pipe_data):
134+
super().__init__(args, comp_data, pipe_data)
135+
self.reset()
136+
137+
def reset(self):
138+
self.comp_id = -1
139+
self.pipe_id = -1
140+
self.start = -1
141+
self.state = -1
142+
143+
def parse_line(self, line):
144+
if match_obj := re.search(r" ipc tx ( |reply|done )", line):
145+
match_start_pos = match_obj.span()[0]
146+
match_end_pos = match_obj.span()[1]
147+
line_split = line.split()
148+
dt_object = datetime.strptime(line_split[2], "%H:%M:%S.%f")
149+
secs = dt_object.second + dt_object.minute * 60 + dt_object.hour * 3600
150+
usecs = secs * 1000000 + dt_object.microsecond
151+
msg_type = line[match_start_pos + 8 : match_end_pos]
152+
msg_part = line[match_end_pos:].split()
153+
msg_str = msg_part[1]
154+
msg_name = msg_part[2]
155+
primary = int(msg_str.split('|')[0], 16)
156+
extension = int(msg_str.split('|')[1].rstrip(":"), 16)
157+
if msg_name == "MOD_INIT_INSTANCE" or msg_part[2] == "MOD_LARGE_CONFIG_SET":
158+
self.parse_mod_msg(msg_name, msg_str, msg_type, usecs, primary)
159+
elif msg_name == "GLB_SET_PIPELINE_STATE":
160+
self.parse_glb_set_msg(msg_type, msg_str, usecs, primary)
161+
return True
162+
return False
163+
164+
def parse_mod_1st(self, usecs, primary):
165+
self.comp_id = primary & 0xFFFFFF
166+
if self.comp_id == 0:
167+
self.comp_id = None
168+
self.start = usecs
169+
170+
def parse_mod_reply(self, comp, msg_name, usecs):
171+
if msg_name == "MOD_INIT_INSTANCE" and self.args.init_messages:
172+
print("%s:\tinit reply\t%d us" % (comp.wname, usecs - self.start))
173+
elif msg_name == "MOD_LARGE_CONFIG_SET" and self.args.config_messages:
174+
print("%s:\tconf reply\t%d us" % (comp.wname, usecs - self.start))
175+
176+
def parse_mod_done(self, comp, msg_name, msg_str, usecs):
177+
message = ""
178+
pipeline_id = ""
179+
if self.args.message:
180+
message = "\t" + msg_str
181+
if self.args.pipeline_id:
182+
pipeline_id = "\tpipeline id: " + str(comp.pipe_id)
183+
if msg_name == "MOD_INIT_INSTANCE":
184+
if self.args.init_messages:
185+
print("%s:\tinit done\t%d us%s%s" %
186+
(comp.wname, usecs - self.start, message, pipeline_id))
187+
comp.init_times.append(usecs - self.start)
188+
elif msg_name == "MOD_LARGE_CONFIG_SET":
189+
if self.args.config_messages:
190+
print("%s:\tconf done\t%d us%s%s" %
191+
(comp.wname, usecs - self.start, message, pipeline_id))
192+
comp.conf_times.append(usecs - self.start)
193+
self.reset()
194+
195+
def parse_mod_msg(self, msg_name, msg_str, msg_type, usecs, primary):
196+
if msg_type == " ":
197+
self.parse_mod_1st(usecs, primary)
198+
if self.comp_id == None:
199+
return
200+
comp = self.comp_data[self.comp_id]
201+
if msg_type == "reply" and self.args.reply_timings:
202+
self.parse_mod_reply(comp, msg_name, usecs)
203+
elif msg_type == "done ":
204+
self.parse_mod_done(comp, msg_name, msg_str, usecs)
205+
206+
def parse_glb_set_1st(self, usecs, primary):
207+
self.pipe_id = (primary & 0x00FF0000) >> 16
208+
self.state = primary & 0xFFFF
209+
self.start = usecs
210+
211+
def parse_glb_set_reply(self, usecs):
212+
print("pipeline id: %d\tstate %d reply\t %d us" %
213+
(self.pipe_id, self.state, usecs - self.start))
214+
self.start = usecs
215+
216+
def parse_glb_set_done(self, msg_str, usecs):
217+
message = ""
218+
if self.args.message:
219+
message = "\t" + msg_str
220+
if self.args.trigger_nessages:
221+
print("pipeline id: %d\tstate %d done\t%d us%s" %
222+
(self.pipe_id, self.state, usecs - self.start, message))
223+
self.pipe_data[self.pipe_id].add_state_timing(self.state, usecs - self.start)
224+
self.reset()
225+
226+
def parse_glb_set_msg(self, msg_type, msg_str, usecs, primary):
227+
if msg_type == " ":
228+
self.parse_glb_set_1st(usecs, primary)
229+
elif msg_type == "reply" and self.args.reply_timings:
230+
self.parse_glb_set_reply(usecs)
231+
elif msg_type == "done ":
232+
self.parse_glb_set_done(msg_str, usecs)
233+
234+
class SOFLinuxLogParser:
235+
def __init__(self, args):
236+
self.args = args
237+
self.comp_data = {}
238+
self.pipe_data = {}
239+
self.pipe_parser = PipelineParser(args, self.comp_data, self.pipe_data)
240+
self.widget_parser = WidgetParser(args, self.comp_data, self.pipe_data)
241+
self.ipc_msg_parser = IpcMsgParser(args, self.comp_data, self.pipe_data)
242+
243+
def read_log_data(self, file):
244+
for line in file:
245+
if self.pipe_parser.parse_line(line):
246+
continue
247+
if self.widget_parser.parse_line(line):
248+
continue
249+
if self.ipc_msg_parser.parse_line(line):
250+
continue
251+
252+
def print_min_max_avg(self, prefix, times):
253+
if len(times) == 0:
254+
return
255+
minval = 1000000
256+
maxval = 0
257+
valsum = 0
258+
for val in times:
259+
if val < minval:
260+
minval = val
261+
if val > maxval:
262+
maxval = val
263+
valsum = valsum + val
264+
print("%s\tmin %d us\tmax %d us\taverage %d us of %d" %
265+
(prefix, minval, maxval, valsum / len(times), len(times)))
266+
267+
def summary(self):
268+
if not self.args.summary:
269+
return
270+
for comp_id in self.comp_data:
271+
mod = self.comp_data[comp_id]
272+
self.print_min_max_avg(mod.wname + " init", mod.init_times)
273+
self.print_min_max_avg(mod.wname + " conf", mod.conf_times)
274+
for pipe_id in self.pipe_data:
275+
pipe = self.pipe_data[pipe_id]
276+
print("%s:" % pipe, end=" ")
277+
for comp in pipe.comps:
278+
print("%s" % comp.wname, end=", ")
279+
print()
280+
for state in pipe.state_times:
281+
state_times_list = pipe.state_times[state]
282+
self.print_min_max_avg(str(pipe) + " " + str(state), state_times_list)
283+
284+
def parse_args():
285+
'''Parse command line arguments'''
286+
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
287+
description=__doc__)
288+
parser.add_argument('filename', nargs="?", help="Optional log file, stdin if not defined")
289+
parser.add_argument('-t', '--trigger-nessages', action="store_true", default=False,
290+
help='Show trigger message handling times')
291+
parser.add_argument('-i', '--init-messages', action="store_true", default=False,
292+
help='Show init message handling times')
293+
parser.add_argument('-c', '--config-messages', action="store_true", default=False,
294+
help='Show large config set message handling times')
295+
parser.add_argument('-r', '--reply-timings', action="store_true", default=False,
296+
help='Show time to reply message, "done" time is from "reply"')
297+
parser.add_argument('-m', '--message', action="store_true", default=False,
298+
help='Show 1st message primary and extension parts')
299+
parser.add_argument('-p', '--pipeline-id', action="store_true", default=False,
300+
help='Show pipeline id')
301+
parser.add_argument('-s', '--summary', action="store_true", default=False,
302+
help='Show average, max, and min latencies of message handling')
303+
return parser.parse_args()
304+
305+
def main():
306+
args = parse_args()
307+
log_parser = SOFLinuxLogParser(args)
308+
309+
if args.filename is None:
310+
log_parser.read_log_data(sys.stdin)
311+
else:
312+
with open(args.filename, 'r', encoding='utf8') as file:
313+
log_parser.read_log_data(file)
314+
315+
log_parser.summary()
316+
317+
if __name__ == "__main__":
318+
main()

0 commit comments

Comments
 (0)