Simon Glass | c2f0190 | 2023-01-15 14:16:00 -0700 | [diff] [blame] | 1 | # SPDX-License-Identifier: GPL-2.0 |
| 2 | # Copyright 2022 Google LLC |
| 3 | # Written by Simon Glass <sjg@chromium.org> |
| 4 | |
| 5 | import os |
| 6 | import pytest |
| 7 | import re |
| 8 | |
| 9 | import u_boot_utils as util |
| 10 | |
| 11 | # This is needed for Azure, since the default '..' directory is not writeable |
| 12 | TMPDIR = '/tmp/test_trace' |
| 13 | |
| 14 | # Decode a function-graph line |
| 15 | RE_LINE = re.compile(r'.*\[000\]\s*([0-9.]*): func.*[|](\s*)(\S.*)?([{};])$') |
| 16 | |
| 17 | |
| 18 | def 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 |
| 64 | size = 0x01000000 |
| 65 | 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 | |
| 73 | def check_function(cons, fname, proftool, map_fname, trace_dat): |
| 74 | """Check that the 'function' output works |
| 75 | |
| 76 | Args: |
| 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 |
| 82 | """ |
| 83 | out = util.run_and_log( |
| 84 | cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname, |
| 85 | 'dump-ftrace']) |
| 86 | |
| 87 | # Check that trace-cmd can read it |
| 88 | out = util.run_and_log(cons, ['trace-cmd', 'dump', trace_dat]) |
| 89 | |
| 90 | # Tracing meta data in file /tmp/test_trace/trace.dat: |
| 91 | # [Initial format] |
| 92 | # 6 [Version] |
| 93 | # 0 [Little endian] |
| 94 | # 4 [Bytes in a long] |
| 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] |
| 104 | # [6 options] |
| 105 | # [Flyrecord tracing data] |
| 106 | # [Tracing clock] |
| 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 |
| 114 | |
| 115 | # Check that the trace has something useful |
| 116 | cmd = f"trace-cmd report {trace_dat} |grep -E '(initf_|initr_)'" |
| 117 | out = util.run_and_log(cons, ['sh', '-c', cmd]) |
| 118 | |
| 119 | # Format: |
| 120 | # unknown option 14 |
| 121 | # u-boot-1 [000] 60.805596: function: initf_malloc |
| 122 | # u-boot-1 [000] 60.805597: function: initf_malloc |
| 123 | # u-boot-1 [000] 60.805601: function: initf_bootstage |
| 124 | # u-boot-1 [000] 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} |
| 127 | base = None |
| 128 | max_delta = 0 |
| 129 | for timestamp in vals.values(): |
| 130 | if base: |
| 131 | max_delta = max(max_delta, timestamp - base) |
| 132 | else: |
| 133 | base = timestamp |
| 134 | |
| 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() |
| 139 | |
| 140 | # All the functions should be executed within five seconds at most |
| 141 | assert max_delta < 5 |
| 142 | |
| 143 | |
| 144 | def check_funcgraph(cons, fname, proftool, map_fname, trace_dat): |
| 145 | """Check that the 'funcgraph' output works |
| 146 | |
| 147 | Args: |
| 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 |
| 153 | |
| 154 | Returns: |
| 155 | int: Time taken by the first part of the initf_dm() function, in us |
| 156 | """ |
| 157 | |
| 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']) |
| 162 | |
| 163 | # Check that the trace has what we expect |
| 164 | cmd = f'trace-cmd report {trace_dat} |head -n 70' |
| 165 | out = util.run_and_log(cons, ['sh', '-c', cmd]) |
| 166 | |
| 167 | # First look for this: |
| 168 | # u-boot-1 [000] 282.101360: funcgraph_entry: 0.004 us | initf_malloc(); |
| 169 | # ... |
| 170 | # u-boot-1 [000] 282.101369: funcgraph_entry: | initf_bootstage() { |
| 171 | # u-boot-1 [000] 282.101369: funcgraph_entry: | bootstage_init() { |
| 172 | # u-boot-1 [000] 282.101369: funcgraph_entry: | dlmalloc() { |
| 173 | # ... |
| 174 | # u-boot-1 [000] 282.101375: funcgraph_exit: 0.001 us | } |
| 175 | # Then look for this: |
| 176 | # u-boot-1 [000] 282.101375: funcgraph_exit: 0.006 us | } |
| 177 | # Then check for this: |
| 178 | # u-boot-1 [000] 282.101375: funcgraph_entry: 0.000 us | event_init(); |
| 179 | |
| 180 | expected_indent = None |
| 181 | found_start = False |
| 182 | found_end = False |
| 183 | upto = None |
| 184 | |
| 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) |
| 189 | if m: |
| 190 | timestamp, indent, func, brace = m.groups() |
| 191 | if found_end: |
| 192 | upto = func |
| 193 | break |
| 194 | elif func == 'initf_bootstage() ': |
| 195 | found_start = True |
| 196 | expected_indent = indent + ' ' |
| 197 | elif found_start and indent == expected_indent and brace == '}': |
| 198 | found_end = True |
| 199 | |
| 200 | # The next function after initf_bootstage() exits should be event_init() |
| 201 | assert upto == 'event_init()' |
| 202 | |
| 203 | # Now look for initf_dm() and dm_timer_init() so we can check the bootstage |
| 204 | # time |
| 205 | cmd = f"trace-cmd report {trace_dat} |grep -E '(initf_dm|dm_timer_init)'" |
| 206 | out = util.run_and_log(cons, ['sh', '-c', cmd]) |
| 207 | |
| 208 | start_timestamp = None |
| 209 | end_timestamp = None |
| 210 | for line in out.splitlines(): |
| 211 | m = RE_LINE.match(line) |
| 212 | if m: |
| 213 | timestamp, indent, func, brace = m.groups() |
| 214 | if func == 'initf_dm() ': |
| 215 | start_timestamp = timestamp |
| 216 | elif func == 'dm_timer_init() ': |
| 217 | end_timestamp = timestamp |
| 218 | break |
| 219 | assert start_timestamp and end_timestamp |
| 220 | |
| 221 | # Convert the time to microseconds |
| 222 | return int((float(end_timestamp) - float(start_timestamp)) * 1000000) |
| 223 | |
| 224 | |
| 225 | def check_flamegraph(cons, fname, proftool, map_fname, trace_fg): |
| 226 | """Check that the 'flamegraph' output works |
| 227 | |
| 228 | This spot checks a few call counts and estimates the time taken by the |
| 229 | initf_dm() function |
| 230 | |
| 231 | Args: |
| 232 | cons (ConsoleBase): U-Boot console |
| 233 | fname (str): Filename of trace file |
| 234 | proftool (str): Filename of proftool |
| 235 | map_fname (str): Filename of System.map |
| 236 | trace_fg (str): Filename of output file |
| 237 | |
| 238 | Returns: |
| 239 | int: Approximate number of microseconds used by the initf_dm() function |
| 240 | """ |
| 241 | |
| 242 | # Generate the flamegraph format |
| 243 | out = util.run_and_log( |
| 244 | cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname, |
| 245 | 'dump-flamegraph']) |
| 246 | |
| 247 | # We expect dm_timer_init() to be called twice: once before relocation and |
| 248 | # once after |
| 249 | look1 = 'initf_dm;dm_timer_init 1' |
| 250 | look2 = 'board_init_r;initr_dm_devices;dm_timer_init 1' |
| 251 | found = 0 |
| 252 | with open(trace_fg, 'r') as fd: |
| 253 | for line in fd: |
| 254 | line = line.strip() |
| 255 | if line == look1 or line == look2: |
| 256 | found += 1 |
| 257 | assert found == 2 |
| 258 | |
| 259 | # Generate the timing graph |
| 260 | out = util.run_and_log( |
| 261 | cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname, |
| 262 | 'dump-flamegraph', '-f', 'timing']) |
| 263 | |
| 264 | # Add up all the time spend in initf_dm() and its children |
| 265 | total = 0 |
| 266 | with open(trace_fg, 'r') as fd: |
| 267 | for line in fd: |
| 268 | line = line.strip() |
| 269 | if line.startswith('initf_dm'): |
| 270 | func, val = line.split() |
| 271 | count = int(val) |
| 272 | total += count |
| 273 | return total |
| 274 | |
| 275 | |
| 276 | @pytest.mark.slow |
| 277 | @pytest.mark.boardspec('sandbox') |
| 278 | @pytest.mark.buildconfigspec('trace') |
| 279 | def test_trace(u_boot_console): |
| 280 | """Test we can build sandbox with trace, collect and process a trace""" |
| 281 | cons = u_boot_console |
| 282 | |
| 283 | if not os.path.exists(TMPDIR): |
| 284 | os.mkdir(TMPDIR) |
| 285 | proftool = os.path.join(cons.config.build_dir, 'tools', 'proftool') |
| 286 | map_fname = os.path.join(cons.config.build_dir, 'System.map') |
| 287 | trace_dat = os.path.join(TMPDIR, 'trace.dat') |
| 288 | trace_fg = os.path.join(TMPDIR, 'trace.fg') |
| 289 | |
| 290 | fname, dm_f_time = collect_trace(cons) |
| 291 | |
| 292 | check_function(cons, fname, proftool, map_fname, trace_dat) |
| 293 | trace_time = check_funcgraph(cons, fname, proftool, map_fname, trace_dat) |
| 294 | |
| 295 | # Check that bootstage and funcgraph agree to within 10 microseconds |
| 296 | diff = abs(trace_time - dm_f_time) |
| 297 | print(f'trace_time {trace_time}, dm_f_time {dm_f_time}') |
| 298 | assert diff / dm_f_time < 0.01 |
| 299 | |
| 300 | fg_time = check_flamegraph(cons, fname, proftool, map_fname, trace_fg) |
| 301 | |
Simon Glass | 420107b | 2023-02-17 15:45:16 -0700 | [diff] [blame] | 302 | # Check that bootstage and flamegraph agree to within 30% |
| 303 | # This allows for CI being slow to run |
Simon Glass | c2f0190 | 2023-01-15 14:16:00 -0700 | [diff] [blame] | 304 | diff = abs(fg_time - dm_f_time) |
Simon Glass | 420107b | 2023-02-17 15:45:16 -0700 | [diff] [blame] | 305 | assert diff / dm_f_time < 0.3 |