You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
288 lines
9.6 KiB
288 lines
9.6 KiB
# Copyright (c) 2013 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.
|
|
|
|
import logging
|
|
import numpy
|
|
import os
|
|
import re
|
|
import threading
|
|
import time
|
|
|
|
TRACING_DIR = '/sys/kernel/debug/tracing'
|
|
BUFFER_SIZE_RUNNING = '5000'
|
|
BUFFER_SIZE_IDLE = '1408'
|
|
|
|
def tracing_write(file_name, msg):
|
|
"""Helper method to write a file in kernel debugfs.
|
|
|
|
@param file_name: The file to write in debugfs.
|
|
@param msg: The content to write.
|
|
"""
|
|
with open(os.path.join(TRACING_DIR, file_name), 'w') as f:
|
|
f.write(msg)
|
|
f.flush()
|
|
|
|
|
|
def enable_tracing(events_list=None):
|
|
"""Enable kernel tracing.
|
|
|
|
@param events_list: The list of events to monitor. Defaults to None to
|
|
monitor all events.
|
|
"""
|
|
tracing_write('trace_clock', 'global')
|
|
tracing_write('buffer_size_kb', BUFFER_SIZE_RUNNING)
|
|
if events_list:
|
|
tracing_write('set_event', '\n'.join(events_list))
|
|
tracing_write('tracing_on', '1')
|
|
|
|
|
|
def disable_tracing():
|
|
"""Disable kernel tracing."""
|
|
tracing_write('tracing_on', '0')
|
|
tracing_write('set_event', '')
|
|
tracing_write('trace', '0')
|
|
tracing_write('buffer_size_kb', BUFFER_SIZE_IDLE)
|
|
|
|
|
|
def get_trace_log():
|
|
"""Get kernel tracing log."""
|
|
with open(os.path.join(TRACING_DIR, 'trace'), 'r') as f:
|
|
return f.read()
|
|
|
|
|
|
class Sampler(object):
|
|
"""Base sampler class."""
|
|
|
|
def __init__(self, period, duration, events=None):
|
|
self.period = period
|
|
self.duration = duration
|
|
self.events = events or []
|
|
self.sampler_callback = None
|
|
self.stop_sampling = threading.Event()
|
|
self.sampling_thread = None
|
|
|
|
@property
|
|
def stopped(self):
|
|
"""Check if sampler is stopped."""
|
|
return self.stop_sampling.is_set()
|
|
|
|
def _do_sampling(self):
|
|
"""Main sampling loop."""
|
|
while True:
|
|
next_sampling_time = time.time() + self.period
|
|
try:
|
|
enable_tracing(events_list=self.events)
|
|
if self.stop_sampling.wait(self.duration):
|
|
return
|
|
self.parse_ftrace(get_trace_log())
|
|
self.sampler_callback(self)
|
|
finally:
|
|
disable_tracing()
|
|
self.reset()
|
|
if self.stop_sampling.wait(next_sampling_time - time.time()):
|
|
return
|
|
|
|
def start_sampling_thread(self):
|
|
"""Start a thread to sample events."""
|
|
if not self.sampler_callback:
|
|
raise RuntimeError('Sampler callback function is not set')
|
|
self.stop_sampling.clear()
|
|
self.sampling_thread = threading.Thread(target=self._do_sampling)
|
|
self.sampling_thread.daemon = True
|
|
self.sampling_thread.start()
|
|
|
|
def stop_sampling_thread(self):
|
|
"""Stop sampling thread."""
|
|
self.stop_sampling.set()
|
|
self.reset()
|
|
|
|
def parse_ftrace(self, data):
|
|
"""Parse ftrace data.
|
|
|
|
@param data: The ftrace data to parse.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def reset(self):
|
|
"""Reset the sampler."""
|
|
raise NotImplementedError
|
|
|
|
|
|
class ExynosSampler(Sampler):
|
|
"""Sampler for Exynos platform."""
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs['events'] = ['exynos_page_flip_state']
|
|
super(ExynosSampler, self).__init__(*args, **kwargs)
|
|
self.frame_buffers = {}
|
|
|
|
def reset(self):
|
|
self.frame_buffers.clear()
|
|
|
|
def parse_ftrace(self, data):
|
|
"""Read and parse the ftrace file"""
|
|
# Parse using RE and named group match (?P<xxx>yyy) for clarity.
|
|
# Format:
|
|
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
|
|
# X-007 [001] .... 001.000001: (a line-wrap in Python here ...)
|
|
# exynos_page_flip_state: pipe=0, fb=25, state=wait_kds
|
|
re_pattern = (
|
|
'\s*(?P<task>.+)-' + # task
|
|
'(?P<pid>\d+)\s+' + # pid
|
|
'\[(?P<cpu>\d+)\]\s+' + # cpu#
|
|
'(?P<inhp>....)\s+' + # inhp: irqs-off, need-resched,
|
|
# hardirq/softirq, preempt-depth
|
|
'(?P<timestamp>\d+\.\d+):\s+' + # timestamp
|
|
'(?P<head>exynos_page_flip_state):\s+' + # head
|
|
'(?P<detail>.*)') # detail: 'pipe=0, fb=25, state=wait_kds'
|
|
|
|
for line in data.split('\n'):
|
|
m = re.match(re_pattern, line)
|
|
if m is None: # not a valid trace line (e.g. comment)
|
|
continue
|
|
m_dict = m.groupdict() # named group of RE match (m_dict['task']...)
|
|
pipe, fb, state = self.exynos_parser(m_dict['detail'])
|
|
timestamp = float(m_dict['timestamp'])
|
|
self.end_last_fb_state(pipe, fb, timestamp)
|
|
self.start_new_fb_state(pipe, fb, state, timestamp)
|
|
|
|
self.calc_stat()
|
|
|
|
def exynos_parser(self, detail):
|
|
"""Parse exynos event's detail.
|
|
|
|
@param detail: a string like 'pipe=0, fb=25, state=wait_kds'
|
|
@return: tuple of (pipe, fb, state), pipe and fb in int,
|
|
state in string
|
|
"""
|
|
re_pattern = (
|
|
'pipe=(?P<pipe>\d+), ' + # pipe
|
|
'fb=(?P<fb>\d+), ' + # fb
|
|
'state=(?P<state>.*)') # state
|
|
if re.match(re_pattern, detail) is None:
|
|
logging.debug('==fail==' + re_pattern + ', ' + detail)
|
|
m_dict = re.match(re_pattern, detail).groupdict()
|
|
return int(m_dict['pipe']), int(m_dict['fb']), m_dict['state']
|
|
|
|
def end_last_fb_state(self, pipe, fb, end_time):
|
|
"""End the currently opened state of the specified frame buffer
|
|
|
|
@param pipe: the pipe id
|
|
@param fb: the frame buffer id
|
|
@param end_time: timestamp when the state ends
|
|
"""
|
|
self.get_frame_buffer(pipe, fb).end_state(end_time)
|
|
|
|
def start_new_fb_state(self, pipe, fb, state, start_time):
|
|
"""Start the specified state on the specified frame buffer
|
|
|
|
@param pipe: the pipe id
|
|
@param fb: the frame buffer id
|
|
@param state: which state to start
|
|
@param start_time: timestamp when the state starts
|
|
"""
|
|
self.get_frame_buffer(pipe, fb).start_state(state, start_time)
|
|
|
|
def calc_stat(self):
|
|
"""Calculate the statistics of state duration of all frame buffers"""
|
|
for fb in self.frame_buffers.values():
|
|
fb.calc_state_avg_stdev()
|
|
|
|
def frame_buffer_unique_hash(self, pipe, fb):
|
|
"""A hash function that returns the unique identifier of a frame buffer.
|
|
The hash is a string that is unique in the sense of pipe and fb.
|
|
|
|
@param pipe: the pipe id
|
|
@param fb: the frame buffer id
|
|
@return: a unique hash string, like "pipe:0,fb:25"
|
|
"""
|
|
return "pipe:%d,fb:%d" % (pipe, fb)
|
|
|
|
def get_frame_buffer(self, pipe, fb):
|
|
"""Return the frame buffer with specified pipe and fb.
|
|
If the frame buffer does not exist, create one and return it.
|
|
|
|
@param pipe: the pipe id
|
|
@param fb: the frame buffer id
|
|
@return: the frame buffer specified by pipe and fb
|
|
"""
|
|
key = self.frame_buffer_unique_hash(pipe, fb)
|
|
if key not in self.frame_buffers:
|
|
self.frame_buffers[key] = FrameBuffer(pipe, fb)
|
|
return self.frame_buffers[key]
|
|
|
|
|
|
class FrameBuffer():
|
|
"""Represents a frame buffer, which holds all its states"""
|
|
def __init__(self, pipe, fb):
|
|
"""Initialize the frame buffer.
|
|
|
|
@param pipe: the pipe id of the frame buffer
|
|
@param fb: the fb id of the frame buffer
|
|
"""
|
|
self.pipe = pipe
|
|
self.fb = fb
|
|
self.states = {}
|
|
self.active_state = None # currently active state (to be ended later)
|
|
|
|
def start_state(self, state_name, start_time):
|
|
"""Start the specified state
|
|
|
|
@param state_name: name of the state to be started
|
|
@param start_time: timestamp when the state starts
|
|
"""
|
|
if state_name not in self.states:
|
|
self.states[state_name] = State(state_name)
|
|
self.states[state_name].start(start_time)
|
|
self.active_state = state_name
|
|
|
|
def end_state(self, end_time):
|
|
"""End the specified state, in which the duration will be stored
|
|
|
|
@param end_time: timestamp when the state ends
|
|
"""
|
|
if self.active_state is not None:
|
|
self.states[self.active_state].end(end_time)
|
|
self.active_state = None
|
|
|
|
def calc_state_avg_stdev(self):
|
|
"""Call all states to compute its own average and standard deviation"""
|
|
logging.debug("====pipe:%d, fb:%d====", self.pipe, self.fb)
|
|
for s in self.states.values():
|
|
s.calc_avg_stdev()
|
|
|
|
|
|
class State():
|
|
"""Hold the data of a specific state (e.g. wait_kds, wait_apply, ...)"""
|
|
def __init__(self, state_name):
|
|
"""Initialize data
|
|
|
|
@param state_name: name of this state
|
|
"""
|
|
self.state_name = state_name
|
|
self.last_start_time = None
|
|
self.duration_data = []
|
|
self.avg = None
|
|
self.stdev = None
|
|
|
|
def start(self, start_time):
|
|
"""Mark this state as started by saving the start time
|
|
|
|
@param start_time: timestamp when the state starts
|
|
"""
|
|
self.last_start_time = start_time
|
|
|
|
def end(self, end_time):
|
|
"""Save the state's duration and mark this state as ended
|
|
|
|
@param end_time: timestamp when the state ends
|
|
"""
|
|
if self.last_start_time is not None:
|
|
self.duration_data.append(end_time - self.last_start_time)
|
|
self.last_start_time = None
|
|
|
|
def calc_avg_stdev(self):
|
|
"""Calculate the average and standard deviation of all duration data"""
|
|
self.avg = numpy.mean(self.duration_data)
|
|
self.stdev = numpy.std(self.duration_data)
|