blob: 3826b66b67008ce8daddf90d81c7338d328b4518 [file] [log] [blame]
Vovo Yang925176b2018-01-25 10:32:54 +08001#!/usr/bin/python
2# 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.
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +08005
Vovo Yang925176b2018-01-25 10:32:54 +08006"""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
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +080015This 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.
Vovo Yang925176b2018-01-25 10:32:54 +080040"""
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +080041import argparse
Vovo Yang925176b2018-01-25 10:32:54 +080042import atexit
43import collections
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +080044import functools
45import logging
Vovo Yang925176b2018-01-25 10:32:54 +080046import os
47import re
48import threading
49import time
50
51TRACE_ROOT = '/sys/kernel/debug/tracing/'
52VBLANK_SWITCH = os.path.join(TRACE_ROOT, 'events/drm/drm_vblank_event/enable')
53FLIP_SWITCH = os.path.join(TRACE_ROOT, 'events/i915/i915_flip_complete/enable')
54WORKQ_SWITCH = os.path.join(
55 TRACE_ROOT, 'events/workqueue/workqueue_execute_start/enable')
56WORKQ_FILTER = os.path.join(
57 TRACE_ROOT, 'events/workqueue/workqueue_execute_start/filter')
58TRACE_SWITCH = os.path.join(TRACE_ROOT, 'tracing_on')
59TRACE_CLOCK = os.path.join(TRACE_ROOT, 'trace_clock')
60TRACE_LOG = os.path.join(TRACE_ROOT, 'trace')
61TRACE_PIPE = os.path.join(TRACE_ROOT, 'trace_pipe')
62TRACE_MARKER = os.path.join(TRACE_ROOT, 'trace_marker')
63REFRESH_RATE = 60
64NOTIFY_STRING = 'notify_collection'
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +080065STOP_STRING = 'stop_tracing'
Vovo Yang925176b2018-01-25 10:32:54 +080066
67
68def is_intel_gpu():
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +080069 """Whether is intel GPU."""
Vovo Yang925176b2018-01-25 10:32:54 +080070 return os.path.isdir('/sys/bus/pci/drivers/i915')
71
72
73def get_kernel_version():
74 with open('/proc/version') as fin:
75 m = re.match(r'Linux version (\d+.\d+).\d+', fin.read())
76 if m:
77 return m.group(1)
78 return 'unknown'
79
80
81def get_trace_method():
82 """Gets the FPS checking method.
83
84 Checks i915_flip_complete for Intel GPU on Kernel 3.18.
85 Checks intel_atomic_commit_work for Intel GPU on Kernel 4.4.
86 Checks drm_vblank_event otherwise.
87 """
88 if is_intel_gpu():
89 kernel_version = get_kernel_version()
90 if kernel_version == '4.4':
91 return 'workq'
92 elif kernel_version == '3.18':
93 return 'flip'
94 # Fallback.
95 return 'vblank'
96
97
98def write_to_file(content, filename):
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +080099 """Writes content to a file."""
Vovo Yang925176b2018-01-25 10:32:54 +0800100 with open(filename, 'w') as fout:
101 fout.write(content)
102
103
104def set_simple_switch(value, filename):
105 with open(filename) as fin:
106 orig = fin.read().strip()
107 atexit.register(write_to_file, orig, filename)
108 write_to_file(value, filename)
109
110
111def set_trace_clock():
112 # Set trace clock to mono time as chrome tracing in CrOS.
113 PREFERRED_TRACE_CLOCK = 'mono'
114 with open(TRACE_CLOCK) as trace_clock:
115 m = re.match(r'.*\[(\w+)\]', trace_clock.read())
116 if m:
117 orig_clock = m.group(1)
118 atexit.register(write_to_file, orig_clock, TRACE_CLOCK)
119 write_to_file(PREFERRED_TRACE_CLOCK, TRACE_CLOCK)
120
121
122def get_kernel_symbol_addr(symbol):
123 # Example line in kallsyms:
124 # ffffffffbc46cb03 T sys_exit
125 with open('/proc/kallsyms') as kallsyms:
126 for line in kallsyms:
127 items = line.split()
128 if items[2] == symbol:
129 addr = items[0]
130 return addr
131 return None
132
133
134def set_workq_filter(function_name):
135 addr = get_kernel_symbol_addr(function_name)
136 if addr:
137 filter = 'function == 0x%s' % addr
138 write_to_file(filter, WORKQ_FILTER)
139 # Sets to 0 to remove the filter.
140 atexit.register(write_to_file, '0', WORKQ_FILTER)
141
142
143def enable_tracing(trace_method):
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +0800144 """Enables tracing."""
Vovo Yang925176b2018-01-25 10:32:54 +0800145 if trace_method == 'workq':
146 set_simple_switch('1', WORKQ_SWITCH)
147 # There are many workqueue_execute_start events,
148 # filter to reduce CPU usage.
149 set_workq_filter('intel_atomic_commit_work')
150 elif trace_method == 'flip':
151 set_simple_switch('1', FLIP_SWITCH)
152 else:
153 set_simple_switch('1', VBLANK_SWITCH)
154
155 set_simple_switch('1', TRACE_SWITCH)
156 set_trace_clock()
157
158
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +0800159def get_fps_info(trace_buffer, end_time):
Vovo Yang925176b2018-01-25 10:32:54 +0800160 # Checks all vblanks in the range [end_time - 1.0, end_time].
161 frame_info = []
162 step = 1.0 / REFRESH_RATE
163 step_time = end_time - 1.0 + step
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800164 frame_times = []
Vovo Yang925176b2018-01-25 10:32:54 +0800165 for _ in range(REFRESH_RATE):
166 # Checks if there are vblanks in a refresh interval.
167 step_count = 0
168 while trace_buffer and trace_buffer[0] < step_time:
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800169 frame_times.append(trace_buffer.popleft())
Vovo Yang925176b2018-01-25 10:32:54 +0800170 step_count += 1
171
172 # Each character represent an 1 / REFRESH_RATE interval.
173 if step_count:
174 if step_count >= 10:
175 frame_info.append('*')
176 else:
177 frame_info.append(str(step_count))
Vovo Yang925176b2018-01-25 10:32:54 +0800178 else:
179 frame_info.append(' ')
180 step_time += step
181
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +0800182 return frame_info, frame_times
183
184
185def start_thread(function, args=()):
186 """Starts a thread with given argument."""
187 new_thread = threading.Thread(target=function, args=args)
188 new_thread.start()
189
190
191class FPSMeter():
192 """A FPSMeter to measure system FPS periodically."""
193 def __init__(self, callback):
194 self.is_stopping = threading.Event()
195 self.callback = callback
196
197 def __enter__(self):
198 self.start()
199 return self
200
201 def __exit__(self, type, value, traceback):
202 self.stop()
203
204 def notify_collection(self, period_sec=1.0):
205 """Writes a notification mark periodically."""
206 logging.info('Notification thread is started')
207 next_notify_time = time.time() + period_sec
208 while True:
209 # Calling time.sleep(1) may suspend for more than 1 second.
210 # Sleeps until a specific time to avoid error accumulation.
211 sleep_time = next_notify_time - time.time()
212 next_notify_time += period_sec
213 # Skips if current time is larger than next_notify_time.
214 if sleep_time > 0:
215 if self.is_stopping.wait(sleep_time):
216 logging.info('Leaving notification thread')
217 # So the main loop doesn't stuck in the readline().
218 write_to_file(STOP_STRING, TRACE_MARKER)
219 break
220 write_to_file(NOTIFY_STRING, TRACE_MARKER)
221
222 def main_loop(self, trace_method):
223 """Main loop to parse the trace.
224
225 There are 2 threads involved:
226 Main thread:
227 Using blocking read to get data from trace_pipe.
228 Notify thread: The main thread may wait indifinitely if there
229 is no new trace. Writes to the pipe once per second to avoid
230 the indefinite waiting.
231 """
232 logging.info('Fps meter main thread is started.')
233
234 # Sample trace:
235 # <idle>-0 [000] dNh3 631.905961: drm_vblank_event: crtc=0, seq=65496
236 # <idle>-0 [000] d.h3 631.922681: drm_vblank_event: crtc=0, seq=65497
237 # fps_meter [003] ...1 632.393953: tracing_mark_write: notify_collection
238 # ...
239 re_notify = re.compile(
240 r'.* (\d+.\d+): tracing_mark_write: ' + NOTIFY_STRING)
241 if trace_method == 'workq':
242 re_trace = re.compile(
243 r'.* (\d+.\d+): workqueue_execute_start: work struct ([\da-f]+): '
244 'function intel_atomic_commit_work')
245 elif trace_method == 'flip':
246 re_trace = re.compile(
247 r'.* (\d+.\d+): i915_flip_complete: plane=(\d+), obj=([\da-f]+)')
248 else:
249 re_trace = re.compile(
250 r'.* (\d+.\d+): drm_vblank_event: crtc=(\d+), seq=(\d+)')
251
252 trace_buffer = collections.deque()
253 with open(TRACE_PIPE) as trace_pipe:
254 # The pipe may block a few seconds if using:
255 # for line in trace_pipe
256 while not self.is_stopping.is_set():
257 line = trace_pipe.readline()
258 m_trace = re_trace.match(line)
259 if m_trace:
260 timestamp = float(m_trace.group(1))
261 trace_buffer.append(timestamp)
262 else:
263 m_notify = re_notify.match(line)
264 if m_notify:
265 timestamp = float(m_notify.group(1))
266 self.callback(get_fps_info(trace_buffer, timestamp))
267 logging.info('Leaving fps meter main thread')
268
269 def start(self):
270 """Starts the FPS meter by launching needed threads."""
271 # The notificaiton thread.
272 start_thread(self.notify_collection)
273
274 # The main thread.
275 trace_method = get_trace_method()
276 enable_tracing(trace_method)
277 start_thread(self.main_loop, [trace_method])
278
279 def stop(self):
280 """Stops the FPS meter. Shut down threads."""
281 logging.info('Shutting down FPS meter')
282 self.is_stopping.set()
283
284
285def output_fps_info(verbose, fps_info):
286 frame_info, frame_times = fps_info
287 fps_count = len([f for f in frame_info if f != ' '])
Vovo Yang925176b2018-01-25 10:32:54 +0800288 frame_info_str = ''.join(frame_info)
289 print '[%s] FPS: %2d' % (frame_info_str, fps_count)
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800290 if frame_times and verbose:
291 print ', '.join(['%.3f' % t for t in frame_times])
Vovo Yang925176b2018-01-25 10:32:54 +0800292
293
Vovo Yang925176b2018-01-25 10:32:54 +0800294if __name__ == '__main__':
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800295 # parsing arguments
296 parser = argparse.ArgumentParser(description='Print fps infomation.')
297 parser.add_argument('-v', dest='verbose', action='store_true',
298 help='print verbose frame time info')
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +0800299 parser.add_argument('-d', dest='debug', action='store_true',
300 help='print debug message')
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800301 args = parser.parse_args()
302
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +0800303 if args.debug:
304 rootLogger = logging.getLogger()
305 rootLogger.setLevel(logging.DEBUG)
306 # StreamHandler() defaults to stderr.
307 rootLogger.addHandler(logging.StreamHandler())
Vovo Yang925176b2018-01-25 10:32:54 +0800308
Cheng-Yu Lee71e236c2018-03-31 06:10:56 +0800309 print 'FPS meter trace method %s' % get_trace_method()
310 with FPSMeter(functools.partial(output_fps_info, args.verbose)) as fps_meter:
311 while True:
312 try:
313 time.sleep(86400)
314 except KeyboardInterrupt:
315 print 'Exiting...'
316 break