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