xref: /aosp_15_r20/external/autotest/client/bin/fps_meter.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li#!/usr/bin/python3
2*9c5db199SXin Li# Copyright 2018 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li"""Checks kernel tracing events to get the FPS of a CrOS device.
7*9c5db199SXin Li
8*9c5db199SXin LiThis script requires root privilege to work properly. It may interfere
9*9c5db199SXin LiChrome tracing because both use ftrace.
10*9c5db199SXin Li
11*9c5db199SXin LiLimitation:
12*9c5db199SXin LiIt doesn't support multi-screen.
13*9c5db199SXin LiIt assumes 60 HZ screen refresh rate.
14*9c5db199SXin Li
15*9c5db199SXin LiThis script can be used both as a stand alone script or a library.
16*9c5db199SXin Li
17*9c5db199SXin LiSample output (when used as a stand alone script):
18*9c5db199SXin Li    # ./fps_meter.py
19*9c5db199SXin Li    trace method: workq
20*9c5db199SXin Li    [111111111111111111111111111111111111111111111111111111111111] FPS: 60
21*9c5db199SXin Li    [111111111111111111111111111111111111111111111111111111111111] FPS: 60
22*9c5db199SXin Li    [11111111111111111111111111111111111111111111111 111111111111] FPS: 59
23*9c5db199SXin Li    [111111111111111111111111111111111111111111111111111111111111] FPS: 60
24*9c5db199SXin Li    [11111111111111 11111111111111111111 11111111 111111111111111] FPS: 57
25*9c5db199SXin Li    [111111111111111111111111111111111    11111111111111111111111] FPS: 56
26*9c5db199SXin Li    [111   111111111111111111111111111111111111111111111111111111] FPS: 57
27*9c5db199SXin Li     ^
28*9c5db199SXin Li         1 : Frame update count detected in this 1/60 sec interval.
29*9c5db199SXin Li
30*9c5db199SXin LiSample Usage (when used as a library).
31*9c5db199SXin Li    def callback(fps):
32*9c5db199SXin Li        ...
33*9c5db199SXin Li
34*9c5db199SXin Li    with FPSMeter(callback) as fps_meter:
35*9c5db199SXin Li        ...
36*9c5db199SXin Li
37*9c5db199SXin LiWhen used as a library, it launches two threads to monitor system FPS rate
38*9c5db199SXin Liperiodically. Each time when a FPS rate is sampled, callback() is called with
39*9c5db199SXin Lithe FPS number as its parameter.
40*9c5db199SXin Li"""
41*9c5db199SXin Li
42*9c5db199SXin Lifrom __future__ import absolute_import
43*9c5db199SXin Lifrom __future__ import division
44*9c5db199SXin Lifrom __future__ import print_function
45*9c5db199SXin Li
46*9c5db199SXin Liimport argparse
47*9c5db199SXin Liimport atexit
48*9c5db199SXin Liimport collections
49*9c5db199SXin Liimport common
50*9c5db199SXin Liimport functools
51*9c5db199SXin Liimport logging
52*9c5db199SXin Liimport os
53*9c5db199SXin Liimport re
54*9c5db199SXin Liimport sys
55*9c5db199SXin Liimport threading
56*9c5db199SXin Liimport time
57*9c5db199SXin Li
58*9c5db199SXin Lifrom six.moves import range
59*9c5db199SXin Li
60*9c5db199SXin Lifrom autotest_lib.client.bin import utils as bin_utils
61*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils as common_lib_utils
62*9c5db199SXin Li
63*9c5db199SXin LiTRACE_ROOT = '/sys/kernel/debug/tracing/'
64*9c5db199SXin LiVBLANK_SWITCH = os.path.join(TRACE_ROOT, 'events/drm/drm_vblank_event/enable')
65*9c5db199SXin LiFLIP_SWITCH = os.path.join(TRACE_ROOT, 'events/i915/i915_flip_complete/enable')
66*9c5db199SXin LiWORKQ_SWITCH = os.path.join(
67*9c5db199SXin Li        TRACE_ROOT, 'events/workqueue/workqueue_execute_start/enable')
68*9c5db199SXin LiWORKQ_FILTER = os.path.join(
69*9c5db199SXin Li        TRACE_ROOT, 'events/workqueue/workqueue_execute_start/filter')
70*9c5db199SXin LiTRACE_SWITCH = os.path.join(TRACE_ROOT, 'tracing_on')
71*9c5db199SXin LiTRACE_CLOCK = os.path.join(TRACE_ROOT, 'trace_clock')
72*9c5db199SXin LiTRACE_LOG = os.path.join(TRACE_ROOT, 'trace')
73*9c5db199SXin LiTRACE_PIPE = os.path.join(TRACE_ROOT, 'trace_pipe')
74*9c5db199SXin LiTRACE_MARKER = os.path.join(TRACE_ROOT, 'trace_marker')
75*9c5db199SXin LiREFRESH_RATE = 60
76*9c5db199SXin LiNOTIFY_STRING = 'notify_collection'
77*9c5db199SXin LiSTOP_STRING = 'stop_tracing'
78*9c5db199SXin Li
79*9c5db199SXin Li
80*9c5db199SXin Lidef is_intel_gpu():
81*9c5db199SXin Li    """Determines whether it is intel GPU."""
82*9c5db199SXin Li    return os.path.isdir('/sys/bus/pci/drivers/i915')
83*9c5db199SXin Li
84*9c5db199SXin Li
85*9c5db199SXin Lidef get_kernel_version():
86*9c5db199SXin Li    """ Retruns the kernel version in form of xx.xx. """
87*9c5db199SXin Li    m = re.match(r'(\d+\.\d+)\.\d+', bin_utils.get_kernel_version())
88*9c5db199SXin Li    if m:
89*9c5db199SXin Li        return m.group(1)
90*9c5db199SXin Li    return 'unknown'
91*9c5db199SXin Li
92*9c5db199SXin Li
93*9c5db199SXin Lidef get_trace_method():
94*9c5db199SXin Li    """Gets the FPS checking method.
95*9c5db199SXin Li
96*9c5db199SXin Li    Checks i915_flip_complete for Intel GPU on Kernel 3.18.
97*9c5db199SXin Li    Checks intel_atomic_commit_work for Intel GPU on Kernel 4.4.
98*9c5db199SXin Li    Checks drm_vblank_event otherwise.
99*9c5db199SXin Li    """
100*9c5db199SXin Li    if is_intel_gpu():
101*9c5db199SXin Li        kernel_version = get_kernel_version()
102*9c5db199SXin Li        if kernel_version == '4.4':
103*9c5db199SXin Li            return 'workq'
104*9c5db199SXin Li        elif kernel_version == '3.18':
105*9c5db199SXin Li            return 'flip'
106*9c5db199SXin Li    # Fallback.
107*9c5db199SXin Li    return 'vblank'
108*9c5db199SXin Li
109*9c5db199SXin Li
110*9c5db199SXin Lidef set_simple_switch(value, filename):
111*9c5db199SXin Li    """ Sets simple switch '1' to the file. """
112*9c5db199SXin Li    orig = common_lib_utils.read_file(filename).strip()
113*9c5db199SXin Li    atexit.register(common_lib_utils.open_write_close, filename, orig)
114*9c5db199SXin Li    common_lib_utils.open_write_close(filename, value)
115*9c5db199SXin Li
116*9c5db199SXin Li
117*9c5db199SXin Lidef set_trace_clock():
118*9c5db199SXin Li    """ Sets trace clock to mono time as chrome tracing in CrOS. """
119*9c5db199SXin Li    PREFERRED_TRACE_CLOCK = 'mono'
120*9c5db199SXin Li    clock = common_lib_utils.read_file(TRACE_CLOCK)
121*9c5db199SXin Li    m = re.match(r'.*\[(\w+)\]', clock)
122*9c5db199SXin Li    if m:
123*9c5db199SXin Li        orig_clock = m.group(1)
124*9c5db199SXin Li        atexit.register(common_lib_utils.open_write_close,
125*9c5db199SXin Li                        TRACE_CLOCK, orig_clock)
126*9c5db199SXin Li    common_lib_utils.open_write_close(TRACE_CLOCK, PREFERRED_TRACE_CLOCK)
127*9c5db199SXin Li
128*9c5db199SXin Li
129*9c5db199SXin Lidef get_kernel_symbol_addr(symbol):
130*9c5db199SXin Li    """ Gets the kernel symple address. Example line in kallsyms:
131*9c5db199SXin Li     ffffffffbc46cb03 T sys_exit
132*9c5db199SXin Li    """
133*9c5db199SXin Li    with open('/proc/kallsyms') as kallsyms:
134*9c5db199SXin Li        for line in kallsyms:
135*9c5db199SXin Li            items = line.split()
136*9c5db199SXin Li            if items[2] == symbol:
137*9c5db199SXin Li                addr = items[0]
138*9c5db199SXin Li                return addr
139*9c5db199SXin Li    return None
140*9c5db199SXin Li
141*9c5db199SXin Li
142*9c5db199SXin Lidef set_workq_filter(function_name):
143*9c5db199SXin Li    """ Sets the workq filter. """
144*9c5db199SXin Li    addr = get_kernel_symbol_addr(function_name)
145*9c5db199SXin Li    if addr:
146*9c5db199SXin Li        filter = 'function == 0x%s' % addr
147*9c5db199SXin Li        common_lib_utils.open_write_close(WORKQ_FILTER, filter)
148*9c5db199SXin Li        # Sets to 0 to remove the filter.
149*9c5db199SXin Li        atexit.register(common_lib_utils.open_write_close, WORKQ_FILTER, '0')
150*9c5db199SXin Li
151*9c5db199SXin Li
152*9c5db199SXin Lidef enable_tracing(trace_method):
153*9c5db199SXin Li    """Enables tracing."""
154*9c5db199SXin Li    if trace_method == 'workq':
155*9c5db199SXin Li        set_simple_switch('1', WORKQ_SWITCH)
156*9c5db199SXin Li        # There are many workqueue_execute_start events,
157*9c5db199SXin Li        # filter to reduce CPU usage.
158*9c5db199SXin Li        set_workq_filter('intel_atomic_commit_work')
159*9c5db199SXin Li    elif trace_method == 'flip':
160*9c5db199SXin Li        set_simple_switch('1', FLIP_SWITCH)
161*9c5db199SXin Li    else:
162*9c5db199SXin Li        set_simple_switch('1', VBLANK_SWITCH)
163*9c5db199SXin Li
164*9c5db199SXin Li    set_simple_switch('1', TRACE_SWITCH)
165*9c5db199SXin Li    set_trace_clock()
166*9c5db199SXin Li
167*9c5db199SXin Li
168*9c5db199SXin Lidef get_fps_info(trace_buffer, end_time):
169*9c5db199SXin Li    """ Checks all vblanks in the range [end_time - 1.0, end_time]. """
170*9c5db199SXin Li    frame_info = []
171*9c5db199SXin Li    step = 1.0 / REFRESH_RATE
172*9c5db199SXin Li    step_time = end_time - 1.0 + step
173*9c5db199SXin Li    frame_times = []
174*9c5db199SXin Li    for _ in range(REFRESH_RATE):
175*9c5db199SXin Li        # Checks if there are vblanks in a refresh interval.
176*9c5db199SXin Li        step_count = 0
177*9c5db199SXin Li        while trace_buffer and trace_buffer[0] < step_time:
178*9c5db199SXin Li            frame_times.append(trace_buffer.popleft())
179*9c5db199SXin Li            step_count += 1
180*9c5db199SXin Li
181*9c5db199SXin Li        # Each character represent an 1 / REFRESH_RATE interval.
182*9c5db199SXin Li        if step_count:
183*9c5db199SXin Li            if step_count >= 10:
184*9c5db199SXin Li                frame_info.append('*')
185*9c5db199SXin Li            else:
186*9c5db199SXin Li                frame_info.append(str(step_count))
187*9c5db199SXin Li        else:
188*9c5db199SXin Li            frame_info.append(' ')
189*9c5db199SXin Li        step_time += step
190*9c5db199SXin Li
191*9c5db199SXin Li    return frame_info, frame_times
192*9c5db199SXin Li
193*9c5db199SXin Li
194*9c5db199SXin Lidef start_thread(function, args=()):
195*9c5db199SXin Li    """ Starts a thread with given argument. """
196*9c5db199SXin Li    new_thread = threading.Thread(target=function, args=args)
197*9c5db199SXin Li    new_thread.start()
198*9c5db199SXin Li
199*9c5db199SXin Li
200*9c5db199SXin Liclass FPSMeter():
201*9c5db199SXin Li    """ Initializes a FPSMeter to measure system FPS periodically. """
202*9c5db199SXin Li    def __init__(self, callback):
203*9c5db199SXin Li        self.is_stopping = threading.Event()
204*9c5db199SXin Li        self.callback = callback
205*9c5db199SXin Li
206*9c5db199SXin Li
207*9c5db199SXin Li    def __enter__(self):
208*9c5db199SXin Li        self.start()
209*9c5db199SXin Li        return self
210*9c5db199SXin Li
211*9c5db199SXin Li
212*9c5db199SXin Li    def __exit__(self, type, value, traceback):
213*9c5db199SXin Li        self.stop()
214*9c5db199SXin Li
215*9c5db199SXin Li
216*9c5db199SXin Li    def notify_collection(self, period_sec=1.0):
217*9c5db199SXin Li        """ Writes a notification mark periodically. """
218*9c5db199SXin Li        logging.info('Notification thread is started')
219*9c5db199SXin Li        next_notify_time = time.time() + period_sec
220*9c5db199SXin Li        while True:
221*9c5db199SXin Li            # Calling time.sleep(1) may suspend for more than 1 second.
222*9c5db199SXin Li            # Sleeps until a specific time to avoid error accumulation.
223*9c5db199SXin Li            sleep_time = next_notify_time - time.time()
224*9c5db199SXin Li            next_notify_time += period_sec
225*9c5db199SXin Li            # Skips if current time is larger than next_notify_time.
226*9c5db199SXin Li            if sleep_time > 0:
227*9c5db199SXin Li                if self.is_stopping.wait(sleep_time):
228*9c5db199SXin Li                    logging.info('Leaving notification thread')
229*9c5db199SXin Li                    # So the main loop doesn't stuck in the readline().
230*9c5db199SXin Li                    common_lib_utils.open_write_close(TRACE_MARKER, STOP_STRING)
231*9c5db199SXin Li                    break
232*9c5db199SXin Li                common_lib_utils.open_write_close(TRACE_MARKER, NOTIFY_STRING)
233*9c5db199SXin Li
234*9c5db199SXin Li
235*9c5db199SXin Li    def main_loop(self, trace_method):
236*9c5db199SXin Li        """ Main loop to parse the trace.
237*9c5db199SXin Li
238*9c5db199SXin Li        There are 2 threads involved:
239*9c5db199SXin Li        Main thread:
240*9c5db199SXin Li            Using blocking read to get data from trace_pipe.
241*9c5db199SXin Li        Notify thread: The main thread may wait indifinitely if there
242*9c5db199SXin Li            is no new trace. Writes to the pipe once per second to avoid
243*9c5db199SXin Li            the indefinite waiting.
244*9c5db199SXin Li        """
245*9c5db199SXin Li        logging.info('Fps meter main thread is started.')
246*9c5db199SXin Li
247*9c5db199SXin Li        # Sample trace:
248*9c5db199SXin Li        # <idle>-0  [000] dNh3 631.905961: drm_vblank_event: crtc=0, seq=65496
249*9c5db199SXin Li        # <idle>-0  [000] d.h3 631.922681: drm_vblank_event: crtc=0, seq=65497
250*9c5db199SXin Li        # fps_meter [003] ..1 632.393953: tracing_mark_write: notify_collection
251*9c5db199SXin Li        # ..
252*9c5db199SXin Li        re_notify = re.compile(
253*9c5db199SXin Li                r'.* (\d+\.\d+): tracing_mark_write: ' + NOTIFY_STRING)
254*9c5db199SXin Li        if trace_method == 'workq':
255*9c5db199SXin Li            re_trace = re.compile(
256*9c5db199SXin Li                    r'.* (\d+\.\d+): workqueue_execute_start: '
257*9c5db199SXin Li                    r'work struct ([\da-f]+): '
258*9c5db199SXin Li                    r'function intel_atomic_commit_work')
259*9c5db199SXin Li        elif trace_method == 'flip':
260*9c5db199SXin Li            re_trace = re.compile(
261*9c5db199SXin Li                    r'.* (\d+\.\d+): i915_flip_complete: '
262*9c5db199SXin Li                    r'plane=(\d+), obj=([\da-f]+)')
263*9c5db199SXin Li        else:
264*9c5db199SXin Li            re_trace = re.compile(
265*9c5db199SXin Li                    r'.* (\d+\.\d+): drm_vblank_event: crtc=(\d+), seq=(\d+)')
266*9c5db199SXin Li
267*9c5db199SXin Li        trace_buffer = collections.deque()
268*9c5db199SXin Li        with open(TRACE_PIPE) as trace_pipe:
269*9c5db199SXin Li            # The pipe may block a few seconds if using:
270*9c5db199SXin Li            # for line in trace_pipe
271*9c5db199SXin Li            while not self.is_stopping.is_set():
272*9c5db199SXin Li                line = trace_pipe.readline()
273*9c5db199SXin Li                m_trace = re_trace.match(line)
274*9c5db199SXin Li                if m_trace:
275*9c5db199SXin Li                    timestamp = float(m_trace.group(1))
276*9c5db199SXin Li                    trace_buffer.append(timestamp)
277*9c5db199SXin Li                else:
278*9c5db199SXin Li                    m_notify = re_notify.match(line)
279*9c5db199SXin Li                    if m_notify:
280*9c5db199SXin Li                        timestamp = float(m_notify.group(1))
281*9c5db199SXin Li                        self.callback(get_fps_info(trace_buffer, timestamp))
282*9c5db199SXin Li            logging.info('Leaving fps meter main thread')
283*9c5db199SXin Li
284*9c5db199SXin Li
285*9c5db199SXin Li    def start(self):
286*9c5db199SXin Li        """ Starts the FPS meter by launching needed threads. """
287*9c5db199SXin Li        # The notificaiton thread.
288*9c5db199SXin Li        start_thread(self.notify_collection)
289*9c5db199SXin Li
290*9c5db199SXin Li        # The main thread.
291*9c5db199SXin Li        trace_method = get_trace_method()
292*9c5db199SXin Li        enable_tracing(trace_method)
293*9c5db199SXin Li        start_thread(self.main_loop, [trace_method])
294*9c5db199SXin Li
295*9c5db199SXin Li
296*9c5db199SXin Li    def stop(self):
297*9c5db199SXin Li        """ Stops the FPS meter. Shut down threads. """
298*9c5db199SXin Li        logging.info('Shutting down FPS meter')
299*9c5db199SXin Li        self.is_stopping.set()
300*9c5db199SXin Li
301*9c5db199SXin Li
302*9c5db199SXin Lidef output_fps_info(verbose, fps_info):
303*9c5db199SXin Li    """ Print the fps info to the screen. """
304*9c5db199SXin Li    frame_info, frame_times = fps_info
305*9c5db199SXin Li    fps_count = len([f for f in frame_info if f != ' '])
306*9c5db199SXin Li    frame_info_str = ''.join(frame_info)
307*9c5db199SXin Li    print('[%s] FPS: %2d' % (frame_info_str, fps_count))
308*9c5db199SXin Li    if frame_times and verbose:
309*9c5db199SXin Li        print(', '.join(['%.3f' % t for t in frame_times]))
310*9c5db199SXin Li
311*9c5db199SXin Li
312*9c5db199SXin Lidef main(argv):
313*9c5db199SXin Li    """ Print fps information on the screen. """
314*9c5db199SXin Li    parser = argparse.ArgumentParser(description='Print fps infomation.')
315*9c5db199SXin Li    parser.add_argument('--verbose', action='store_true',
316*9c5db199SXin Li                                        help='print verbose frame time info')
317*9c5db199SXin Li    parser.add_argument('--debug', action='store_true',
318*9c5db199SXin Li                                        help='print debug message')
319*9c5db199SXin Li    options = parser.parse_args()
320*9c5db199SXin Li
321*9c5db199SXin Li    if options.debug:
322*9c5db199SXin Li        rootLogger = logging.getLogger()
323*9c5db199SXin Li        rootLogger.setLevel(logging.DEBUG)
324*9c5db199SXin Li        # StreamHandler() defaults to stderr.
325*9c5db199SXin Li        rootLogger.addHandler(logging.StreamHandler())
326*9c5db199SXin Li
327*9c5db199SXin Li    print('FPS meter trace method %s' % get_trace_method())
328*9c5db199SXin Li    with FPSMeter(functools.partial(output_fps_info, options.verbose)):
329*9c5db199SXin Li        while True:
330*9c5db199SXin Li            try:
331*9c5db199SXin Li                time.sleep(86400)
332*9c5db199SXin Li            except KeyboardInterrupt:
333*9c5db199SXin Li                print('Exiting...')
334*9c5db199SXin Li                break
335*9c5db199SXin Li
336*9c5db199SXin Li
337*9c5db199SXin Liif __name__ == '__main__':
338*9c5db199SXin Li    sys.exit(main(sys.argv[1:]))
339