| # 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) |