test_trace.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. # SPDX-License-Identifier: GPL-2.0
  2. # Copyright 2022 Google LLC
  3. # Written by Simon Glass <sjg@chromium.org>
  4. import os
  5. import pytest
  6. import re
  7. import u_boot_utils as util
  8. # This is needed for Azure, since the default '..' directory is not writeable
  9. TMPDIR = '/tmp/test_trace'
  10. # Decode a function-graph line
  11. RE_LINE = re.compile(r'.*\[000\]\s*([0-9.]*): func.*[|](\s*)(\S.*)?([{};])$')
  12. def collect_trace(cons):
  13. """Build U-Boot and run it to collect a trace
  14. Args:
  15. cons (ConsoleBase): U-Boot console
  16. Returns:
  17. tuple:
  18. str: Filename of the output trace file
  19. int: Microseconds taken for initf_dm according to bootstage
  20. """
  21. cons.run_command('trace pause')
  22. out = cons.run_command('trace stats')
  23. # The output is something like this:
  24. # 251,003 function sites
  25. # 1,160,283 function calls
  26. # 0 untracked function calls
  27. # 1,230,758 traced function calls (341538 dropped due to overflow)
  28. # 33 maximum observed call depth
  29. # 15 call depth limit
  30. # 748,268 calls not traced due to depth
  31. # 1,230,758 max function calls
  32. # Get a dict of values from the output
  33. lines = [line.split(maxsplit=1) for line in out.splitlines() if line]
  34. vals = {key: val.replace(',', '') for val, key in lines}
  35. assert int(vals['function sites']) > 100000
  36. assert int(vals['function calls']) > 200000
  37. assert int(vals['untracked function calls']) == 0
  38. assert int(vals['maximum observed call depth']) > 30
  39. assert (vals['call depth limit'] ==
  40. cons.config.buildconfig.get('config_trace_call_depth_limit'))
  41. assert int(vals['calls not traced due to depth']) > 100000
  42. out = cons.run_command('bootstage report')
  43. # Accumulated time:
  44. # 19,104 dm_r
  45. # 23,078 of_live
  46. # 46,280 dm_f
  47. dm_f_time = [line.split()[0] for line in out.replace(',', '').splitlines()
  48. if 'dm_f' in line]
  49. # Read out the trace data
  50. addr = 0x02000000
  51. size = 0x01000000
  52. out = cons.run_command(f'trace calls {addr:x} {size:x}')
  53. print(out)
  54. fname = os.path.join(TMPDIR, 'trace')
  55. out = cons.run_command(
  56. 'host save hostfs - %x %s ${profoffset}' % (addr, fname))
  57. return fname, int(dm_f_time[0])
  58. def check_function(cons, fname, proftool, map_fname, trace_dat):
  59. """Check that the 'function' output works
  60. Args:
  61. cons (ConsoleBase): U-Boot console
  62. fname (str): Filename of trace file
  63. proftool (str): Filename of proftool
  64. map_fname (str): Filename of System.map
  65. trace_dat (str): Filename of output file
  66. """
  67. out = util.run_and_log(
  68. cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
  69. 'dump-ftrace'])
  70. # Check that trace-cmd can read it
  71. out = util.run_and_log(cons, ['trace-cmd', 'dump', trace_dat])
  72. # Tracing meta data in file /tmp/test_trace/trace.dat:
  73. # [Initial format]
  74. # 6 [Version]
  75. # 0 [Little endian]
  76. # 4 [Bytes in a long]
  77. # 4096 [Page size, bytes]
  78. # [Header page, 205 bytes]
  79. # [Header event, 205 bytes]
  80. # [Ftrace format, 3 events]
  81. # [Events format, 0 systems]
  82. # [Kallsyms, 342244 bytes]
  83. # [Trace printk, 0 bytes]
  84. # [Saved command lines, 9 bytes]
  85. # 1 [CPUs with tracing data]
  86. # [6 options]
  87. # [Flyrecord tracing data]
  88. # [Tracing clock]
  89. # [local] global counter uptime perf mono mono_raw boot x86-tsc
  90. assert '[Flyrecord tracing data]' in out
  91. assert '4096 [Page size, bytes]' in out
  92. kallsyms = [line.split() for line in out.splitlines() if 'Kallsyms' in line]
  93. # [['[Kallsyms,', '342244', 'bytes]']]
  94. val = int(kallsyms[0][1])
  95. assert val > 50000 # Should be at least 50KB of symbols
  96. # Check that the trace has something useful
  97. cmd = f"trace-cmd report {trace_dat} |grep -E '(initf_|initr_)'"
  98. out = util.run_and_log(cons, ['sh', '-c', cmd])
  99. # Format:
  100. # unknown option 14
  101. # u-boot-1 [000] 60.805596: function: initf_malloc
  102. # u-boot-1 [000] 60.805597: function: initf_malloc
  103. # u-boot-1 [000] 60.805601: function: initf_bootstage
  104. # u-boot-1 [000] 60.805607: function: initf_bootstage
  105. lines = [line.replace(':', '').split() for line in out.splitlines()]
  106. vals = {items[4]: float(items[2]) for items in lines if len(items) == 5}
  107. base = None
  108. max_delta = 0
  109. for timestamp in vals.values():
  110. if base:
  111. max_delta = max(max_delta, timestamp - base)
  112. else:
  113. base = timestamp
  114. # Check for some expected functions
  115. assert 'initf_malloc' in vals.keys()
  116. assert 'initr_watchdog' in vals.keys()
  117. assert 'initr_dm' in vals.keys()
  118. # All the functions should be executed within five seconds at most
  119. assert max_delta < 5
  120. def check_funcgraph(cons, fname, proftool, map_fname, trace_dat):
  121. """Check that the 'funcgraph' output works
  122. Args:
  123. cons (ConsoleBase): U-Boot console
  124. fname (str): Filename of trace file
  125. proftool (str): Filename of proftool
  126. map_fname (str): Filename of System.map
  127. trace_dat (str): Filename of output file
  128. Returns:
  129. int: Time taken by the first part of the initf_dm() function, in us
  130. """
  131. # Generate the funcgraph format
  132. out = util.run_and_log(
  133. cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
  134. 'dump-ftrace', '-f', 'funcgraph'])
  135. # Check that the trace has what we expect
  136. cmd = f'trace-cmd report {trace_dat} |head -n 70'
  137. out = util.run_and_log(cons, ['sh', '-c', cmd])
  138. # First look for this:
  139. # u-boot-1 [000] 282.101360: funcgraph_entry: 0.004 us | initf_malloc();
  140. # ...
  141. # u-boot-1 [000] 282.101369: funcgraph_entry: | initf_bootstage() {
  142. # u-boot-1 [000] 282.101369: funcgraph_entry: | bootstage_init() {
  143. # u-boot-1 [000] 282.101369: funcgraph_entry: | dlmalloc() {
  144. # ...
  145. # u-boot-1 [000] 282.101375: funcgraph_exit: 0.001 us | }
  146. # Then look for this:
  147. # u-boot-1 [000] 282.101375: funcgraph_exit: 0.006 us | }
  148. # Then check for this:
  149. # u-boot-1 [000] 282.101375: funcgraph_entry: 0.000 us | event_init();
  150. expected_indent = None
  151. found_start = False
  152. found_end = False
  153. upto = None
  154. # Look for initf_bootstage() entry and make sure we see the exit
  155. # Collect the time for initf_dm()
  156. for line in out.splitlines():
  157. m = RE_LINE.match(line)
  158. if m:
  159. timestamp, indent, func, brace = m.groups()
  160. if found_end:
  161. upto = func
  162. break
  163. elif func == 'initf_bootstage() ':
  164. found_start = True
  165. expected_indent = indent + ' '
  166. elif found_start and indent == expected_indent and brace == '}':
  167. found_end = True
  168. # The next function after initf_bootstage() exits should be event_init()
  169. assert upto == 'event_init()'
  170. # Now look for initf_dm() and dm_timer_init() so we can check the bootstage
  171. # time
  172. cmd = f"trace-cmd report {trace_dat} |grep -E '(initf_dm|dm_timer_init)'"
  173. out = util.run_and_log(cons, ['sh', '-c', cmd])
  174. start_timestamp = None
  175. end_timestamp = None
  176. for line in out.splitlines():
  177. m = RE_LINE.match(line)
  178. if m:
  179. timestamp, indent, func, brace = m.groups()
  180. if func == 'initf_dm() ':
  181. start_timestamp = timestamp
  182. elif func == 'dm_timer_init() ':
  183. end_timestamp = timestamp
  184. break
  185. assert start_timestamp and end_timestamp
  186. # Convert the time to microseconds
  187. return int((float(end_timestamp) - float(start_timestamp)) * 1000000)
  188. def check_flamegraph(cons, fname, proftool, map_fname, trace_fg):
  189. """Check that the 'flamegraph' output works
  190. This spot checks a few call counts and estimates the time taken by the
  191. initf_dm() function
  192. Args:
  193. cons (ConsoleBase): U-Boot console
  194. fname (str): Filename of trace file
  195. proftool (str): Filename of proftool
  196. map_fname (str): Filename of System.map
  197. trace_fg (str): Filename of output file
  198. Returns:
  199. int: Approximate number of microseconds used by the initf_dm() function
  200. """
  201. # Generate the flamegraph format
  202. out = util.run_and_log(
  203. cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
  204. 'dump-flamegraph'])
  205. # We expect dm_timer_init() to be called twice: once before relocation and
  206. # once after
  207. look1 = 'initf_dm;dm_timer_init 1'
  208. look2 = 'board_init_r;initr_dm_devices;dm_timer_init 1'
  209. found = 0
  210. with open(trace_fg, 'r') as fd:
  211. for line in fd:
  212. line = line.strip()
  213. if line == look1 or line == look2:
  214. found += 1
  215. assert found == 2
  216. # Generate the timing graph
  217. out = util.run_and_log(
  218. cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
  219. 'dump-flamegraph', '-f', 'timing'])
  220. # Add up all the time spend in initf_dm() and its children
  221. total = 0
  222. with open(trace_fg, 'r') as fd:
  223. for line in fd:
  224. line = line.strip()
  225. if line.startswith('initf_dm'):
  226. func, val = line.split()
  227. count = int(val)
  228. total += count
  229. return total
  230. @pytest.mark.slow
  231. @pytest.mark.boardspec('sandbox')
  232. @pytest.mark.buildconfigspec('trace')
  233. def test_trace(u_boot_console):
  234. """Test we can build sandbox with trace, collect and process a trace"""
  235. cons = u_boot_console
  236. if not os.path.exists(TMPDIR):
  237. os.mkdir(TMPDIR)
  238. proftool = os.path.join(cons.config.build_dir, 'tools', 'proftool')
  239. map_fname = os.path.join(cons.config.build_dir, 'System.map')
  240. trace_dat = os.path.join(TMPDIR, 'trace.dat')
  241. trace_fg = os.path.join(TMPDIR, 'trace.fg')
  242. fname, dm_f_time = collect_trace(cons)
  243. check_function(cons, fname, proftool, map_fname, trace_dat)
  244. trace_time = check_funcgraph(cons, fname, proftool, map_fname, trace_dat)
  245. # Check that bootstage and funcgraph agree to within 10 microseconds
  246. diff = abs(trace_time - dm_f_time)
  247. print(f'trace_time {trace_time}, dm_f_time {dm_f_time}')
  248. assert diff / dm_f_time < 0.01
  249. fg_time = check_flamegraph(cons, fname, proftool, map_fname, trace_fg)
  250. # Check that bootstage and flamegraph agree to within 30%
  251. # This allows for CI being slow to run
  252. diff = abs(fg_time - dm_f_time)
  253. assert diff / dm_f_time < 0.3