summaryrefslogtreecommitdiff
path: root/test/py/tests/test_trace.py
blob: ac3e95925e95fc2d4282958c183e2b1d3d6feee4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# SPDX-License-Identifier: GPL-2.0
# Copyright 2022 Google LLC
# Written by Simon Glass <sjg@chromium.org>

import os
import pytest
import re

import u_boot_utils as util

# This is needed for Azure, since the default '..' directory is not writeable
TMPDIR = '/tmp/test_trace'

# Decode a function-graph line
RE_LINE = re.compile(r'.*\[000\]\s*([0-9.]*): func.*[|](\s*)(\S.*)?([{};])$')


def collect_trace(cons):
    """Build U-Boot and run it to collect a trace

    Args:
        cons (ConsoleBase): U-Boot console

    Returns:
        tuple:
            str: Filename of the output trace file
            int: Microseconds taken for initf_dm according to bootstage
    """
    cons.run_command('trace pause')
    out = cons.run_command('trace stats')

    # The output is something like this:
    #    251,003 function sites
    #  1,160,283 function calls
    #          0 untracked function calls
    #  1,230,758 traced function calls (341538 dropped due to overflow)
    #         33 maximum observed call depth
    #         15 call depth limit
    #    748,268 calls not traced due to depth
    #  1,230,758 max function calls

    # Get a dict of values from the output
    lines = [line.split(maxsplit=1) for line in out.splitlines() if line]
    vals = {key: val.replace(',', '') for val, key in lines}

    assert int(vals['function sites']) > 100000
    assert int(vals['function calls']) > 200000
    assert int(vals['untracked function calls']) == 0
    assert int(vals['maximum observed call depth']) > 30
    assert (vals['call depth limit'] ==
            cons.config.buildconfig.get('config_trace_call_depth_limit'))
    assert int(vals['calls not traced due to depth']) > 100000

    out = cons.run_command('bootstage report')
    # Accumulated time:
    #           19,104  dm_r
    #           23,078  of_live
    #           46,280  dm_f
    dm_f_time = [line.split()[0] for line in out.replace(',', '').splitlines()
                 if 'dm_f' in line]

    # Read out the trace data
    addr = 0x02000000
    size = 0x01000000
    out = cons.run_command(f'trace calls {addr:x} {size:x}')
    print(out)
    fname = os.path.join(TMPDIR, 'trace')
    out = cons.run_command(
        'host save hostfs - %x %s ${profoffset}' % (addr, fname))
    return fname, int(dm_f_time[0])


def check_function(cons, fname, proftool, map_fname, trace_dat):
    """Check that the 'function' output works

    Args:
        cons (ConsoleBase): U-Boot console
        fname (str): Filename of trace file
        proftool (str): Filename of proftool
        map_fname (str): Filename of System.map
        trace_dat (str): Filename of output file
    """
    out = util.run_and_log(
        cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
               'dump-ftrace'])

    # Check that trace-cmd can read it
    out = util.run_and_log(cons, ['trace-cmd', 'dump', trace_dat])

    # Tracing meta data in file /tmp/test_trace/trace.dat:
    #    [Initial format]
    #            6        [Version]
    #            0        [Little endian]
    #            4        [Bytes in a long]
    #            4096        [Page size, bytes]
    #    [Header page, 205 bytes]
    #    [Header event, 205 bytes]
    #    [Ftrace format, 3 events]
    #    [Events format, 0 systems]
    #    [Kallsyms, 342244 bytes]
    #    [Trace printk, 0 bytes]
    #    [Saved command lines, 9 bytes]
    #    1 [CPUs with tracing data]
    #    [6 options]
    #    [Flyrecord tracing data]
    #    [Tracing clock]
    #            [local] global counter uptime perf mono mono_raw boot x86-tsc
    assert '[Flyrecord tracing data]' in out
    assert '4096	[Page size, bytes]' in out
    kallsyms = [line.split() for line in out.splitlines() if 'Kallsyms' in line]
    # [['[Kallsyms,', '342244', 'bytes]']]
    val = int(kallsyms[0][1])
    assert val > 50000  # Should be at least 50KB of symbols

    # Check that the trace has something useful
    cmd = f"trace-cmd report {trace_dat} |grep -E '(initf_|initr_)'"
    out = util.run_and_log(cons, ['sh', '-c', cmd])

    # Format:
    # unknown option 14
    #      u-boot-1     [000]    60.805596: function:             initf_malloc
    #      u-boot-1     [000]    60.805597: function:             initf_malloc
    #      u-boot-1     [000]    60.805601: function:             initf_bootstage
    #      u-boot-1     [000]    60.805607: function:             initf_bootstage
    lines = [line.replace(':', '').split() for line in out.splitlines()]
    vals = {items[4]: float(items[2]) for items in lines if len(items) == 5}
    base = None
    max_delta = 0
    for timestamp in vals.values():
        if base:
            max_delta = max(max_delta, timestamp - base)
        else:
            base = timestamp

    # Check for some expected functions
    assert 'initf_malloc' in vals.keys()
    assert 'initr_watchdog' in vals.keys()
    assert 'initr_dm' in vals.keys()

    # All the functions should be executed within five seconds at most
    assert max_delta < 5


