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