• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2#
3# Copyright 2016 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6#
7
8"""Generates an HTML file with plot of buffer level in the audio thread log."""
9
10import argparse
11import collections
12import logging
13import string
14
15page_content = string.Template("""
16<html meta charset="UTF8">
17<head>
18  <!-- Load c3.css -->
19  <link href="https://rawgit.com/masayuki0812/c3/master/c3.css" rel="stylesheet" type="text/css">
20  <!-- Load d3.js and c3.js -->
21  <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
22  <script src="https://rawgit.com/masayuki0812/c3/master/c3.js" charset="utf-8"></script>
23  <style type="text/css">
24    .c3-grid text {
25        fill: grey;
26    }
27    .event_log_box {
28      font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
29      font-size: 20px;
30      font-style: normal;
31      font-variant: normal;
32      font-weight: 300;
33      line-height: 26.4px;
34      white-space: pre;
35      height:50%;
36      width:48%;
37      border:1px solid #ccc;
38      overflow:auto;
39    }
40    .checkbox {
41      font-size: 30px;
42      border: 2px;
43    }
44    .device {
45      font-size: 15px;
46    }
47    .stream{
48      font-size: 15px;
49    }
50    .fetch{
51    }
52    .wake{
53    }
54  </style>
55  <script type="text/javascript">
56    draw_chart = function() {
57      var chart = c3.generate({
58        data: {
59          x: 'time',
60          columns: [
61              ['time',                   $times],
62              ['buffer_level',           $buffer_levels],
63          ],
64          type: 'bar',
65          types: {
66              buffer_level: 'line',
67          },
68        },
69        zoom: {
70          enabled: true,
71        },
72
73        grid: {
74          x: {
75            lines: [
76              $grids,
77            ],
78          },
79        },
80
81        axis: {
82          y: {min: 0, max: $max_y},
83        },
84      });
85    };
86
87    logs = `$logs`;
88    put_logs = function () {
89      document.getElementById('logs').innerHTML = logs;
90    };
91
92    set_initial_checkbox_value = function () {
93      document.getElementById('device').checked = true;
94      document.getElementById('stream').checked = true;
95      document.getElementById('fetch').checked = true;
96      document.getElementById('wake').checked = true;
97    }
98
99    window.onload = function() {
100      draw_chart();
101      put_logs();
102      set_initial_checkbox_value();
103    };
104
105    function handleClick(checkbox) {
106      var class_name = checkbox.id;
107      var elements = document.getElementsByClassName(class_name);
108      var i;
109
110      if (checkbox.checked) {
111        display_value = "block";
112      } else {
113        display_value = "none"
114      }
115
116      console.log("change " + class_name + " to " + display_value);
117      for (i = 0; i < elements.length; i++) {
118        elements[i].style.display = display_value;
119      }
120    }
121
122  </script>
123</head>
124
125<body>
126  <div id="chart" style="height:50%; width:100%" ></div>
127  <div style="margin:0 auto"; class="checkbox">
128      <label><input type="checkbox" onclick="handleClick(this);" id="device">Show device removed/added event</label>
129      <label><input type="checkbox" onclick="handleClick(this);" id="stream">Show stream removed/added event</label>
130      <label><input type="checkbox" onclick="handleClick(this);" id="fetch">Show fetch event</label>
131      <label><input type="checkbox" onclick="handleClick(this);" id="wake">Show wake by num_fds=1 event</label>
132  </div>
133  <div class="event_log_box", id="logs", style="float:left;"></div>
134  <textarea class="event_log_box", id="text", style="float:right;"></textarea>
135</body>
136</html>
137""")
138
139
140Tag = collections.namedtuple('Tag', {'time', 'text', 'position', 'class_name'})
141"""
142The tuple for tags shown on the plot on certain time.
143text is the tag to show, position is the tag position, which is one of
144'start', 'middle', 'end', class_name is one of 'device', 'stream', 'fetch',
145and 'wake' which will be their CSS class name.
146"""
147
148class EventData(object):
149    """The base class of an event."""
150    def __init__(self, time, name):
151        """Initializes an EventData.
152
153        @param time: A string for event time.
154        @param name: A string for event name.
155
156        """
157        self.time = time
158        self.name = name
159        self._text = None
160        self._position = None
161        self._class_name = None
162
163    def GetTag(self):
164        """Gets the tag for this event.
165
166        @returns: A Tag object. Returns None if no need to show tag.
167
168        """
169        if self._text:
170            return Tag(
171                    time=self.time, text=self._text, position=self._position,
172                    class_name=self._class_name)
173        return None
174
175
176class DeviceEvent(EventData):
177    """Class for device event."""
178    def __init__(self, time, name, device):
179        """Initializes a DeviceEvent.
180
181        @param time: A string for event time.
182        @param name: A string for event name.
183        @param device: A string for device index.
184
185        """
186        super(DeviceEvent, self).__init__(time, name)
187        self.device = device
188        self._position = 'start'
189        self._class_name = 'device'
190
191
192class DeviceRemovedEvent(DeviceEvent):
193    """Class for device removed event."""
194    def __init__(self, time, name, device):
195        """Initializes a DeviceRemovedEvent.
196
197        @param time: A string for event time.
198        @param name: A string for event name.
199        @param device: A string for device index.
200
201        """
202        super(DeviceRemovedEvent, self).__init__(time, name, device)
203        self._text = 'Removed Device %s' % self.device
204
205
206class DeviceAddedEvent(DeviceEvent):
207    """Class for device added event."""
208    def __init__(self, time, name, device):
209        """Initializes a DeviceAddedEvent.
210
211        @param time: A string for event time.
212        @param name: A string for event name.
213        @param device: A string for device index.
214
215        """
216        super(DeviceAddedEvent, self).__init__(time, name, device)
217        self._text = 'Added Device %s' % self.device
218
219
220class LevelEvent(DeviceEvent):
221    """Class for device event with buffer level."""
222    def __init__(self, time, name, device, level):
223        """Initializes a LevelEvent.
224
225        @param time: A string for event time.
226        @param name: A string for event name.
227        @param device: A string for device index.
228        @param level: An int for buffer level.
229
230        """
231        super(LevelEvent, self).__init__(time, name, device)
232        self.level = level
233
234
235class StreamEvent(EventData):
236    """Class for event with stream."""
237    def __init__(self, time, name, stream):
238        """Initializes a StreamEvent.
239
240        @param time: A string for event time.
241        @param name: A string for event name.
242        @param stream: A string for stream id.
243
244        """
245        super(StreamEvent, self).__init__(time, name)
246        self.stream = stream
247        self._class_name = 'stream'
248
249
250class FetchStreamEvent(StreamEvent):
251    """Class for stream fetch event."""
252    def __init__(self, time, name, stream):
253        """Initializes a FetchStreamEvent.
254
255        @param time: A string for event time.
256        @param name: A string for event name.
257        @param stream: A string for stream id.
258
259        """
260        super(FetchStreamEvent, self).__init__(time, name, stream)
261        self._text = 'Fetch %s' % self.stream
262        self._position = 'end'
263        self._class_name = 'fetch'
264
265
266class StreamAddedEvent(StreamEvent):
267    """Class for stream added event."""
268    def __init__(self, time, name, stream):
269        """Initializes a StreamAddedEvent.
270
271        @param time: A string for event time.
272        @param name: A string for event name.
273        @param stream: A string for stream id.
274
275        """
276        super(StreamAddedEvent, self).__init__(time, name, stream)
277        self._text = 'Add stream %s' % self.stream
278        self._position = 'middle'
279
280
281class StreamRemovedEvent(StreamEvent):
282    """Class for stream removed event."""
283    def __init__(self, time, name, stream):
284        """Initializes a StreamRemovedEvent.
285
286        @param time: A string for event time.
287        @param name: A string for event name.
288        @param stream: A string for stream id.
289
290        """
291        super(StreamRemovedEvent, self).__init__(time, name, stream)
292        self._text = 'Remove stream %s' % self.stream
293        self._position = 'middle'
294
295
296class WakeEvent(EventData):
297    """Class for wake event."""
298    def __init__(self, time, name, num_fds):
299        """Initializes a WakeEvent.
300
301        @param time: A string for event time.
302        @param name: A string for event name.
303        @param num_fds: A string for number of fd that wakes audio thread up.
304
305        """
306        super(WakeEvent, self).__init__(time, name)
307        self._position = 'middle'
308        self._class_name = 'wake'
309        if num_fds != '0':
310            self._text = 'num_fds %s' % num_fds
311
312
313class C3LogWriter(object):
314    """Class to handle event data and fill an HTML page using c3.js library"""
315    def __init__(self):
316        """Initializes a C3LogWriter."""
317        self.times = []
318        self.buffer_levels = []
319        self.tags = []
320        self.max_y = 0
321
322    def AddEvent(self, event):
323        """Digests an event.
324
325        Add a tag if this event needs to be shown on grid.
326        Add a buffer level data into buffer_levels if this event has buffer
327        level.
328
329        @param event: An EventData object.
330
331        """
332        tag = event.GetTag()
333        if tag:
334            self.tags.append(tag)
335
336        if isinstance(event, LevelEvent):
337            self.times.append(event.time)
338            self.buffer_levels.append(str(event.level))
339            if event.level > self.max_y:
340                self.max_y = event.level
341            logging.debug('add data for a level event %s: %s',
342                          event.time, event.level)
343
344        if (isinstance(event, DeviceAddedEvent) or
345            isinstance(event, DeviceRemovedEvent)):
346            self.times.append(event.time)
347            self.buffer_levels.append('null')
348
349    def _GetGrids(self):
350        """Gets the content to be filled for grids.
351
352        @returns: A str for grid with format:
353           '{value: time1, text: "tag1", position: "position1"},
354            {value: time1, text: "tag1"},...'
355
356        """
357        grids = []
358        for tag in self.tags:
359            content = ('{value: %s, text: "%s", position: "%s", '
360                       'class: "%s"}') % (
361                              tag.time, tag.text, tag.position, tag.class_name)
362            grids.append(content)
363        grids_joined = ', '.join(grids)
364        return grids_joined
365
366    def FillPage(self, page_template):
367        """Fills in the page template with content.
368
369        @param page_template: A string for HTML page content with variables
370                              to be filled.
371
372        @returns: A string for filled page.
373
374        """
375        times = ', '.join(self.times)
376        buffer_levels = ', '.join(self.buffer_levels)
377        grids = self._GetGrids()
378        filled = page_template.safe_substitute(
379                times=times,
380                buffer_levels=buffer_levels,
381                grids=grids,
382                max_y=str(self.max_y))
383        return filled
384
385
386class EventLogParser(object):
387    """Class for event log parser."""
388    def __init__(self):
389        """Initializes an EventLogParse."""
390        self.parsed_events = []
391
392    def AddEventLog(self, event_log):
393        """Digests a line of event log.
394
395        @param event_log: A line for event log.
396
397        """
398        event = self._ParseOneLine(event_log)
399        if event:
400            self.parsed_events.append(event)
401
402    def GetParsedEvents(self):
403        """Gets the list of parsed events.
404
405        @returns: A list of parsed EventData.
406
407        """
408        return self.parsed_events
409
410    def _ParseOneLine(self, line):
411        """Parses one line of event log.
412
413        Split a line like
414        169536.504763588  WRITE_STREAMS_FETCH_STREAM     id:0 cbth:512 delay:1136
415        into time, name, and props where
416        time = '169536.504763588'
417        name = 'WRITE_STREAMS_FETCH_STREAM'
418        props = {
419            'id': 0,
420            'cb_th': 512,
421            'delay': 1136
422        }
423
424        @param line: A line of event log.
425
426        @returns: A EventData object.
427
428        """
429        line_split = line.split()
430        time, name = line_split[0], line_split[1]
431        logging.debug('time: %s, name: %s', time, name)
432        props = {}
433        for index in xrange(2, len(line_split)):
434            key, value = line_split[index].split(':')
435            props[key] = value
436        logging.debug('props: %s', props)
437        return self._CreateEventData(time, name, props)
438
439    def _CreateEventData(self, time, name, props):
440        """Creates an EventData based on event name.
441
442        @param time: A string for event time.
443        @param name: A string for event name.
444        @param props: A dict for event properties.
445
446        @returns: A EventData object.
447
448        """
449        if name == 'WRITE_STREAMS_FETCH_STREAM':
450            return FetchStreamEvent(time, name, stream=props['id'])
451        if name == 'STREAM_ADDED':
452            return StreamAddedEvent(time, name, stream=props['id'])
453        if name == 'STREAM_REMOVED':
454            return StreamRemovedEvent(time, name, stream=props['id'])
455        if name in ['FILL_AUDIO', 'SET_DEV_WAKE']:
456            return LevelEvent(
457                    time, name, device=props['dev'],
458                    level=int(props['hw_level']))
459        if name == 'DEV_ADDED':
460            return DeviceAddedEvent(time, name, device=props['dev'])
461        if name == 'DEV_REMOVED':
462            return DeviceRemovedEvent(time, name, device=props['dev'])
463        if name == 'WAKE':
464            return WakeEvent(time, name, num_fds=props['num_fds'])
465        return None
466
467
468class AudioThreadLogParser(object):
469    """Class for audio thread log parser."""
470    def __init__(self, path):
471        """Initializes an AudioThreadLogParser.
472
473        @param path: The audio thread log file path.
474
475        """
476        self.path = path
477        self.content = None
478
479    def Parse(self):
480        """Prases the audio thread logs.
481
482        @returns: A list of event log lines.
483
484        """
485        logging.debug('Using file: %s', self.path)
486        with open(self.path, 'r') as f:
487            self.content = f.read().splitlines()
488
489        # Event logs starting at two lines after 'Audio Thread Event Log'.
490        index_start = self.content.index('Audio Thread Event Log:') + 2
491        # If input is from audio_diagnostic result, use aplay -l line to find
492        # the end of audio thread event logs.
493        try:
494            index_end = self.content.index('=== aplay -l ===')
495        except ValueError:
496            logging.debug(
497                    'Can not find aplay line. This is not from diagnostic')
498            index_end = len(self.content)
499        event_logs = self.content[index_start:index_end]
500        logging.info('Parsed %s log events', len(event_logs))
501        return event_logs
502
503    def FillLogs(self, page_template):
504        """Fills the HTML page template with contents for audio thread logs.
505
506        @param page_template: A string for HTML page content with log variable
507                              to be filled.
508
509        @returns: A string for filled page.
510
511        """
512        logs = '\n<br>'.join(self.content)
513        return page_template.substitute(logs=logs)
514
515
516def ParseArgs():
517    """Parses the arguments.
518
519    @returns: The namespace containing parsed arguments.
520
521    """
522    parser = argparse.ArgumentParser(
523            description='Draw time chart from audio thread log',
524            formatter_class=argparse.ArgumentDefaultsHelpFormatter)
525    parser.add_argument('FILE', type=str, help='The audio thread log file')
526    parser.add_argument('-o', type=str, dest='output',
527                        default='view.html', help='The output HTML file')
528    parser.add_argument('-d', dest='debug', action='store_true',
529                        default=False, help='Show debug message')
530    return parser.parse_args()
531
532
533def Main():
534    """The Main program."""
535    options = ParseArgs()
536    logging.basicConfig(
537            format='%(asctime)s:%(levelname)s:%(message)s',
538            level=logging.DEBUG if options.debug else logging.INFO)
539
540    # Gets lines of event logs.
541    audio_thread_log_parser = AudioThreadLogParser(options.FILE)
542    event_logs = audio_thread_log_parser.Parse()
543
544    # Parses event logs into events.
545    event_log_parser = EventLogParser()
546    for event_log in event_logs:
547        event_log_parser.AddEventLog(event_log)
548    events = event_log_parser.GetParsedEvents()
549
550    # Reads in events in preparation of filling HTML template.
551    c3_writer = C3LogWriter()
552    for event in events:
553        c3_writer.AddEvent(event)
554
555    # Fills in buffer level chart.
556    page_content_with_chart = c3_writer.FillPage(page_content)
557
558    # Fills in audio thread log into text box.
559    page_content_with_chart_and_logs = audio_thread_log_parser.FillLogs(
560            string.Template(page_content_with_chart))
561
562    with open(options.output, 'w') as f:
563        f.write(page_content_with_chart_and_logs)
564
565
566if __name__ == '__main__':
567    Main()
568