def check_funcgraph(cons, fname, proftool, map_fname, trace_dat):
    """Check that the 'funcgraph' output works

    Args:
        cons (ConsoleBase): U-Boot console
        fname (str): Filename of trace file
        proftool (str): Filename of proftool
        map_fname (str): Filename of System.map
        trace_dat (str): Filename of output file

    Returns:
        int: Time taken by the first part of the initf_dm() function, in us
    """

    # Generate the funcgraph format
    out = util.run_and_log(
        cons, [proftool, '-t', fname, '-o', trace_dat, '-m', map_fname,
               'dump-ftrace', '-f', 'funcgraph'])

    # Check that the trace has what we expect
    cmd = f'trace-cmd report {trace_dat} |head -n 70'
    out = util.run_and_log(cons, ['sh', '-c', cmd])

    # First look for this:
    #  u-boot-1     [000]   282.101360: funcgraph_entry:        0.004 us   |    initf_malloc();
    # ...
    #  u-boot-1     [000]   282.101369: funcgraph_entry:                   |    initf_bootstage() {
    #  u-boot-1     [000]   282.101369: funcgraph_entry:                   |      bootstage_init() {
    #  u-boot-1     [000]   282.101369: funcgraph_entry:                   |        dlmalloc() {
    # ...
    #  u-boot-1     [000]   282.101375: funcgraph_exit:         0.001 us   |        }
    # Then look for this:
    #  u-boot-1     [000]   282.101375: funcgraph_exit:         0.006 us   |      }
    # Then check for this:
    #  u-boot-1     [000]   282.101375: funcgraph_entry:        0.000 us   |    event_init();

    expected_indent = None
    found_start = False
    found_end = False
    upto = None

    # Look for initf_bootstage() entry and make sure we see the exit
    # Collect the time for initf_dm()
    for line in out.splitlines():
        m = RE_LINE.match(line)
        if m:
            timestamp, indent, func, brace = m.groups()
            if found_end:
                upto = func
                break
            elif func == 'initf_bootstage() ':
                found_start = True
                expected_indent = indent + '  '
            elif found_start and indent == expected_indent and brace == '}':
                found_end = True

    # The next function after initf_bootstage() exits should be event_init()
    assert upto == 'event_init()'

    # Now look for initf_dm() and dm_timer_init() so we can check the bootstage
    # time
    cmd = f"trace-cmd report {trace_dat} |grep -E '(initf_dm|dm_timer_init)'"
    out = util.run_and_log(cons, ['sh', '-c', cmd])

    start_timestamp = None
    end_timestamp = None
    for line in out.splitlines():
        m = RE_LINE.match(line)
        if m:
            timestamp, indent, func, brace = m.groups()
            if func == 'initf_dm() ':
                start_timestamp = timestamp
            elif func == 'dm_timer_init() ':
                end_timestamp = timestamp
                break
    assert start_timestamp and end_timestamp

    # Convert the time to microseconds
    return int((float(end_timestamp) - float(start_timestamp)) * 1000000)


def check_flamegraph(cons, fname, proftool, map_fname, trace_fg):
    """Check that the 'flamegraph' output works

    This spot checks a few call counts and estimates the time taken by the
    initf_dm() function

    Args:
        cons (ConsoleBase): U-Boot console
        fname (str): Filename of trace file
        proftool (str): Filename of proftool
        map_fname (str): Filename of System.map
        trace_fg (str): Filename of output file

    Returns:
        int: Approximate number of microseconds used by the initf_dm() function
    """

    # Generate the flamegraph format
    out = util.run_and_log(
        cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
               'dump-flamegraph'])

    # We expect dm_timer_init() to be called twice: once before relocation and
    # once after
    look1 = 'initf_dm;dm_timer_init 1'
    look2 = 'board_init_r;initr_dm_devices;dm_timer_init 1'
    found = 0
    with open(trace_fg, 'r') as fd:
        for line in fd:
            line = line.strip()
            if line == look1 or line == look2:
                found += 1
    assert found == 2

    # Generate the timing graph
    out = util.run_and_log(
        cons, [proftool, '-t', fname, '-o', trace_fg, '-m', map_fname,
               'dump-flamegraph', '-f', 'timing'])

    # Add up all the time spend in initf_dm() and its children
    total = 0
    with open(trace_fg, 'r') as fd:
        for line in fd:
            line = line.strip()
            if line.startswith('initf_dm'):
                func, val = line.split()
                count = int(val)
                total += count
    return total


@pytest.mark.slow
@pytest.mark.boardspec('sandbox')
@pytest.mark.buildconfigspec('trace')
def test_trace(u_boot_console):
    """Test we can build sandbox with trace, collect and process a trace"""
    cons = u_boot_console

    if not os.path.exists(TMPDIR):
        os.mkdir(TMPDIR)
    proftool = os.path.join(cons.config.build_dir, 'tools', 'proftool')
    map_fname = os.path.join(cons.config.build_dir, 'System.map')
    trace_dat = os.path.join(TMPDIR, 'trace.dat')
    trace_fg = os.path.join(TMPDIR, 'trace.fg')

    fname, dm_f_time = collect_trace(cons)

    check_function(cons, fname, proftool, map_fname, trace_dat)
    trace_time = check_funcgraph(cons, fname, proftool, map_fname, trace_dat)

    # Check that bootstage and funcgraph agree to within 10 microseconds
    diff = abs(trace_time - dm_f_time)
    print(f'trace_time {trace_time}, dm_f_time {dm_f_time}')
    assert diff / dm_f_time < 0.01

    fg_time = check_flamegraph(cons, fname, proftool, map_fname, trace_fg)

    # Check that bootstage and flamegraph agree to within 30%
    # This allows for CI being slow to run
    diff = abs(fg_time - dm_f_time)
    assert diff / dm_f_time < 0.3