#!/usr/bin/python
#
# Copyright 2016 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.
#
"""Generates an HTML file with plot of buffer level in the audio thread log."""
import argparse
import collections
import logging
import string
page_content = string.Template("""
""")
Tag = collections.namedtuple('Tag', {'time', 'text', 'position', 'class_name'})
"""
The tuple for tags shown on the plot on certain time.
text is the tag to show, position is the tag position, which is one of
'start', 'middle', 'end', class_name is one of 'device', 'stream', 'fetch',
and 'wake' which will be their CSS class name.
"""
class EventData(object):
"""The base class of an event."""
def __init__(self, time, name):
"""Initializes an EventData.
@param time: A string for event time.
@param name: A string for event name.
"""
self.time = time
self.name = name
self._text = None
self._position = None
self._class_name = None
def GetTag(self):
"""Gets the tag for this event.
@returns: A Tag object. Returns None if no need to show tag.
"""
if self._text:
return Tag(
time=self.time, text=self._text, position=self._position,
class_name=self._class_name)
return None
class DeviceEvent(EventData):
"""Class for device event."""
def __init__(self, time, name, device):
"""Initializes a DeviceEvent.
@param time: A string for event time.
@param name: A string for event name.
@param device: A string for device index.
"""
super(DeviceEvent, self).__init__(time, name)
self.device = device
self._position = 'start'
self._class_name = 'device'
class DeviceRemovedEvent(DeviceEvent):
"""Class for device removed event."""
def __init__(self, time, name, device):
"""Initializes a DeviceRemovedEvent.
@param time: A string for event time.
@param name: A string for event name.
@param device: A string for device index.
"""
super(DeviceRemovedEvent, self).__init__(time, name, device)
self._text = 'Removed Device %s' % self.device
class DeviceAddedEvent(DeviceEvent):
"""Class for device added event."""
def __init__(self, time, name, device):
"""Initializes a DeviceAddedEvent.
@param time: A string for event time.
@param name: A string for event name.
@param device: A string for device index.
"""
super(DeviceAddedEvent, self).__init__(time, name, device)
self._text = 'Added Device %s' % self.device
class LevelEvent(DeviceEvent):
"""Class for device event with buffer level."""
def __init__(self, time, name, device, level):
"""Initializes a LevelEvent.
@param time: A string for event time.
@param name: A string for event name.
@param device: A string for device index.
@param level: An int for buffer level.
"""
super(LevelEvent, self).__init__(time, name, device)
self.level = level
class StreamEvent(EventData):
"""Class for event with stream."""
def __init__(self, time, name, stream):
"""Initializes a StreamEvent.
@param time: A string for event time.
@param name: A string for event name.
@param stream: A string for stream id.
"""
super(StreamEvent, self).__init__(time, name)
self.stream = stream
self._class_name = 'stream'
class FetchStreamEvent(StreamEvent):
"""Class for stream fetch event."""
def __init__(self, time, name, stream):
"""Initializes a FetchStreamEvent.
@param time: A string for event time.
@param name: A string for event name.
@param stream: A string for stream id.
"""
super(FetchStreamEvent, self).__init__(time, name, stream)
self._text = 'Fetch %s' % self.stream
self._position = 'end'
self._class_name = 'fetch'
class StreamAddedEvent(StreamEvent):
"""Class for stream added event."""
def __init__(self, time, name, stream):
"""Initializes a StreamAddedEvent.
@param time: A string for event time.
@param name: A string for event name.
@param stream: A string for stream id.
"""
super(StreamAddedEvent, self).__init__(time, name, stream)
self._text = 'Add stream %s' % self.stream
self._position = 'middle'
class StreamRemovedEvent(StreamEvent):
"""Class for stream removed event."""
def __init__(self, time, name, stream):
"""Initializes a StreamRemovedEvent.
@param time: A string for event time.
@param name: A string for event name.
@param stream: A string for stream id.
"""
super(StreamRemovedEvent, self).__init__(time, name, stream)
self._text = 'Remove stream %s' % self.stream
self._position = 'middle'
class WakeEvent(EventData):
"""Class for wake event."""
def __init__(self, time, name, num_fds):
"""Initializes a WakeEvent.
@param time: A string for event time.
@param name: A string for event name.
@param num_fds: A string for number of fd that wakes audio thread up.
"""
super(WakeEvent, self).__init__(time, name)
self._position = 'middle'
self._class_name = 'wake'
if num_fds != '0':
self._text = 'num_fds %s' % num_fds
class C3LogWriter(object):
"""Class to handle event data and fill an HTML page using c3.js library"""
def __init__(self):
"""Initializes a C3LogWriter."""
self.times = []
self.buffer_levels = []
self.tags = []
self.max_y = 0
def AddEvent(self, event):
"""Digests an event.
Add a tag if this event needs to be shown on grid.
Add a buffer level data into buffer_levels if this event has buffer
level.
@param event: An EventData object.
"""
tag = event.GetTag()
if tag:
self.tags.append(tag)
if isinstance(event, LevelEvent):
self.times.append(event.time)
self.buffer_levels.append(str(event.level))
if event.level > self.max_y:
self.max_y = event.level
logging.debug('add data for a level event %s: %s',
event.time, event.level)
if (isinstance(event, DeviceAddedEvent) or
isinstance(event, DeviceRemovedEvent)):
self.times.append(event.time)
self.buffer_levels.append('null')
def _GetGrids(self):
"""Gets the content to be filled for grids.
@returns: A str for grid with format:
'{value: time1, text: "tag1", position: "position1"},
{value: time1, text: "tag1"},...'
"""
grids = []
for tag in self.tags:
content = ('{value: %s, text: "%s", position: "%s", '
'class: "%s"}') % (
tag.time, tag.text, tag.position, tag.class_name)
grids.append(content)
grids_joined = ', '.join(grids)
return grids_joined
def FillPage(self, page_template):
"""Fills in the page template with content.
@param page_template: A string for HTML page content with variables
to be filled.
@returns: A string for filled page.
"""
times = ', '.join(self.times)
buffer_levels = ', '.join(self.buffer_levels)
grids = self._GetGrids()
filled = page_template.safe_substitute(
times=times,
buffer_levels=buffer_levels,
grids=grids,
max_y=str(self.max_y))
return filled
class EventLogParser(object):
"""Class for event log parser."""
def __init__(self):
"""Initializes an EventLogParse."""
self.parsed_events = []
def AddEventLog(self, event_log):
"""Digests a line of event log.
@param event_log: A line for event log.
"""
event = self._ParseOneLine(event_log)
if event:
self.parsed_events.append(event)
def GetParsedEvents(self):
"""Gets the list of parsed events.
@returns: A list of parsed EventData.
"""
return self.parsed_events
def _ParseOneLine(self, line):
"""Parses one line of event log.
Split a line like
169536.504763588 WRITE_STREAMS_FETCH_STREAM id:0 cbth:512 delay:1136
into time, name, and props where
time = '169536.504763588'
name = 'WRITE_STREAMS_FETCH_STREAM'
props = {
'id': 0,
'cb_th': 512,
'delay': 1136
}
@param line: A line of event log.
@returns: A EventData object.
"""
line_split = line.split()
time, name = line_split[0], line_split[1]
logging.debug('time: %s, name: %s', time, name)
props = {}
for index in xrange(2, len(line_split)):
key, value = line_split[index].split(':')
props[key] = value
logging.debug('props: %s', props)
return self._CreateEventData(time, name, props)
def _CreateEventData(self, time, name, props):
"""Creates an EventData based on event name.
@param time: A string for event time.
@param name: A string for event name.
@param props: A dict for event properties.
@returns: A EventData object.
"""
if name == 'WRITE_STREAMS_FETCH_STREAM':
return FetchStreamEvent(time, name, stream=props['id'])
if name == 'STREAM_ADDED':
return StreamAddedEvent(time, name, stream=props['id'])
if name == 'STREAM_REMOVED':
return StreamRemovedEvent(time, name, stream=props['id'])
if name in ['FILL_AUDIO', 'SET_DEV_WAKE']:
return LevelEvent(
time, name, device=props['dev'],
level=int(props['hw_level']))
if name == 'DEV_ADDED':
return DeviceAddedEvent(time, name, device=props['dev'])
if name == 'DEV_REMOVED':
return DeviceRemovedEvent(time, name, device=props['dev'])
if name == 'WAKE':
return WakeEvent(time, name, num_fds=props['num_fds'])
return None
class AudioThreadLogParser(object):
"""Class for audio thread log parser."""
def __init__(self, path):
"""Initializes an AudioThreadLogParser.
@param path: The audio thread log file path.
"""
self.path = path
self.content = None
def Parse(self):
"""Prases the audio thread logs.
@returns: A list of event log lines.
"""
logging.debug('Using file: %s', self.path)
with open(self.path, 'r') as f:
self.content = f.read().splitlines()
# Event logs starting at two lines after 'Audio Thread Event Log'.
index_start = self.content.index('Audio Thread Event Log:') + 2
# If input is from audio_diagnostic result, use aplay -l line to find
# the end of audio thread event logs.
try:
index_end = self.content.index('=== aplay -l ===')
except ValueError:
logging.debug(
'Can not find aplay line. This is not from diagnostic')
index_end = len(self.content)
event_logs = self.content[index_start:index_end]
logging.info('Parsed %s log events', len(event_logs))
return event_logs
def FillLogs(self, page_template):
"""Fills the HTML page template with contents for audio thread logs.
@param page_template: A string for HTML page content with log variable
to be filled.
@returns: A string for filled page.
"""
logs = '\n '.join(self.content)
return page_template.substitute(logs=logs)
def ParseArgs():
"""Parses the arguments.
@returns: The namespace containing parsed arguments.
"""
parser = argparse.ArgumentParser(
description='Draw time chart from audio thread log',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('FILE', type=str, help='The audio thread log file')
parser.add_argument('-o', type=str, dest='output',
default='view.html', help='The output HTML file')
parser.add_argument('-d', dest='debug', action='store_true',
default=False, help='Show debug message')
return parser.parse_args()
def Main():
"""The Main program."""
options = ParseArgs()
logging.basicConfig(
format='%(asctime)s:%(levelname)s:%(message)s',
level=logging.DEBUG if options.debug else logging.INFO)
# Gets lines of event logs.
audio_thread_log_parser = AudioThreadLogParser(options.FILE)
event_logs = audio_thread_log_parser.Parse()
# Parses event logs into events.
event_log_parser = EventLogParser()
for event_log in event_logs:
event_log_parser.AddEventLog(event_log)
events = event_log_parser.GetParsedEvents()
# Reads in events in preparation of filling HTML template.
c3_writer = C3LogWriter()
for event in events:
c3_writer.AddEvent(event)
# Fills in buffer level chart.
page_content_with_chart = c3_writer.FillPage(page_content)
# Fills in audio thread log into text box.
page_content_with_chart_and_logs = audio_thread_log_parser.FillLogs(
string.Template(page_content_with_chart))
with open(options.output, 'w') as f:
f.write(page_content_with_chart_and_logs)
if __name__ == '__main__':
Main()