Home | History | Annotate | Download | only in net_internals
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * This visualizer displays the log in a timeline graph
      7  *
      8  *   - Use HTML5 canvas
      9  *   - Can zoom in result by select time range
     10  *   - Display different levels of logs in different layers of canvases
     11  *
     12  */
     13 var CrosLogVisualizer = (function() {
     14   'use strict';
     15 
     16   // HTML attributes of canvas
     17   var LOG_VISUALIZER_CANVAS_CLASS = 'cros-log-analyzer-visualizer-canvas';
     18   var LOG_VISUALIZER_CANVAS_WIDTH = 980;
     19   var LOG_VISUALIZER_CANVAS_HEIGHT = 100;
     20 
     21   // Special HTML classes
     22   var LOG_VISUALIZER_TIMELINE_ID = 'cros-log-analyzer-visualizer-timeline';
     23   var LOG_VISUALIZER_TIME_DISPLAY_CLASS =
     24       'cros-log-analyzer-visualizer-time-display';
     25   var LOG_VISUALIZER_RESET_BTN_ID =
     26       'cros-log-analyzer-visualizer-reset-btn';
     27   var LOG_VISUALIZER_TRACKING_LAYER_ID =
     28       'cros-log-analyzer-visualizer-tracking-layer';
     29 
     30   /**
     31    * Event level list
     32    * This list is used for intialization of canvases. And the canvas
     33    * with lowest priority should be created first. Hence the list is
     34    * sorted in decreasing order.
     35    */
     36   var LOG_EVENT_LEVEL_PRIORITY_LIST = {
     37     'Unknown': 4,
     38     'Warning': 2,
     39     'Info': 3,
     40     'Error': 1
     41   };
     42 
     43   // Color mapping of different levels
     44   var LOG_EVENT_COLORS_LIST = {
     45     'Error': '#FF99A3',
     46     'Warning': '#FAE5C3',
     47     'Info': '#C3E3FA',
     48     'Unknown': 'gray'
     49   };
     50 
     51   /**
     52    * @constructor
     53    */
     54   function CrosLogVisualizer(logAnalyzer, containerID) {
     55     /**
     56      * Pass the LogAnalyzer in as a reference so the visualizer can
     57      * synchrous with the log filter.
     58      */
     59     this.logAnalyzer = logAnalyzer;
     60 
     61     // If the data is initialized
     62     this.dataIntialized = false;
     63     // Stores all the log entries as events
     64     this.events = [];
     65     // A front layer that handles control events
     66     this.trackingLayer = this.createTrackingLayer();
     67 
     68     // References to HTML elements
     69     this.container = document.getElementById(containerID);
     70     this.timeline = this.createTimeline();
     71     this.timeDisplay = this.createTimeDisplay();
     72     this.btnReset = this.createBtnReset();
     73     // Canvases
     74     this.canvases = {};
     75     for (var level in LOG_EVENT_LEVEL_PRIORITY_LIST) {
     76       this.canvases[level] = this.createCanvas();
     77       this.container.appendChild(this.canvases[level]);
     78     }
     79 
     80     // Append all the elements to the container
     81     this.container.appendChild(this.timeline);
     82     this.container.appendChild(this.timeDisplay);
     83     this.container.appendChild(this.trackingLayer);
     84     this.container.appendChild(this.btnReset);
     85 
     86     this.container.addEventListener('webkitAnimationEnd', function() {
     87       this.container.classList.remove('cros-log-analyzer-flash');
     88     }.bind(this), false);
     89   }
     90 
     91   CrosLogVisualizer.prototype = {
     92     /**
     93      * Called during the initialization of the View. Create a overlay
     94      * DIV on top of the canvas that handles the mouse events
     95      */
     96     createTrackingLayer: function() {
     97       var trackingLayer = document.createElement('div');
     98       trackingLayer.setAttribute('id', LOG_VISUALIZER_TRACKING_LAYER_ID);
     99       trackingLayer.addEventListener('mousemove', this.onHovered_.bind(this));
    100       trackingLayer.addEventListener('mousedown', this.onMouseDown_.bind(this));
    101       trackingLayer.addEventListener('mouseup', this.onMouseUp_.bind(this));
    102       return trackingLayer;
    103     },
    104 
    105     /**
    106      * This function is called during the initialization of the view.
    107      * It creates the timeline that moves along with the mouse on canvas.
    108      * When user click, a rectangle can be dragged out to select the range
    109      * to zoom.
    110      */
    111     createTimeline: function() {
    112       var timeline = document.createElement('div');
    113       timeline.setAttribute('id', LOG_VISUALIZER_TIMELINE_ID);
    114       timeline.style.height = LOG_VISUALIZER_CANVAS_HEIGHT + 'px';
    115       timeline.addEventListener('mousedown', function(event) { return false; });
    116       return timeline;
    117     },
    118 
    119     /**
    120      * This function is called during the initialization of the view.
    121      * It creates a time display that moves with the timeline
    122      */
    123     createTimeDisplay: function() {
    124       var timeDisplay = document.createElement('p');
    125       timeDisplay.className = LOG_VISUALIZER_TIME_DISPLAY_CLASS;
    126       timeDisplay.style.top = LOG_VISUALIZER_CANVAS_HEIGHT + 'px';
    127       return timeDisplay;
    128     },
    129 
    130     /**
    131      * Called during the initialization of the View. Create a button that
    132      * resets the canvas to initial status (without zoom)
    133      */
    134     createBtnReset: function() {
    135       var btnReset = document.createElement('input');
    136       btnReset.setAttribute('type', 'button');
    137       btnReset.setAttribute('value', 'Reset');
    138       btnReset.setAttribute('id', LOG_VISUALIZER_RESET_BTN_ID);
    139       btnReset.addEventListener('click', this.reset.bind(this));
    140       return btnReset;
    141     },
    142 
    143     /**
    144      * Called during the initialization of the View. Create a empty canvas
    145      * that visualizes log when the data is ready
    146      */
    147     createCanvas: function() {
    148       var canvas = document.createElement('canvas');
    149       canvas.width = LOG_VISUALIZER_CANVAS_WIDTH;
    150       canvas.height = LOG_VISUALIZER_CANVAS_HEIGHT;
    151       canvas.className = LOG_VISUALIZER_CANVAS_CLASS;
    152       return canvas;
    153     },
    154 
    155     /**
    156      * Returns the context of corresponding canvas based on level
    157      */
    158     getContext: function(level) {
    159       return this.canvases[level].getContext('2d');
    160     },
    161 
    162     /**
    163      * Erases everything from all the canvases
    164      */
    165     clearCanvas: function() {
    166       for (var level in LOG_EVENT_LEVEL_PRIORITY_LIST) {
    167         var ctx = this.getContext(level);
    168         ctx.clearRect(0, 0, LOG_VISUALIZER_CANVAS_WIDTH,
    169             LOG_VISUALIZER_CANVAS_HEIGHT);
    170       }
    171     },
    172 
    173     /**
    174      * Initializes the parameters needed for drawing:
    175      *  - lower/upperBound: Time range (Events out of range will be skipped)
    176      *  - totalDuration: The length of time range
    177      *  - unitDuration: The unit time length per pixel
    178      */
    179     initialize: function() {
    180       if (this.events.length == 0)
    181         return;
    182       this.dragMode = false;
    183       this.dataIntialized = true;
    184       this.events.sort(this.compareTime);
    185       this.lowerBound = this.events[0].time;
    186       this.upperBound = this.events[this.events.length - 1].time;
    187       this.totalDuration = Math.abs(this.upperBound.getTime() -
    188           this.lowerBound.getTime());
    189       this.unitDuration = this.totalDuration / LOG_VISUALIZER_CANVAS_WIDTH;
    190     },
    191 
    192     /**
    193      * CSS3 fadeIn/fadeOut effects
    194      */
    195     flashEffect: function() {
    196       this.container.classList.add('cros-log-analyzer-flash');
    197     },
    198 
    199     /**
    200      * Reset the canvas to the initial time range
    201      * Redraw everything on the canvas
    202      * Fade in/out effects while redrawing
    203      */
    204     reset: function() {
    205       // Reset all the parameters as initial
    206       this.initialize();
    207       // Reset the visibility of the entries in the log table
    208       this.logAnalyzer.filterLog();
    209       this.flashEffect();
    210      },
    211 
    212     /**
    213      * A wrapper function for drawing
    214      */
    215     drawEvents: function() {
    216       if (this.events.length == 0)
    217         return;
    218       for (var i in this.events) {
    219         this.drawEvent(this.events[i]);
    220       }
    221     },
    222 
    223     /**
    224      * The main function that handles drawing on the canvas.
    225      * Every event is represented as a vertical line.
    226      */
    227     drawEvent: function(event) {
    228       if (!event.visibility) {
    229         // Skip hidden events
    230         return;
    231       }
    232       var ctx = this.getContext(event.level);
    233       ctx.beginPath();
    234       // Get the x-coordinate of the line
    235       var startPosition = this.getPosition(event.time);
    236       if (startPosition != this.old) {
    237         this.old = startPosition;
    238       }
    239       ctx.rect(startPosition, 0, 2, LOG_VISUALIZER_CANVAS_HEIGHT);
    240       // Get the color of the line
    241       ctx.fillStyle = LOG_EVENT_COLORS_LIST[event.level];
    242       ctx.fill();
    243       ctx.closePath();
    244     },
    245 
    246     /**
    247      * This function is called every time the graph is zoomed.
    248      * It recalculates all the parameters based on the distance and direction
    249      * of dragging.
    250      */
    251     reCalculate: function() {
    252       if (this.dragDistance >= 0) {
    253         // if user drags to right
    254         this.upperBound = new Date((this.timelineLeft + this.dragDistance) *
    255             this.unitDuration + this.lowerBound.getTime());
    256         this.lowerBound = new Date(this.timelineLeft * this.unitDuration +
    257             this.lowerBound.getTime());
    258       } else {
    259         // if user drags to left
    260         this.upperBound = new Date(this.timelineLeft * this.unitDuration +
    261             this.lowerBound.getTime());
    262         this.lowerBound = new Date((this.timelineLeft + this.dragDistance) *
    263             this.unitDuration + this.lowerBound.getTime());
    264       }
    265       this.totalDuration = this.upperBound.getTime() -
    266           this.lowerBound.getTime();
    267       this.unitDuration = this.totalDuration / LOG_VISUALIZER_CANVAS_WIDTH;
    268     },
    269 
    270     /**
    271      * Check if the time of a event is out of bound
    272      */
    273     isOutOfBound: function(event) {
    274       return event.time.getTime() < this.lowerBound.getTime() ||
    275               event.time.getTime() > this.upperBound.getTime();
    276     },
    277 
    278     /**
    279      * This function returns the offset on x-coordinate of canvas based on
    280      * the time
    281      */
    282     getPosition: function(time) {
    283       return (time.getTime() - this.lowerBound.getTime()) / this.unitDuration;
    284     },
    285 
    286     /**
    287      * This function updates the events array and refresh the canvas.
    288      */
    289     updateEvents: function(newEvents) {
    290       this.events.length = 0;
    291       for (var i in newEvents) {
    292         this.events.push(newEvents[i]);
    293       }
    294       if (!this.dataIntialized) {
    295         this.initialize();
    296       }
    297       this.clearCanvas();
    298       this.drawEvents();
    299     },
    300 
    301     /**
    302      * This is a helper function that returns the time object based on the
    303      * offset of x-coordinate on the canvs.
    304      */
    305     getOffsetTime: function(offset) {
    306       return new Date(this.lowerBound.getTime() + offset * this.unitDuration);
    307     },
    308 
    309     /**
    310      * This function is triggered when the hovering event is detected
    311      * When the mouse is hovering we have two control mode:
    312      *  - If it is in drag mode, we need to resize the width of the timeline
    313      *  - If not, we need to move the timeline and time display to the
    314      * x-coordinate position of the mouse
    315      */
    316     onHovered_: function(event) {
    317       var offsetX = event.offsetX;
    318       if (this.lastOffsetX == offsetX) {
    319         // If the mouse does not move, we just skip the event
    320         return;
    321       }
    322 
    323       if (this.dragMode == true) {
    324         // If the mouse is in drag mode
    325         this.dragDistance = offsetX - this.timelineLeft;
    326         if (this.dragDistance >= 0) {
    327           // If the mouse is moving right
    328           this.timeline.style.width = this.dragDistance + 'px';
    329         } else {
    330           // If the mouse is moving left
    331           this.timeline.style.width = -this.dragDistance + 'px';
    332           this.timeline.style.left = offsetX + 'px';
    333         }
    334       } else {
    335         // If the mouse is not in drag mode we just move the timeline
    336         this.timeline.style.width = '2px';
    337         this.timeline.style.left = offsetX + 'px';
    338       }
    339 
    340       // update time display
    341       this.timeDisplay.style.left = offsetX + 'px';
    342       this.timeDisplay.textContent =
    343           this.getOffsetTime(offsetX).toTimeString().substr(0, 8);
    344       // update the last offset
    345       this.lastOffsetX = offsetX;
    346     },
    347 
    348     /**
    349      * This function is the handler for the onMouseDown event on the canvas
    350      */
    351     onMouseDown_: function(event) {
    352       // Enter drag mode which let user choose a time range to zoom in
    353       this.dragMode = true;
    354       this.timelineLeft = event.offsetX;
    355       // Create a duration display to indicate the duration of range.
    356       this.timeDurationDisplay = this.createTimeDisplay();
    357       this.container.appendChild(this.timeDurationDisplay);
    358     },
    359 
    360     /**
    361      * This function is the handler for the onMouseUp event on the canvas
    362      */
    363     onMouseUp_: function(event) {
    364       // Remove the duration display
    365       this.container.removeChild(this.timeDurationDisplay);
    366       // End the drag mode
    367       this.dragMode = false;
    368       // Recalculate the pamameter based on the range user select
    369       this.reCalculate();
    370       // Filter the log table and hide the entries that are not in the range
    371       this.logAnalyzer.filterLog();
    372     },
    373   };
    374 
    375   return CrosLogVisualizer;
    376 })();
    377