#!/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()