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