blob: 44239da5280821f86dc8a9e016da671055691e38 [file] [log] [blame]
Simon Glassc2f01902023-01-15 14:16:00 -07001# SPDX-License-Identifier: GPL-2.0
2# Copyright 2022 Google LLC
3# Written by Simon Glass <sjg@chromium.org>
4
5import os
6import pytest
7import re
8
9import u_boot_utils as util
10
11# This is needed for Azure, since the default '..' directory is not writeable
12TMPDIR = '/tmp/test_trace'
13
14# Decode a function-graph line
Simon Glass267ca3d2024-06-23 14:30:20 -060015RE_LINE = re.compile(r'.*0\.\.\.\.\.? \s*([0-9.]*): func.*[|](\s*)(\S.*)?([{};])$')
Simon Glassc2f01902023-01-15 14:16:00 -070016
17
18def collect_trace(cons):
19 """Build U-Boot and run it to collect a trace
20
21 Args:
22 cons (ConsoleBase): U-Boot console
23
24 Returns:
25 tuple:
26 str: Filename of the output trace file
27 int: Microseconds taken for initf_dm according to bootstage
28 """
29 cons.run_command('trace pause')
30 out = cons.run_command('trace stats')
31
32 # The output is something like this:
33 # 251,003 function sites
34 # 1,160,283 function calls
35 # 0 untracked function calls
36 # 1,230,758 traced function calls (341538 dropped due to overflow)
37 # 33 maximum observed call depth
38 # 15 call depth limit
39 # 748,268 calls not traced due to depth
40 # 1,230,758 max function calls
41
42 # Get a dict of values from the output
43 lines = [line.split(maxsplit=1) for line in out.splitlines() if line]
44 vals = {key: val.replace(',', '') for val, key in lines}
45
46 assert int(vals['function sites']) > 100000
47 assert int(vals['function calls']) > 200000
48 assert int(vals['untracked function calls']) == 0
49 assert int(vals['maximum observed call depth']) > 30
50 assert (vals['call depth limit'] ==
51 cons.config.buildconfig.get('config_trace_call_depth_limit'))
52 assert int(vals['calls not traced due to depth']) > 100000
53
54 out = cons.run_command('bootstage report')
55 # Accumulated time:
56 # 19,104 dm_r
57 # 23,078 of_live
58 # 46,280 dm_f
59 dm_f_time = [line.split()[0] for line in out.replace(',', '').splitlines()
60 if 'dm_f' in line]
61
62 # Read out the trace data
63 addr = 0x02000000
Sughosh Ganu0c3783e2023-08-22 23:10:02 +053064 size = 0x02000000
Simon Glassc2f01902023-01-15 14:16:00 -070065 out = cons.run_command(f'trace calls {addr:x} {size:x}')
66 print(out)
67 fname = os.path.join(TMPDIR, 'trace')
68 out = cons.run_command(
69 'host save hostfs - %x %s ${profoffset}' % (addr, fname))
70 return fname, int(dm_f_time[0])
71
72
Jerome Forissier3b3d7322024-12-13 13:45:37 +010073def wipe_and_collect_trace(cons):
74 """Pause and wipe traces, return the number of calls (should be zero)
75
76 Args:
77 cons (ConsoleBase): U-Boot console
78
79 Returns:
80 int: the number of traced function calls reported by 'trace stats'
81 """
82 cons.run_command('trace pause')
83 cons.run_command('trace wipe')
84 out = cons.run_command('trace stats')
85
86 # The output is something like this:
87 # 117,221 function sites
88 # 0 function calls
89 # 0 untracked function calls
90 # 0 traced function calls
91
92 # Get a dict of values from the output
93 lines = [line.split(maxsplit=1) for line in out.splitlines() if line]
94 vals = {key: val.replace(',', '') for val, key in lines}
95
96 return int(vals['traced function calls'])
97
98
Simon Glassc2f01902023-01-15 14:16:00 -070099def check_function(cons, fname, proftool, map_fname, trace_dat):
100 """Check that the 'function' output works
101
102 Args:
103 cons (ConsoleBase): U-Boot console
104 fname (str): Filename of trace file
105 proftool (str): Filename of proftool
106 map_fname (str): Filename of System.map
107 trace_dat (str): Filename of output file
108 """
109 out = util.run_and_log(
110 cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
111 'dump-ftrace'])
112
113 # Check that trace-cmd can read it
114 out = util.run_and_log(cons, ['trace-cmd', 'dump', trace_dat])
115
116 # Tracing meta data in file /tmp/test_trace/trace.dat:
117 # [Initial format]
118 # 6 [Version]
119 # 0 [Little endian]
120 # 4 [Bytes in a long]
121 # 4096 [Page size, bytes]
122 # [Header page, 205 bytes]
123 # [Header event, 205 bytes]
124 # [Ftrace format, 3 events]
125 # [Events format, 0 systems]
126 # [Kallsyms, 342244 bytes]
127 # [Trace printk, 0 bytes]
128 # [Saved command lines, 9 bytes]
129 # 1 [CPUs with tracing data]
130 # [6 options]
131 # [Flyrecord tracing data]
132 # [Tracing clock]
133 # [local] global counter uptime perf mono mono_raw boot x86-tsc
134 assert '[Flyrecord tracing data]' in out
135 assert '4096 [Page size, bytes]' in out
136 kallsyms = [line.split() for line in out.splitlines() if 'Kallsyms' in line]
137 # [['[Kallsyms,', '342244', 'bytes]']]
138 val = int(kallsyms[0][1])
139 assert val > 50000 # Should be at least 50KB of symbols
140
141 # Check that the trace has something useful
Tom Rini62dde542024-01-17 17:12:36 -0500142 cmd = f"trace-cmd report -l {trace_dat} |grep -E '(initf_|initr_)'"
Simon Glassc2f01902023-01-15 14:16:00 -0700143 out = util.run_and_log(cons, ['sh', '-c', cmd])
144
145 # Format:
Tom Rini62dde542024-01-17 17:12:36 -0500146 # u-boot-1 0..... 60.805596: function: initf_malloc
147 # u-boot-1 0..... 60.805597: function: initf_malloc
148 # u-boot-1 0..... 60.805601: function: initf_bootstage
149 # u-boot-1 0..... 60.805607: function: initf_bootstage
150
Simon Glassc2f01902023-01-15 14:16:00 -0700151 lines = [line.replace(':', '').split() for line in out.splitlines()]
152 vals = {items[4]: float(items[2]) for items in lines if len(items) == 5}
153 base = None
154 max_delta = 0
155 for timestamp in vals.values():
156 if base:
157 max_delta = max(max_delta, timestamp - base)
158 else:
159 base = timestamp
160
161 # Check for some expected functions
162 assert 'initf_malloc' in vals.keys()
163 assert 'initr_watchdog' in vals.keys()
164 assert 'initr_dm' in vals.keys()
165
166 # All the functions should be executed within five seconds at most
167 assert max_delta < 5
168
169
170def check_funcgraph(cons, fname, proftool, map_fname, trace_dat):
171 """Check that the 'funcgraph' output works
172
173 Args:
174 cons (ConsoleBase): U-Boot console
175 fname (str): Filename of trace file
176 proftool (str): Filename of proftool
177 map_fname (str): Filename of System.map
178 trace_dat (str): Filename of output file
179
180 Returns:
181 int: Time taken by the first part of the initf_dm() function, in us
182 """
183
184 # Generate the funcgraph format
185 out = util.run_and_log(
186 cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
187 'dump-ftrace', '-f', 'funcgraph'])
188
189 # Check that the trace has what we expect
Tom Rini62dde542024-01-17 17:12:36 -0500190 cmd = f'trace-cmd report -l {trace_dat} |head -n 70'
Simon Glassc2f01902023-01-15 14:16:00 -0700191 out = util.run_and_log(cons, ['sh', '-c', cmd])
192
193 # First look for this:
Tom Rini62dde542024-01-17 17:12:36 -0500194 # u-boot-1 0..... 282.101360: funcgraph_entry: 0.004 us | initf_malloc();
Simon Glassc2f01902023-01-15 14:16:00 -0700195 # ...
Tom Rini62dde542024-01-17 17:12:36 -0500196 # u-boot-1 0..... 282.101369: funcgraph_entry: | initf_bootstage() {
197 # u-boot-1 0..... 282.101369: funcgraph_entry: | bootstage_init() {
198 # u-boot-1 0..... 282.101369: funcgraph_entry: | dlmalloc() {
Simon Glassc2f01902023-01-15 14:16:00 -0700199 # ...
Tom Rini62dde542024-01-17 17:12:36 -0500200 # u-boot-1 0..... 282.101375: funcgraph_exit: 0.001 us | }
Simon Glassc2f01902023-01-15 14:16:00 -0700201 # Then look for this:
Tom Rini62dde542024-01-17 17:12:36 -0500202 # u-boot-1 0..... 282.101375: funcgraph_exit: 0.006 us | }
Simon Glassc2f01902023-01-15 14:16:00 -0700203 # Then check for this:
Simon Glass4a45d5d2024-06-23 14:30:22 -0600204 # u-boot-1 0..... 282.101375: funcgraph_entry: 0.000 us | calc_reloc_ofs();
Simon Glassc2f01902023-01-15 14:16:00 -0700205
206 expected_indent = None
207 found_start = False
208 found_end = False
209 upto = None
210
211 # Look for initf_bootstage() entry and make sure we see the exit
212 # Collect the time for initf_dm()
213 for line in out.splitlines():
214 m = RE_LINE.match(line)
215 if m:
216 timestamp, indent, func, brace = m.groups()
217 if found_end:
218 upto = func
219 break
220 elif func == 'initf_bootstage() ':
221 found_start = True
222 expected_indent = indent + ' '
223 elif found_start and indent == expected_indent and brace == '}':
224 found_end = True
225
Simon Glass6b42d382023-08-21 21:16:54 -0600226 # The next function after initf_bootstage() exits should be
227 # initcall_is_event()
Simon Glass4a45d5d2024-06-23 14:30:22 -0600228 assert upto == 'calc_reloc_ofs()'
Simon Glassc2f01902023-01-15 14:16:00 -0700229
230 # Now look for initf_dm() and dm_timer_init() so we can check the bootstage
231 # time
Tom Rini62dde542024-01-17 17:12:36 -0500232 cmd = f"trace-cmd report -l {trace_dat} |grep -E '(initf_dm|dm_timer_init)'"
Simon Glassc2f01902023-01-15 14:16:00 -0700233 out = util.run_and_log(cons, ['sh', '-c', cmd])
234
235 start_timestamp = None
236 end_timestamp = None
237 for line in out.splitlines():
238 m = RE_LINE.match(line)
239 if m:
240 timestamp, indent, func, brace = m.groups()
241 if func == 'initf_dm() ':
242 start_timestamp = timestamp
243 elif func == 'dm_timer_init() ':
244 end_timestamp = timestamp
245 break
246 assert start_timestamp and end_timestamp
247
248 # Convert the time to microseconds
249 return int((float(end_timestamp) - float(start_timestamp)) * 1000000)
250
251
252def check_flamegraph(cons, fname, proftool, map_fname, trace_fg):
253 """Check that the 'flamegraph' output works
254
255 This spot checks a few call counts and estimates the time taken by the
256 initf_dm() function
257
258 Args:
259 cons (ConsoleBase): U-Boot console
260 fname (str): Filename of trace file
261 proftool (str): Filename of proftool
262 map_fname (str): Filename of System.map
263 trace_fg (str): Filename of output file
264
265 Returns:
266 int: Approximate number of microseconds used by the initf_dm() function
267 """
268
269 # Generate the flamegraph format
270 out = util.run_and_log(
271 cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
272 'dump-flamegraph'])
273
274 # We expect dm_timer_init() to be called twice: once before relocation and
275 # once after
276 look1 = 'initf_dm;dm_timer_init 1'
Simon Glass6b42d382023-08-21 21:16:54 -0600277 look2 = 'board_init_r;initcall_run_list;initr_dm_devices;dm_timer_init 1'
Simon Glassc2f01902023-01-15 14:16:00 -0700278 found = 0
279 with open(trace_fg, 'r') as fd:
280 for line in fd:
281 line = line.strip()
282 if line == look1 or line == look2:
283 found += 1
284 assert found == 2
285
286 # Generate the timing graph
287 out = util.run_and_log(
288 cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
289 'dump-flamegraph', '-f', 'timing'])
290
291 # Add up all the time spend in initf_dm() and its children
292 total = 0
293 with open(trace_fg, 'r') as fd:
294 for line in fd:
295 line = line.strip()
296 if line.startswith('initf_dm'):
297 func, val = line.split()
298 count = int(val)
299 total += count
300 return total
301
Simon Glass6b42d382023-08-21 21:16:54 -0600302check_flamegraph
Simon Glassc2f01902023-01-15 14:16:00 -0700303@pytest.mark.slow
304@pytest.mark.boardspec('sandbox')
305@pytest.mark.buildconfigspec('trace')
306def test_trace(u_boot_console):
307 """Test we can build sandbox with trace, collect and process a trace"""
308 cons = u_boot_console
309
310 if not os.path.exists(TMPDIR):
311 os.mkdir(TMPDIR)
312 proftool = os.path.join(cons.config.build_dir, 'tools', 'proftool')
313 map_fname = os.path.join(cons.config.build_dir, 'System.map')
314 trace_dat = os.path.join(TMPDIR, 'trace.dat')
315 trace_fg = os.path.join(TMPDIR, 'trace.fg')
316
317 fname, dm_f_time = collect_trace(cons)
318
319 check_function(cons, fname, proftool, map_fname, trace_dat)
320 trace_time = check_funcgraph(cons, fname, proftool, map_fname, trace_dat)
321
322 # Check that bootstage and funcgraph agree to within 10 microseconds
323 diff = abs(trace_time - dm_f_time)
324 print(f'trace_time {trace_time}, dm_f_time {dm_f_time}')
325 assert diff / dm_f_time < 0.01
326
327 fg_time = check_flamegraph(cons, fname, proftool, map_fname, trace_fg)
328
Simon Glass420107b2023-02-17 15:45:16 -0700329 # Check that bootstage and flamegraph agree to within 30%
330 # This allows for CI being slow to run
Simon Glassc2f01902023-01-15 14:16:00 -0700331 diff = abs(fg_time - dm_f_time)
Simon Glass420107b2023-02-17 15:45:16 -0700332 assert diff / dm_f_time < 0.3
Jerome Forissier3b3d7322024-12-13 13:45:37 +0100333
334 # Check that the trace buffer can be wiped
335 numcalls = wipe_and_collect_trace(cons)
336 assert numcalls == 0