288 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
| # 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)
 |