Home | History | Annotate | Download | only in src
      1 // Copyright (c) 2012 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 'use strict';
      6 
      7 /**
      8  * @fileoverview TimelineView visualizes TRACE_EVENT events using the
      9  * tracing.Timeline component and adds in selection summary and control buttons.
     10  */
     11 base.requireStylesheet('timeline_view');
     12 
     13 base.require('timeline');
     14 base.require('timeline_analysis');
     15 base.require('timeline_category_filter_dialog');
     16 base.require('timeline_filter');
     17 base.require('timeline_find_control');
     18 base.require('overlay');
     19 base.require('trace_event_importer');
     20 base.require('linux_perf_importer');
     21 base.require('settings');
     22 
     23 base.exportTo('tracing', function() {
     24 
     25   /**
     26    * TimelineView
     27    * @constructor
     28    * @extends {HTMLDivElement}
     29    */
     30   var TimelineView = base.ui.define('div');
     31 
     32   TimelineView.prototype = {
     33     __proto__: HTMLDivElement.prototype,
     34 
     35     decorate: function() {
     36       this.classList.add('timeline-view');
     37 
     38       // Create individual elements.
     39       this.titleEl_ = document.createElement('div');
     40       this.titleEl_.textContent = 'Tracing: ';
     41       this.titleEl_.className = 'title';
     42 
     43       this.controlDiv_ = document.createElement('div');
     44       this.controlDiv_.className = 'control';
     45 
     46       this.leftControlsEl_ = document.createElement('div');
     47       this.leftControlsEl_.className = 'controls';
     48       this.rightControlsEl_ = document.createElement('div');
     49       this.rightControlsEl_.className = 'controls';
     50 
     51       var spacingEl = document.createElement('div');
     52       spacingEl.className = 'spacer';
     53 
     54       this.timelineContainer_ = document.createElement('div');
     55       this.timelineContainer_.className = 'timeline-container';
     56 
     57       var analysisContainer_ = document.createElement('div');
     58       analysisContainer_.className = 'analysis-container';
     59 
     60       this.analysisEl_ = new tracing.TimelineAnalysisView();
     61 
     62       this.dragEl_ = new TimelineDragHandle();
     63       this.dragEl_.target = analysisContainer_;
     64 
     65       this.findCtl_ = new tracing.TimelineFindControl();
     66       this.findCtl_.controller = new tracing.TimelineFindController();
     67 
     68       this.importErrorsButton_ = this.createImportErrorsButton_();
     69       this.categoryFilterButton_ = this.createCategoryFilterButton_();
     70       this.categoryFilterButton_.callback =
     71           this.updateCategoryFilterFromSettings_.bind(this);
     72       this.metadataButton_ = this.createMetadataButton_();
     73 
     74       // Connect everything up.
     75       this.rightControls.appendChild(this.importErrorsButton_);
     76       this.rightControls.appendChild(this.categoryFilterButton_);
     77       this.rightControls.appendChild(this.metadataButton_);
     78       this.rightControls.appendChild(this.findCtl_);
     79       this.controlDiv_.appendChild(this.titleEl_);
     80       this.controlDiv_.appendChild(this.leftControlsEl_);
     81       this.controlDiv_.appendChild(spacingEl);
     82       this.controlDiv_.appendChild(this.rightControlsEl_);
     83       this.appendChild(this.controlDiv_);
     84 
     85       this.appendChild(this.timelineContainer_);
     86       this.appendChild(this.dragEl_);
     87 
     88       analysisContainer_.appendChild(this.analysisEl_);
     89       this.appendChild(analysisContainer_);
     90 
     91       this.rightControls.appendChild(this.createHelpButton_());
     92 
     93       // Bookkeeping.
     94       this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
     95       document.addEventListener('keypress', this.onKeypress_.bind(this), true);
     96     },
     97 
     98     createImportErrorsButton_: function() {
     99       var dlg = new tracing.Overlay();
    100       dlg.classList.add('timeline-view-import-errors-overlay');
    101       dlg.autoClose = true;
    102 
    103       var showEl = document.createElement('div');
    104       showEl.className = 'timeline-button timeline-view-import-errors-button' +
    105           ' timeline-view-info-button';
    106       showEl.textContent = 'Import errors!';
    107 
    108       var textEl = document.createElement('div');
    109       textEl.className = 'info-button-text import-errors-dialog-text';
    110 
    111       var containerEl = document.createElement('div');
    112       containerEl.className = 'info-button-container' +
    113           'import-errors-dialog';
    114 
    115       containerEl.textContent = 'Errors occurred during import:';
    116       containerEl.appendChild(textEl);
    117       dlg.appendChild(containerEl);
    118 
    119       var that = this;
    120       function onClick() {
    121         dlg.visible = true;
    122         textEl.textContent = that.model.importErrors.join('\n');
    123       }
    124       showEl.addEventListener('click', onClick.bind(this));
    125 
    126       function updateVisibility() {
    127         if (that.model &&
    128             that.model.importErrors.length)
    129           showEl.style.display = '';
    130         else
    131           showEl.style.display = 'none';
    132       }
    133       updateVisibility();
    134       that.addEventListener('modelChange', updateVisibility);
    135 
    136       return showEl;
    137     },
    138 
    139     createCategoryFilterButton_: function() {
    140       // Set by the embedder of the help button that we create in this function.
    141       var callback;
    142 
    143       var showEl = document.createElement('div');
    144       showEl.className = 'timeline-button timeline-view-info-button';
    145       showEl.textContent = 'Categories';
    146       showEl.__defineSetter__('callback', function(value) {
    147         callback = value;
    148       });
    149 
    150 
    151       var that = this;
    152       function onClick() {
    153         var dlg = new tracing.TimelineCategoryFilterDialog();
    154         dlg.model = that.model;
    155         dlg.settings = that.settings;
    156         dlg.settingUpdatedCallback = callback;
    157         dlg.visible = true;
    158       }
    159 
    160       function updateVisibility() {
    161         if (that.model)
    162           showEl.style.display = '';
    163         else
    164           showEl.style.display = 'none';
    165       }
    166       updateVisibility();
    167       that.addEventListener('modelChange', updateVisibility);
    168 
    169       showEl.addEventListener('click', onClick.bind(this));
    170       return showEl;
    171     },
    172 
    173     createHelpButton_: function() {
    174       var dlg = new tracing.Overlay();
    175       dlg.classList.add('timeline-view-help-overlay');
    176       dlg.autoClose = true;
    177       dlg.additionalCloseKeyCodes.push('?'.charCodeAt(0));
    178 
    179       var showEl = document.createElement('div');
    180       showEl.className = 'timeline-button timeline-view-help-button';
    181       showEl.textContent = '?';
    182 
    183       var helpTextEl = document.createElement('div');
    184       helpTextEl.style.whiteSpace = 'pre';
    185       helpTextEl.style.fontFamily = 'monospace';
    186       dlg.appendChild(helpTextEl);
    187 
    188       function onClick(e) {
    189         dlg.visible = true;
    190         if (this.timeline_)
    191           helpTextEl.textContent = this.timeline_.keyHelp;
    192         else
    193           helpTextEl.textContent = 'No content loaded. For interesting help,' +
    194               ' load something.';
    195 
    196         // Stop event so it doesn't trigger new click listener on document.
    197         e.stopPropagation();
    198         return false;
    199       }
    200 
    201       showEl.addEventListener('click', onClick.bind(this));
    202 
    203       return showEl;
    204     },
    205 
    206     createMetadataButton_: function() {
    207       var dlg = new tracing.Overlay();
    208       dlg.classList.add('timeline-view-metadata-overlay');
    209       dlg.autoClose = true;
    210 
    211       var showEl = document.createElement('div');
    212       showEl.className = 'timeline-button timeline-view-metadata-button' +
    213           ' timeline-view-info-button';
    214       showEl.textContent = 'Metadata';
    215 
    216       var textEl = document.createElement('div');
    217       textEl.className = 'info-button-text metadata-dialog-text';
    218 
    219       var containerEl = document.createElement('div');
    220       containerEl.className = 'info-button-container metadata-dialog';
    221 
    222       containerEl.textContent = 'Metadata Info:';
    223       containerEl.appendChild(textEl);
    224       dlg.appendChild(containerEl);
    225 
    226       var that = this;
    227       function onClick() {
    228         dlg.visible = true;
    229 
    230         var metadataStrings = [];
    231 
    232         var model = that.model;
    233         for (var data in model.metadata) {
    234           metadataStrings.push(JSON.stringify(model.metadata[data].name) +
    235               ': ' + JSON.stringify(model.metadata[data].value));
    236         }
    237         textEl.textContent = metadataStrings.join('\n');
    238       }
    239       showEl.addEventListener('click', onClick.bind(this));
    240 
    241       function updateVisibility() {
    242         if (that.model &&
    243             that.model.metadata.length)
    244           showEl.style.display = '';
    245         else
    246           showEl.style.display = 'none';
    247       }
    248       updateVisibility();
    249       that.addEventListener('modelChange', updateVisibility);
    250 
    251       return showEl;
    252     },
    253 
    254     get leftControls() {
    255       return this.leftControlsEl_;
    256     },
    257 
    258     get rightControls() {
    259       return this.rightControlsEl_;
    260     },
    261 
    262     get title() {
    263       return this.titleEl_.textContent.substring(
    264           this.titleEl_.textContent.length - 2);
    265     },
    266 
    267     set title(text) {
    268       this.titleEl_.textContent = text + ':';
    269     },
    270 
    271     set traceData(traceData) {
    272       this.model = new tracing.TimelineModel(traceData);
    273     },
    274 
    275     get model() {
    276       if (this.timeline_)
    277         return this.timeline_.model;
    278       return undefined;
    279     },
    280 
    281     set model(model) {
    282       var modelInstanceChanged = model != this.model;
    283       var modelValid = model && model.minTimestamp !== undefined;
    284 
    285       // Remove old timeline if the model has completely changed.
    286       if (modelInstanceChanged) {
    287         this.timelineContainer_.textContent = '';
    288         if (this.timeline_) {
    289           this.timeline_.removeEventListener(
    290               'selectionChange', this.onSelectionChangedBoundToThis_);
    291           this.timeline_.detach();
    292           this.timeline_ = undefined;
    293           this.findCtl_.controller.timeline = undefined;
    294         }
    295       }
    296 
    297       // Create new timeline if needed.
    298       if (modelValid && !this.timeline_) {
    299         this.timeline_ = new tracing.Timeline();
    300         this.timeline_.focusElement =
    301             this.focusElement_ ? this.focusElement_ : this.parentElement;
    302         this.timelineContainer_.appendChild(this.timeline_);
    303         this.findCtl_.controller.timeline = this.timeline_;
    304         this.timeline_.addEventListener(
    305             'selectionChange', this.onSelectionChangedBoundToThis_);
    306         this.updateCategoryFilterFromSettings_();
    307       }
    308 
    309       // Set the model.
    310       if (modelValid)
    311         this.timeline_.model = model;
    312       base.dispatchSimpleEvent(this, 'modelChange');
    313 
    314       // Do things that are selection specific
    315       if (modelInstanceChanged)
    316         this.onSelectionChanged_();
    317     },
    318 
    319     get timeline() {
    320       return this.timeline_;
    321     },
    322 
    323     get settings() {
    324       if (!this.settings_)
    325         this.settings_ = new base.Settings();
    326       return this.settings_;
    327     },
    328 
    329     /**
    330      * Sets the element whose focus state will determine whether
    331      * to respond to keybaord input.
    332      */
    333     set focusElement(value) {
    334       this.focusElement_ = value;
    335       if (this.timeline_)
    336         this.timeline_.focusElement = value;
    337     },
    338 
    339     /**
    340      * @return {Element} The element whose focused state determines
    341      * whether to respond to keyboard inputs.
    342      * Defaults to the parent element.
    343      */
    344     get focusElement() {
    345       if (this.focusElement_)
    346         return this.focusElement_;
    347       return this.parentElement;
    348     },
    349 
    350     /**
    351      * @return {boolean} Whether the current timeline is attached to the
    352      * document.
    353      */
    354     get isAttachedToDocument_() {
    355       var cur = this;
    356       while (cur.parentNode)
    357         cur = cur.parentNode;
    358       return cur == this.ownerDocument;
    359     },
    360 
    361     get listenToKeys_() {
    362       if (!this.isAttachedToDocument_)
    363         return;
    364       if (!this.focusElement_)
    365         return true;
    366       if (this.focusElement.tabIndex >= 0)
    367         return document.activeElement == this.focusElement;
    368       return true;
    369     },
    370 
    371     onKeypress_: function(e) {
    372       if (!this.listenToKeys_)
    373         return;
    374 
    375       if (event.keyCode == '/'.charCodeAt(0)) { // / key
    376         this.findCtl_.focus();
    377         event.preventDefault();
    378         return;
    379       } else if (e.keyCode == '?'.charCodeAt(0)) {
    380         this.querySelector('.timeline-view-help-button').click();
    381         e.preventDefault();
    382       }
    383     },
    384 
    385     beginFind: function() {
    386       if (this.findInProgress_)
    387         return;
    388       this.findInProgress_ = true;
    389       var dlg = tracing.TimelineFindControl();
    390       dlg.controller = new tracing.TimelineFindController();
    391       dlg.controller.timeline = this.timeline;
    392       dlg.visible = true;
    393       dlg.addEventListener('close', function() {
    394         this.findInProgress_ = false;
    395       }.bind(this));
    396       dlg.addEventListener('findNext', function() {
    397       });
    398       dlg.addEventListener('findPrevious', function() {
    399       });
    400     },
    401 
    402     onSelectionChanged_: function(e) {
    403       var oldScrollTop = this.timelineContainer_.scrollTop;
    404 
    405       var selection = this.timeline_ ?
    406           this.timeline_.selection :
    407           new tracing.TimelineSelection();
    408       this.analysisEl_.selection = selection;
    409       this.timelineContainer_.scrollTop = oldScrollTop;
    410     },
    411 
    412     updateCategoryFilterFromSettings_: function() {
    413       if (!this.timeline_)
    414         return;
    415 
    416       // Get the disabled categories from settings.
    417       var categories = this.settings.keys('categories');
    418       var disabledCategories = [];
    419       for (var i = 0; i < categories.length; i++) {
    420         if (this.settings.get(categories[i], 'true', 'categories') == 'false')
    421           disabledCategories.push(categories[i]);
    422       }
    423 
    424       this.timeline_.categoryFilter =
    425           new tracing.TimelineCategoryFilter(disabledCategories);
    426     }
    427   };
    428 
    429   /**
    430    * Timeline Drag Handle
    431    * Detects when user clicks handle determines new height of container based
    432    * on user's vertical mouse move and resizes the target.
    433    * @constructor
    434    * @extends {HTMLDivElement}
    435    * You will need to set target to be the draggable element
    436    */
    437   var TimelineDragHandle = base.ui.define('div');
    438 
    439   TimelineDragHandle.prototype = {
    440     __proto__: HTMLDivElement.prototype,
    441 
    442     decorate: function() {
    443       this.className = 'timeline-drag-handle';
    444       this.lastMousePosY = 0;
    445       this.dragAnalysis = this.dragAnalysis.bind(this);
    446       this.onMouseUp = this.onMouseUp.bind(this);
    447       this.addEventListener('mousedown', this.onMouseDown);
    448     },
    449 
    450     dragAnalysis: function(e) {
    451       // Compute the difference in height position.
    452       var dy = this.lastMousePosY - e.clientY;
    453       // If style is not set, start off with computed height.
    454       if (!this.target.style.height)
    455         this.target.style.height = window.getComputedStyle(this.target).height;
    456       // Calculate new height of the container.
    457       this.target.style.height = parseInt(this.target.style.height) + dy + 'px';
    458       this.lastMousePosY = e.clientY;
    459     },
    460 
    461     onMouseDown: function(e) {
    462       this.lastMousePosY = e.clientY;
    463       document.addEventListener('mousemove', this.dragAnalysis);
    464       document.addEventListener('mouseup', this.onMouseUp);
    465       e.stopPropagation();
    466       return false;
    467     },
    468 
    469     onMouseUp: function(e) {
    470       document.removeEventListener('mousemove', this.dragAnalysis);
    471       document.removeEventListener('mouseup', this.onMouseUp);
    472     }
    473   };
    474 
    475   return {
    476     TimelineView: TimelineView
    477   };
    478 });
    479