fps_meter: measuring fps with ftrace

Sample output:
[111111111111111111111111111111111111111111111111111111111111] FPS: 60
[11111111111111 11111111111111111111 11111111 111111111111111] FPS: 57
[111111111111111111111111111111111    11111111111111111111111] FPS: 56

BUG=chromium:805780
TEST=manual test

Change-Id: Id174a1027aca4e7d3365338331a64f16cf886f04
Reviewed-on: https://chromium-review.googlesource.com/880532
Commit-Ready: Vovo Yang <vovoy@chromium.org>
Tested-by: Vovo Yang <vovoy@chromium.org>
Reviewed-by: Cheng-Yu Lee <cylee@google.com>
diff --git a/fps_meter/fps_meter.py b/fps_meter/fps_meter.py
new file mode 100755
index 0000000..c7ca29a
--- /dev/null
+++ b/fps_meter/fps_meter.py
@@ -0,0 +1,237 @@
+#!/usr/bin/python
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Checks kernel tracing events to get the FPS of a CrOS device.
+
+This script requires root privilege to work properly. It may interfere
+Chrome tracing because both use ftrace.
+
+Limitation:
+It doesn't support multi-screen.
+It assumes 60 HZ screen refresh rate.
+
+Sample output:
+# ./fps_meter.py
+trace method: workq
+[111111111111111111111111111111111111111111111111111111111111] FPS: 60
+[111111111111111111111111111111111111111111111111111111111111] FPS: 60
+[11111111111111111111111111111111111111111111111 111111111111] FPS: 59
+[111111111111111111111111111111111111111111111111111111111111] FPS: 60
+[11111111111111 11111111111111111111 11111111 111111111111111] FPS: 57
+[111111111111111111111111111111111    11111111111111111111111] FPS: 56
+[111   111111111111111111111111111111111111111111111111111111] FPS: 57
+ ^
+ 1 : Frame update count detected in this 1/60 sec interval.
+"""
+import atexit
+import collections
+import os
+import re
+import threading
+import time
+
+TRACE_ROOT = '/sys/kernel/debug/tracing/'
+VBLANK_SWITCH = os.path.join(TRACE_ROOT, 'events/drm/drm_vblank_event/enable')
+FLIP_SWITCH = os.path.join(TRACE_ROOT, 'events/i915/i915_flip_complete/enable')
+WORKQ_SWITCH = os.path.join(
+    TRACE_ROOT, 'events/workqueue/workqueue_execute_start/enable')
+WORKQ_FILTER = os.path.join(
+    TRACE_ROOT, 'events/workqueue/workqueue_execute_start/filter')
+TRACE_SWITCH = os.path.join(TRACE_ROOT, 'tracing_on')
+TRACE_CLOCK = os.path.join(TRACE_ROOT, 'trace_clock')
+TRACE_LOG = os.path.join(TRACE_ROOT, 'trace')
+TRACE_PIPE = os.path.join(TRACE_ROOT, 'trace_pipe')
+TRACE_MARKER = os.path.join(TRACE_ROOT, 'trace_marker')
+REFRESH_RATE = 60
+NOTIFY_STRING = 'notify_collection'
+
+
+def is_intel_gpu():
+  return os.path.isdir('/sys/bus/pci/drivers/i915')
+
+
+def get_kernel_version():
+  with open('/proc/version') as fin:
+    m = re.match(r'Linux version (\d+.\d+).\d+', fin.read())
+    if m:
+      return m.group(1)
+  return 'unknown'
+
+
+def get_trace_method():
+  """Gets the FPS checking method.
+
+  Checks i915_flip_complete for Intel GPU on Kernel 3.18.
+  Checks intel_atomic_commit_work for Intel GPU on Kernel 4.4.
+  Checks drm_vblank_event otherwise.
+  """
+  if is_intel_gpu():
+    kernel_version = get_kernel_version()
+    if kernel_version == '4.4':
+      return 'workq'
+    elif kernel_version == '3.18':
+      return 'flip'
+  # Fallback.
+  return 'vblank'
+
+
+def write_to_file(content, filename):
+  with open(filename, 'w') as fout:
+    fout.write(content)
+
+
+def set_simple_switch(value, filename):
+  with open(filename) as fin:
+    orig = fin.read().strip()
+    atexit.register(write_to_file, orig, filename)
+  write_to_file(value, filename)
+
+
+def set_trace_clock():
+  # Set trace clock to mono time as chrome tracing in CrOS.
+  PREFERRED_TRACE_CLOCK = 'mono'
+  with open(TRACE_CLOCK) as trace_clock:
+    m = re.match(r'.*\[(\w+)\]', trace_clock.read())
+    if m:
+      orig_clock = m.group(1)
+      atexit.register(write_to_file, orig_clock, TRACE_CLOCK)
+  write_to_file(PREFERRED_TRACE_CLOCK, TRACE_CLOCK)
+
+
+def get_kernel_symbol_addr(symbol):
+  # Example line in kallsyms:
+  # ffffffffbc46cb03 T sys_exit
+  with open('/proc/kallsyms') as kallsyms:
+    for line in kallsyms:
+      items = line.split()
+      if items[2] == symbol:
+        addr = items[0]
+        return addr
+  return None
+
+
+def set_workq_filter(function_name):
+  addr = get_kernel_symbol_addr(function_name)
+  if addr:
+    filter = 'function == 0x%s' % addr
+    write_to_file(filter, WORKQ_FILTER)
+    # Sets to 0 to remove the filter.
+    atexit.register(write_to_file, '0', WORKQ_FILTER)
+
+
+def enable_tracing(trace_method):
+  if trace_method == 'workq':
+    set_simple_switch('1', WORKQ_SWITCH)
+    # There are many workqueue_execute_start events,
+    # filter to reduce CPU usage.
+    set_workq_filter('intel_atomic_commit_work')
+  elif trace_method == 'flip':
+    set_simple_switch('1', FLIP_SWITCH)
+  else:
+    set_simple_switch('1', VBLANK_SWITCH)
+
+  set_simple_switch('1', TRACE_SWITCH)
+  set_trace_clock()
+
+
+def notify_collection(period_sec=1.0):
+  next_notify_time = time.time() + period_sec
+  while True:
+    # Calling time.sleep(1) may suspend for more than 1 second.
+    # Sleeps until a specific time to avoid error accumulation.
+    sleep_time = next_notify_time - time.time()
+    next_notify_time += period_sec
+    # Skips if current time is larger than next_notify_time.
+    if sleep_time > 0:
+      time.sleep(sleep_time)
+      write_to_file(NOTIFY_STRING, TRACE_MARKER)
+
+
+def start_notify_thread():
+  notify_thread = threading.Thread(target=notify_collection)
+  notify_thread.daemon = True
+  notify_thread.start()
+
+
+def process_trace_buffer(trace_buffer, end_time):
+  # Checks all vblanks in the range [end_time - 1.0, end_time].
+  frame_info = []
+  step = 1.0 / REFRESH_RATE
+  step_time = end_time - 1.0 + step
+  fps_count = 0
+  for _ in range(REFRESH_RATE):
+    # Checks if there are vblanks in a refresh interval.
+    step_count = 0
+    while trace_buffer and trace_buffer[0] < step_time:
+      trace_buffer.popleft()
+      step_count += 1
+
+    # Each character represent an 1 / REFRESH_RATE interval.
+    if step_count:
+      if step_count >= 10:
+        frame_info.append('*')
+      else:
+        frame_info.append(str(step_count))
+      fps_count += 1
+    else:
+      frame_info.append(' ')
+    step_time += step
+
+  frame_info_str = ''.join(frame_info)
+  print '[%s] FPS: %2d' % (frame_info_str, fps_count)
+
+
+def main_loop(trace_method):
+  """Main loop to parse the trace.
+
+  There are 2 threads involved:
+  Main thread:
+    Using blocking read to get data from trace_pipe.
+  Notify thread: The main thread may wait indifinitely if there
+    is no new trace. Writes to the pipe once per second to avoid
+    the indefinite waiting.
+  """
+  start_notify_thread()
+
+  # Sample trace:
+  # <idle>-0  [000] dNh3 631.905961: drm_vblank_event: crtc=0, seq=65496
+  # <idle>-0  [000] d.h3 631.922681: drm_vblank_event: crtc=0, seq=65497
+  # fps_meter [003] ...1 632.393953: tracing_mark_write: notify_collection
+  # ...
+  re_notify = re.compile(
+      r'.* (\d+.\d+): tracing_mark_write: ' + NOTIFY_STRING)
+  if trace_method == 'workq':
+    re_trace = re.compile(
+        r'.* (\d+.\d+): workqueue_execute_start: work struct ([\da-f]+): '
+        'function intel_atomic_commit_work')
+  elif trace_method == 'flip':
+    re_trace = re.compile(
+        r'.* (\d+.\d+): i915_flip_complete: plane=(\d+), obj=([\da-f]+)')
+  else:
+    re_trace = re.compile(
+        r'.* (\d+.\d+): drm_vblank_event: crtc=(\d+), seq=(\d+)')
+
+  trace_buffer = collections.deque()
+  with open(TRACE_PIPE) as trace_pipe:
+    # The pipe may block a few seconds if using:
+    # for line in trace_pipe
+    while True:
+      line = trace_pipe.readline()
+      m_trace = re_trace.match(line)
+      if m_trace:
+        timestamp = float(m_trace.group(1))
+        trace_buffer.append(timestamp)
+      else:
+        m_notify = re_notify.match(line)
+        if m_notify:
+          timestamp = float(m_notify.group(1))
+          process_trace_buffer(trace_buffer, timestamp)
+
+
+if __name__ == '__main__':
+  trace_method = get_trace_method()
+  print 'trace method:', trace_method
+  enable_tracing(trace_method)
+  main_loop(trace_method)
+