1 # SPDX-License-Identifier: GPL-2.0
2 # Copyright 2022 Google LLC
3 # Written by Simon Glass <sjg@chromium.org>
9 import u_boot_utils as util
11 # This is needed for Azure, since the default '..' directory is not writeable
12 TMPDIR = '/tmp/test_trace'
14 # Decode a function-graph line
15 RE_LINE = re.compile(r'.*0\.\.\.\.\. \s*([0-9.]*): func.*[|](\s*)(\S.*)?([{};])$')
18 def collect_trace(cons):
19 """Build U-Boot and run it to collect a trace
22 cons (ConsoleBase): U-Boot console
26 str: Filename of the output trace file
27 int: Microseconds taken for initf_dm according to bootstage
29 cons.run_command('trace pause')
30 out = cons.run_command('trace stats')
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
39 # 748,268 calls not traced due to depth
40 # 1,230,758 max function calls
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}
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
54 out = cons.run_command('bootstage report')
59 dm_f_time = [line.split()[0] for line in out.replace(',', '').splitlines()
62 # Read out the trace data
65 out = cons.run_command(f'trace calls {addr:x} {size:x}')
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])
73 def check_function(cons, fname, proftool, map_fname, trace_dat):
74 """Check that the 'function' output works
77 cons (ConsoleBase): U-Boot console
78 fname (str): Filename of trace file
79 proftool (str): Filename of proftool
80 map_fname (str): Filename of System.map
81 trace_dat (str): Filename of output file
83 out = util.run_and_log(
84 cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
87 # Check that trace-cmd can read it
88 out = util.run_and_log(cons, ['trace-cmd', 'dump', trace_dat])
90 # Tracing meta data in file /tmp/test_trace/trace.dat:
95 # 4096 [Page size, bytes]
96 # [Header page, 205 bytes]
97 # [Header event, 205 bytes]
98 # [Ftrace format, 3 events]
99 # [Events format, 0 systems]
100 # [Kallsyms, 342244 bytes]
101 # [Trace printk, 0 bytes]
102 # [Saved command lines, 9 bytes]
103 # 1 [CPUs with tracing data]
105 # [Flyrecord tracing data]
107 # [local] global counter uptime perf mono mono_raw boot x86-tsc
108 assert '[Flyrecord tracing data]' in out
109 assert '4096 [Page size, bytes]' in out
110 kallsyms = [line.split() for line in out.splitlines() if 'Kallsyms' in line]
111 # [['[Kallsyms,', '342244', 'bytes]']]
112 val = int(kallsyms[0][1])
113 assert val > 50000 # Should be at least 50KB of symbols
115 # Check that the trace has something useful
116 cmd = f"trace-cmd report -l {trace_dat} |grep -E '(initf_|initr_)'"
117 out = util.run_and_log(cons, ['sh', '-c', cmd])
120 # u-boot-1 0..... 60.805596: function: initf_malloc
121 # u-boot-1 0..... 60.805597: function: initf_malloc
122 # u-boot-1 0..... 60.805601: function: initf_bootstage
123 # u-boot-1 0..... 60.805607: function: initf_bootstage
125 lines = [line.replace(':', '').split() for line in out.splitlines()]
126 vals = {items[4]: float(items[2]) for items in lines if len(items) == 5}
129 for timestamp in vals.values():
131 max_delta = max(max_delta, timestamp - base)
135 # Check for some expected functions
136 assert 'initf_malloc' in vals.keys()
137 assert 'initr_watchdog' in vals.keys()
138 assert 'initr_dm' in vals.keys()
140 # All the functions should be executed within five seconds at most
144 def check_funcgraph(cons, fname, proftool, map_fname, trace_dat):
145 """Check that the 'funcgraph' output works
148 cons (ConsoleBase): U-Boot console
149 fname (str): Filename of trace file
150 proftool (str): Filename of proftool
151 map_fname (str): Filename of System.map
152 trace_dat (str): Filename of output file
155 int: Time taken by the first part of the initf_dm() function, in us
158 # Generate the funcgraph format
159 out = util.run_and_log(
160 cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
161 'dump-ftrace', '-f', 'funcgraph'])
163 # Check that the trace has what we expect
164 cmd = f'trace-cmd report -l {trace_dat} |head -n 70'
165 out = util.run_and_log(cons, ['sh', '-c', cmd])
167 # First look for this:
168 # u-boot-1 0..... 282.101360: funcgraph_entry: 0.004 us | initf_malloc();
170 # u-boot-1 0..... 282.101369: funcgraph_entry: | initf_bootstage() {
171 # u-boot-1 0..... 282.101369: funcgraph_entry: | bootstage_init() {
172 # u-boot-1 0..... 282.101369: funcgraph_entry: | dlmalloc() {
174 # u-boot-1 0..... 282.101375: funcgraph_exit: 0.001 us | }
175 # Then look for this:
176 # u-boot-1 0..... 282.101375: funcgraph_exit: 0.006 us | }
177 # Then check for this:
178 # u-boot-1 0..... 282.101375: funcgraph_entry: 0.000 us | initcall_is_event();
180 expected_indent = None
185 # Look for initf_bootstage() entry and make sure we see the exit
186 # Collect the time for initf_dm()
187 for line in out.splitlines():
188 m = RE_LINE.match(line)
190 timestamp, indent, func, brace = m.groups()
194 elif func == 'initf_bootstage() ':
196 expected_indent = indent + ' '
197 elif found_start and indent == expected_indent and brace == '}':
200 # The next function after initf_bootstage() exits should be
201 # initcall_is_event()
202 assert upto == 'initcall_is_event()'
204 # Now look for initf_dm() and dm_timer_init() so we can check the bootstage
206 cmd = f"trace-cmd report -l {trace_dat} |grep -E '(initf_dm|dm_timer_init)'"
207 out = util.run_and_log(cons, ['sh', '-c', cmd])
209 start_timestamp = None
211 for line in out.splitlines():
212 m = RE_LINE.match(line)
214 timestamp, indent, func, brace = m.groups()
215 if func == 'initf_dm() ':
216 start_timestamp = timestamp
217 elif func == 'dm_timer_init() ':
218 end_timestamp = timestamp
220 assert start_timestamp and end_timestamp
222 # Convert the time to microseconds
223 return int((float(end_timestamp) - float(start_timestamp)) * 1000000)
226 def check_flamegraph(cons, fname, proftool, map_fname, trace_fg):
227 """Check that the 'flamegraph' output works
229 This spot checks a few call counts and estimates the time taken by the
233 cons (ConsoleBase): U-Boot console
234 fname (str): Filename of trace file
235 proftool (str): Filename of proftool
236 map_fname (str): Filename of System.map
237 trace_fg (str): Filename of output file
240 int: Approximate number of microseconds used by the initf_dm() function
243 # Generate the flamegraph format
244 out = util.run_and_log(
245 cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
248 # We expect dm_timer_init() to be called twice: once before relocation and
250 look1 = 'initf_dm;dm_timer_init 1'
251 look2 = 'board_init_r;initcall_run_list;initr_dm_devices;dm_timer_init 1'
253 with open(trace_fg, 'r') as fd:
256 if line == look1 or line == look2:
260 # Generate the timing graph
261 out = util.run_and_log(
262 cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
263 'dump-flamegraph', '-f', 'timing'])
265 # Add up all the time spend in initf_dm() and its children
267 with open(trace_fg, 'r') as fd:
270 if line.startswith('initf_dm'):
271 func, val = line.split()
278 @pytest.mark.boardspec('sandbox')
279 @pytest.mark.buildconfigspec('trace')
280 def test_trace(u_boot_console):
281 """Test we can build sandbox with trace, collect and process a trace"""
282 cons = u_boot_console
284 if not os.path.exists(TMPDIR):
286 proftool = os.path.join(cons.config.build_dir, 'tools', 'proftool')
287 map_fname = os.path.join(cons.config.build_dir, 'System.map')
288 trace_dat = os.path.join(TMPDIR, 'trace.dat')
289 trace_fg = os.path.join(TMPDIR, 'trace.fg')
291 fname, dm_f_time = collect_trace(cons)
293 check_function(cons, fname, proftool, map_fname, trace_dat)
294 trace_time = check_funcgraph(cons, fname, proftool, map_fname, trace_dat)
296 # Check that bootstage and funcgraph agree to within 10 microseconds
297 diff = abs(trace_time - dm_f_time)
298 print(f'trace_time {trace_time}, dm_f_time {dm_f_time}')
299 assert diff / dm_f_time < 0.01
301 fg_time = check_flamegraph(cons, fname, proftool, map_fname, trace_fg)
303 # Check that bootstage and flamegraph agree to within 30%
304 # This allows for CI being slow to run
305 diff = abs(fg_time - dm_f_time)
306 assert diff / dm_f_time < 0.3