blob: c7ca29a788f2f5e3abb4383f8ba8474ff673135c [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"""
27import atexit
28import collections
29import os
30import re
31import threading
32import time
33
34TRACE_ROOT = '/sys/kernel/debug/tracing/'
35VBLANK_SWITCH = os.path.join(TRACE_ROOT, 'events/drm/drm_vblank_event/enable')
36FLIP_SWITCH = os.path.join(TRACE_ROOT, 'events/i915/i915_flip_complete/enable')
37WORKQ_SWITCH = os.path.join(
38 TRACE_ROOT, 'events/workqueue/workqueue_execute_start/enable')
39WORKQ_FILTER = os.path.join(
40 TRACE_ROOT, 'events/workqueue/workqueue_execute_start/filter')
41TRACE_SWITCH = os.path.join(TRACE_ROOT, 'tracing_on')
42TRACE_CLOCK = os.path.join(TRACE_ROOT, 'trace_clock')
43TRACE_LOG = os.path.join(TRACE_ROOT, 'trace')
44TRACE_PIPE = os.path.join(TRACE_ROOT, 'trace_pipe')
45TRACE_MARKER = os.path.join(TRACE_ROOT, 'trace_marker')
46REFRESH_RATE = 60
47NOTIFY_STRING = 'notify_collection'
48
49
50def is_intel_gpu():
51 return os.path.isdir('/sys/bus/pci/drivers/i915')
52
53
54def get_kernel_version():
55 with open('/proc/version') as fin:
56 m = re.match(r'Linux version (\d+.\d+).\d+', fin.read())
57 if m:
58 return m.group(1)
59 return 'unknown'
60
61
62def get_trace_method():
63 """Gets the FPS checking method.
64
65 Checks i915_flip_complete for Intel GPU on Kernel 3.18.
66 Checks intel_atomic_commit_work for Intel GPU on Kernel 4.4.
67 Checks drm_vblank_event otherwise.
68 """
69 if is_intel_gpu():
70 kernel_version = get_kernel_version()
71 if kernel_version == '4.4':
72 return 'workq'
73 elif kernel_version == '3.18':
74 return 'flip'
75 # Fallback.
76 return 'vblank'
77
78
79def write_to_file(content, filename):
80 with open(filename, 'w') as fout:
81 fout.write(content)
82
83
84def set_simple_switch(value, filename):
85 with open(filename) as fin:
86 orig = fin.read().strip()
87 atexit.register(write_to_file, orig, filename)
88 write_to_file(value, filename)
89
90
91def set_trace_clock():
92 # Set trace clock to mono time as chrome tracing in CrOS.
93 PREFERRED_TRACE_CLOCK = 'mono'
94 with open(TRACE_CLOCK) as trace_clock:
95 m = re.match(r'.*\[(\w+)\]', trace_clock.read())
96 if m:
97 orig_clock = m.group(1)
98 atexit.register(write_to_file, orig_clock, TRACE_CLOCK)
99 write_to_file(PREFERRED_TRACE_CLOCK, TRACE_CLOCK)
100
101
102def get_kernel_symbol_addr(symbol):
103 # Example line in kallsyms:
104 # ffffffffbc46cb03 T sys_exit
105 with open('/proc/kallsyms') as kallsyms:
106 for line in kallsyms:
107 items = line.split()
108 if items[2] == symbol:
109 addr = items[0]
110 return addr
111 return None
112
113
114def set_workq_filter(function_name):
115 addr = get_kernel_symbol_addr(function_name)
116 if addr:
117 filter = 'function == 0x%s' % addr
118 write_to_file(filter, WORKQ_FILTER)
119 # Sets to 0 to remove the filter.
120 atexit.register(write_to_file, '0', WORKQ_FILTER)
121
122
123def enable_tracing(trace_method):
124 if trace_method == 'workq':
125 set_simple_switch('1', WORKQ_SWITCH)
126 # There are many workqueue_execute_start events,
127 # filter to reduce CPU usage.
128 set_workq_filter('intel_atomic_commit_work')
129 elif trace_method == 'flip':
130 set_simple_switch('1', FLIP_SWITCH)
131 else:
132 set_simple_switch('1', VBLANK_SWITCH)
133
134 set_simple_switch('1', TRACE_SWITCH)
135 set_trace_clock()
136
137
138def notify_collection(period_sec=1.0):
139 next_notify_time = time.time() + period_sec
140 while True:
141 # Calling time.sleep(1) may suspend for more than 1 second.
142 # Sleeps until a specific time to avoid error accumulation.
143 sleep_time = next_notify_time - time.time()
144 next_notify_time += period_sec
145 # Skips if current time is larger than next_notify_time.
146 if sleep_time > 0:
147 time.sleep(sleep_time)
148 write_to_file(NOTIFY_STRING, TRACE_MARKER)
149
150
151def start_notify_thread():
152 notify_thread = threading.Thread(target=notify_collection)
153 notify_thread.daemon = True
154 notify_thread.start()
155
156
157def process_trace_buffer(trace_buffer, end_time):
158 # Checks all vblanks in the range [end_time - 1.0, end_time].
159 frame_info = []
160 step = 1.0 / REFRESH_RATE
161 step_time = end_time - 1.0 + step
162 fps_count = 0
163 for _ in range(REFRESH_RATE):
164 # Checks if there are vblanks in a refresh interval.
165 step_count = 0
166 while trace_buffer and trace_buffer[0] < step_time:
167 trace_buffer.popleft()
168 step_count += 1
169
170 # Each character represent an 1 / REFRESH_RATE interval.
171 if step_count:
172 if step_count >= 10:
173 frame_info.append('*')
174 else:
175 frame_info.append(str(step_count))
176 fps_count += 1
177 else:
178 frame_info.append(' ')
179 step_time += step
180
181 frame_info_str = ''.join(frame_info)
182 print '[%s] FPS: %2d' % (frame_info_str, fps_count)
183
184
185def main_loop(trace_method):
186 """Main loop to parse the trace.
187
188 There are 2 threads involved:
189 Main thread:
190 Using blocking read to get data from trace_pipe.
191 Notify thread: The main thread may wait indifinitely if there
192 is no new trace. Writes to the pipe once per second to avoid
193 the indefinite waiting.
194 """
195 start_notify_thread()
196
197 # Sample trace:
198 # <idle>-0 [000] dNh3 631.905961: drm_vblank_event: crtc=0, seq=65496
199 # <idle>-0 [000] d.h3 631.922681: drm_vblank_event: crtc=0, seq=65497
200 # fps_meter [003] ...1 632.393953: tracing_mark_write: notify_collection
201 # ...
202 re_notify = re.compile(
203 r'.* (\d+.\d+): tracing_mark_write: ' + NOTIFY_STRING)
204 if trace_method == 'workq':
205 re_trace = re.compile(
206 r'.* (\d+.\d+): workqueue_execute_start: work struct ([\da-f]+): '
207 'function intel_atomic_commit_work')
208 elif trace_method == 'flip':
209 re_trace = re.compile(
210 r'.* (\d+.\d+): i915_flip_complete: plane=(\d+), obj=([\da-f]+)')
211 else:
212 re_trace = re.compile(
213 r'.* (\d+.\d+): drm_vblank_event: crtc=(\d+), seq=(\d+)')
214
215 trace_buffer = collections.deque()
216 with open(TRACE_PIPE) as trace_pipe:
217 # The pipe may block a few seconds if using:
218 # for line in trace_pipe
219 while True:
220 line = trace_pipe.readline()
221 m_trace = re_trace.match(line)
222 if m_trace:
223 timestamp = float(m_trace.group(1))
224 trace_buffer.append(timestamp)
225 else:
226 m_notify = re_notify.match(line)
227 if m_notify:
228 timestamp = float(m_notify.group(1))
229 process_trace_buffer(trace_buffer, timestamp)
230
231
232if __name__ == '__main__':
233 trace_method = get_trace_method()
234 print 'trace method:', trace_method
235 enable_tracing(trace_method)
236 main_loop(trace_method)
237