blob: 1e754f88e1f04bf557abb7e50e53760ff5868cda [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.
5"""Checks kernel tracing events to get the FPS of a CrOS device.
6
7This script requires root privilege to work properly. It may interfere
8Chrome tracing because both use ftrace.
9
10Limitation:
11It doesn't support multi-screen.
12It assumes 60 HZ screen refresh rate.
13
14Sample output:
15# ./fps_meter.py
16trace method: workq
17[111111111111111111111111111111111111111111111111111111111111] FPS: 60
18[111111111111111111111111111111111111111111111111111111111111] FPS: 60
19[11111111111111111111111111111111111111111111111 111111111111] FPS: 59
20[111111111111111111111111111111111111111111111111111111111111] FPS: 60
21[11111111111111 11111111111111111111 11111111 111111111111111] FPS: 57
22[111111111111111111111111111111111 11111111111111111111111] FPS: 56
23[111 111111111111111111111111111111111111111111111111111111] FPS: 57
24 ^
25 1 : Frame update count detected in this 1/60 sec interval.
26"""
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +080027import argparse
Vovo Yang925176b2018-01-25 10:32:54 +080028import atexit
29import collections
30import os
31import re
32import threading
33import time
34
35TRACE_ROOT = '/sys/kernel/debug/tracing/'
36VBLANK_SWITCH = os.path.join(TRACE_ROOT, 'events/drm/drm_vblank_event/enable')
37FLIP_SWITCH = os.path.join(TRACE_ROOT, 'events/i915/i915_flip_complete/enable')
38WORKQ_SWITCH = os.path.join(
39 TRACE_ROOT, 'events/workqueue/workqueue_execute_start/enable')
40WORKQ_FILTER = os.path.join(
41 TRACE_ROOT, 'events/workqueue/workqueue_execute_start/filter')
42TRACE_SWITCH = os.path.join(TRACE_ROOT, 'tracing_on')
43TRACE_CLOCK = os.path.join(TRACE_ROOT, 'trace_clock')
44TRACE_LOG = os.path.join(TRACE_ROOT, 'trace')
45TRACE_PIPE = os.path.join(TRACE_ROOT, 'trace_pipe')
46TRACE_MARKER = os.path.join(TRACE_ROOT, 'trace_marker')
47REFRESH_RATE = 60
48NOTIFY_STRING = 'notify_collection'
49
50
51def is_intel_gpu():
52 return os.path.isdir('/sys/bus/pci/drivers/i915')
53
54
55def get_kernel_version():
56 with open('/proc/version') as fin:
57 m = re.match(r'Linux version (\d+.\d+).\d+', fin.read())
58 if m:
59 return m.group(1)
60 return 'unknown'
61
62
63def get_trace_method():
64 """Gets the FPS checking method.
65
66 Checks i915_flip_complete for Intel GPU on Kernel 3.18.
67 Checks intel_atomic_commit_work for Intel GPU on Kernel 4.4.
68 Checks drm_vblank_event otherwise.
69 """
70 if is_intel_gpu():
71 kernel_version = get_kernel_version()
72 if kernel_version == '4.4':
73 return 'workq'
74 elif kernel_version == '3.18':
75 return 'flip'
76 # Fallback.
77 return 'vblank'
78
79
80def write_to_file(content, filename):
81 with open(filename, 'w') as fout:
82 fout.write(content)
83
84
85def set_simple_switch(value, filename):
86 with open(filename) as fin:
87 orig = fin.read().strip()
88 atexit.register(write_to_file, orig, filename)
89 write_to_file(value, filename)
90
91
92def set_trace_clock():
93 # Set trace clock to mono time as chrome tracing in CrOS.
94 PREFERRED_TRACE_CLOCK = 'mono'
95 with open(TRACE_CLOCK) as trace_clock:
96 m = re.match(r'.*\[(\w+)\]', trace_clock.read())
97 if m:
98 orig_clock = m.group(1)
99 atexit.register(write_to_file, orig_clock, TRACE_CLOCK)
100 write_to_file(PREFERRED_TRACE_CLOCK, TRACE_CLOCK)
101
102
103def get_kernel_symbol_addr(symbol):
104 # Example line in kallsyms:
105 # ffffffffbc46cb03 T sys_exit
106 with open('/proc/kallsyms') as kallsyms:
107 for line in kallsyms:
108 items = line.split()
109 if items[2] == symbol:
110 addr = items[0]
111 return addr
112 return None
113
114
115def set_workq_filter(function_name):
116 addr = get_kernel_symbol_addr(function_name)
117 if addr:
118 filter = 'function == 0x%s' % addr
119 write_to_file(filter, WORKQ_FILTER)
120 # Sets to 0 to remove the filter.
121 atexit.register(write_to_file, '0', WORKQ_FILTER)
122
123
124def enable_tracing(trace_method):
125 if trace_method == 'workq':
126 set_simple_switch('1', WORKQ_SWITCH)
127 # There are many workqueue_execute_start events,
128 # filter to reduce CPU usage.
129 set_workq_filter('intel_atomic_commit_work')
130 elif trace_method == 'flip':
131 set_simple_switch('1', FLIP_SWITCH)
132 else:
133 set_simple_switch('1', VBLANK_SWITCH)
134
135 set_simple_switch('1', TRACE_SWITCH)
136 set_trace_clock()
137
138
139def notify_collection(period_sec=1.0):
140 next_notify_time = time.time() + period_sec
141 while True:
142 # Calling time.sleep(1) may suspend for more than 1 second.
143 # Sleeps until a specific time to avoid error accumulation.
144 sleep_time = next_notify_time - time.time()
145 next_notify_time += period_sec
146 # Skips if current time is larger than next_notify_time.
147 if sleep_time > 0:
148 time.sleep(sleep_time)
149 write_to_file(NOTIFY_STRING, TRACE_MARKER)
150
151
152def start_notify_thread():
153 notify_thread = threading.Thread(target=notify_collection)
154 notify_thread.daemon = True
155 notify_thread.start()
156
157
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800158def process_trace_buffer(trace_buffer, end_time, verbose=False):
Vovo Yang925176b2018-01-25 10:32:54 +0800159 # Checks all vblanks in the range [end_time - 1.0, end_time].
160 frame_info = []
161 step = 1.0 / REFRESH_RATE
162 step_time = end_time - 1.0 + step
163 fps_count = 0
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))
178 fps_count += 1
179 else:
180 frame_info.append(' ')
181 step_time += step
182
183 frame_info_str = ''.join(frame_info)
184 print '[%s] FPS: %2d' % (frame_info_str, fps_count)
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800185 if frame_times and verbose:
186 print ', '.join(['%.3f' % t for t in frame_times])
Vovo Yang925176b2018-01-25 10:32:54 +0800187
188
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800189def main_loop(trace_method, verbose=False):
Vovo Yang925176b2018-01-25 10:32:54 +0800190 """Main loop to parse the trace.
191
192 There are 2 threads involved:
193 Main thread:
194 Using blocking read to get data from trace_pipe.
195 Notify thread: The main thread may wait indifinitely if there
196 is no new trace. Writes to the pipe once per second to avoid
197 the indefinite waiting.
198 """
199 start_notify_thread()
200
201 # Sample trace:
202 # <idle>-0 [000] dNh3 631.905961: drm_vblank_event: crtc=0, seq=65496
203 # <idle>-0 [000] d.h3 631.922681: drm_vblank_event: crtc=0, seq=65497
204 # fps_meter [003] ...1 632.393953: tracing_mark_write: notify_collection
205 # ...
206 re_notify = re.compile(
207 r'.* (\d+.\d+): tracing_mark_write: ' + NOTIFY_STRING)
208 if trace_method == 'workq':
209 re_trace = re.compile(
210 r'.* (\d+.\d+): workqueue_execute_start: work struct ([\da-f]+): '
211 'function intel_atomic_commit_work')
212 elif trace_method == 'flip':
213 re_trace = re.compile(
214 r'.* (\d+.\d+): i915_flip_complete: plane=(\d+), obj=([\da-f]+)')
215 else:
216 re_trace = re.compile(
217 r'.* (\d+.\d+): drm_vblank_event: crtc=(\d+), seq=(\d+)')
218
219 trace_buffer = collections.deque()
220 with open(TRACE_PIPE) as trace_pipe:
221 # The pipe may block a few seconds if using:
222 # for line in trace_pipe
223 while True:
224 line = trace_pipe.readline()
225 m_trace = re_trace.match(line)
226 if m_trace:
227 timestamp = float(m_trace.group(1))
228 trace_buffer.append(timestamp)
229 else:
230 m_notify = re_notify.match(line)
231 if m_notify:
232 timestamp = float(m_notify.group(1))
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800233 process_trace_buffer(trace_buffer, timestamp, verbose)
Vovo Yang925176b2018-01-25 10:32:54 +0800234
235
236if __name__ == '__main__':
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800237 # parsing arguments
238 parser = argparse.ArgumentParser(description='Print fps infomation.')
239 parser.add_argument('-v', dest='verbose', action='store_true',
240 help='print verbose frame time info')
241 args = parser.parse_args()
242
Vovo Yang925176b2018-01-25 10:32:54 +0800243 trace_method = get_trace_method()
244 print 'trace method:', trace_method
245 enable_tracing(trace_method)
Kuo-Hsin Yangec36bcd2018-02-12 23:37:36 +0800246 main_loop(trace_method, args.verbose)
Vovo Yang925176b2018-01-25 10:32:54 +0800247