Home | History | Annotate | Download | only in tracing
      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 cr.define('tracing', function() {
     12   /**
     13    * TimelineFindControl
     14    * @constructor
     15    * @extends {tracing.Overlay}
     16    */
     17   var TimelineFindControl = cr.ui.define('div');
     18 
     19   TimelineFindControl.prototype = {
     20     __proto__: tracing.Overlay.prototype,
     21 
     22     decorate: function() {
     23       tracing.Overlay.prototype.decorate.call(this);
     24 
     25       this.className = 'timeline-find-control';
     26 
     27       this.hitCountEl_ = document.createElement('div');
     28       this.hitCountEl_.className = 'hit-count-label';
     29       this.hitCountEl_.textContent = '1 of 7';
     30 
     31       var findPreviousBn = document.createElement('div');
     32       findPreviousBn.className = 'timeline-button find-previous';
     33       findPreviousBn.textContent = '\u2190';
     34       findPreviousBn.addEventListener('click', function() {
     35         this.controller.findPrevious();
     36         this.updateHitCountEl_();
     37       }.bind(this));
     38 
     39       var findNextBn = document.createElement('div');
     40       findNextBn.className = 'timeline-button find-next';
     41       findNextBn.textContent = '\u2192';
     42       findNextBn.addEventListener('click', function() {
     43         this.controller.findNext();
     44         this.updateHitCountEl_();
     45       }.bind(this));
     46 
     47       // Filter input element.
     48       this.filterEl_ = document.createElement('input');
     49       this.filterEl_.type = 'input';
     50 
     51       this.filterEl_.addEventListener('input', function(e) {
     52         this.controller.filterText = this.filterEl_.value;
     53         this.updateHitCountEl_();
     54       }.bind(this));
     55 
     56       this.filterEl_.addEventListener('keydown', function(e) {
     57         if (e.keyCode == 13) {
     58           findNextBn.click();
     59         } else if (e.keyCode == 27) {
     60           this.filterEl_.blur();
     61           this.updateHitCountEl_();
     62         }
     63       }.bind(this));
     64 
     65       this.filterEl_.addEventListener('blur', function(e) {
     66         this.updateHitCountEl_();
     67       }.bind(this));
     68 
     69       this.filterEl_.addEventListener('focus', function(e) {
     70         this.updateHitCountEl_();
     71       }.bind(this));
     72 
     73       // Attach everything.
     74       this.appendChild(this.filterEl_);
     75 
     76       this.appendChild(findPreviousBn);
     77       this.appendChild(findNextBn);
     78       this.appendChild(this.hitCountEl_);
     79 
     80       this.updateHitCountEl_();
     81     },
     82 
     83     get controller() {
     84       return this.controller_;
     85     },
     86 
     87     set controller(c) {
     88       this.controller_ = c;
     89       this.updateHitCountEl_();
     90     },
     91 
     92     focus: function() {
     93       this.filterEl_.selectionStart = 0;
     94       this.filterEl_.selectionEnd = this.filterEl_.value.length;
     95       this.filterEl_.focus();
     96     },
     97 
     98     updateHitCountEl_: function() {
     99       if (!this.controller || document.activeElement != this.filterEl_) {
    100         this.hitCountEl_.textContent = '';
    101         return;
    102       }
    103       var i = this.controller.currentHitIndex;
    104       var n = this.controller.filterHits.length;
    105       if (n == 0)
    106         this.hitCountEl_.textContent = '0 of 0';
    107       else
    108         this.hitCountEl_.textContent = (i + 1) + ' of ' + n;
    109     }
    110   };
    111 
    112   function TimelineFindController() {
    113     this.timeline_ = undefined;
    114     this.model_ = undefined;
    115     this.filterText_ = '';
    116     this.filterHits_ = new tracing.TimelineSelection();
    117     this.filterHitsDirty_ = true;
    118     this.currentHitIndex_ = 0;
    119   };
    120 
    121   TimelineFindController.prototype = {
    122     __proto__: Object.prototype,
    123 
    124     get timeline() {
    125       return this.timeline_;
    126     },
    127 
    128     set timeline(t) {
    129       this.timeline_ = t;
    130       this.filterHitsDirty_ = true;
    131     },
    132 
    133     get filterText() {
    134       return this.filterText_;
    135     },
    136 
    137     set filterText(f) {
    138       if (f == this.filterText_)
    139         return;
    140       this.filterText_ = f;
    141       this.filterHitsDirty_ = true;
    142       this.findNext();
    143     },
    144 
    145     get filterHits() {
    146       if (this.filterHitsDirty_) {
    147         this.filterHitsDirty_ = false;
    148         if (this.timeline_) {
    149           var filter = new tracing.TimelineFilter(this.filterText);
    150           this.filterHits_.clear();
    151           this.timeline.addAllObjectsMatchingFilterToSelection(
    152             filter, this.filterHits_);
    153           this.currentHitIndex_ = this.filterHits_.length - 1;
    154         } else {
    155           this.filterHits_.clear();
    156           this.currentHitIndex_ = 0;
    157         }
    158       }
    159       return this.filterHits_;
    160     },
    161 
    162     get currentHitIndex() {
    163       return this.currentHitIndex_;
    164     },
    165 
    166     find_: function(dir) {
    167       if (!this.timeline)
    168         return;
    169 
    170       var N = this.filterHits.length;
    171       this.currentHitIndex_ = this.currentHitIndex_ + dir;
    172 
    173       if (this.currentHitIndex_ < 0) this.currentHitIndex_ = N - 1;
    174       if (this.currentHitIndex_ >= N) this.currentHitIndex_ = 0;
    175 
    176       if (this.currentHitIndex_ < 0 || this.currentHitIndex_ >= N) {
    177         this.timeline.selection = new tracing.TimelineSelection();
    178         return;
    179       }
    180 
    181       // We allow the zoom level to change on the first hit level. But, when
    182       // then cycling through subsequent changes, restrict it to panning.
    183       var zoomAllowed = this.currentHitIndex_ == 0;
    184       var subSelection = this.filterHits.subSelection(this.currentHitIndex_);
    185       this.timeline.setSelectionAndMakeVisible(subSelection, zoomAllowed);
    186     },
    187 
    188     findNext: function() {
    189       this.find_(1);
    190     },
    191 
    192     findPrevious: function() {
    193       this.find_(-1);
    194     },
    195   };
    196 
    197   /**
    198    * TimelineView
    199    * @constructor
    200    * @extends {HTMLDivElement}
    201    */
    202   var TimelineView = cr.ui.define('div');
    203 
    204   TimelineView.prototype = {
    205     __proto__: HTMLDivElement.prototype,
    206 
    207     decorate: function() {
    208       this.classList.add('timeline-view');
    209 
    210       // Create individual elements.
    211       this.titleEl_ = document.createElement('div');
    212       this.titleEl_.textContent = 'Tracing: ';
    213 
    214       this.controlDiv_ = document.createElement('div');
    215       this.controlDiv_.className = 'control';
    216 
    217       this.leftControlsEl_ = document.createElement('div');
    218       this.leftControlsEl_.className = 'controls';
    219       this.rightControlsEl_ = document.createElement('div');
    220       this.rightControlsEl_.className = 'controls';
    221 
    222       var spacingEl = document.createElement('div');
    223       spacingEl.className = 'spacer';
    224 
    225       this.timelineContainer_ = document.createElement('div');
    226       this.timelineContainer_.className = 'timeline-container';
    227 
    228       var analysisContainer_ = document.createElement('div');
    229       analysisContainer_.className = 'analysis-container';
    230 
    231       this.analysisEl_ = new tracing.TimelineAnalysisView();
    232 
    233       this.findCtl_ = new TimelineFindControl();
    234       this.findCtl_.controller = new TimelineFindController();
    235 
    236       // Connect everything up.
    237       this.rightControls.appendChild(this.findCtl_);
    238       this.controlDiv_.appendChild(this.titleEl_);
    239       this.controlDiv_.appendChild(this.leftControlsEl_);
    240       this.controlDiv_.appendChild(spacingEl);
    241       this.controlDiv_.appendChild(this.rightControlsEl_);
    242       this.appendChild(this.controlDiv_);
    243 
    244       this.appendChild(this.timelineContainer_);
    245 
    246       analysisContainer_.appendChild(this.analysisEl_);
    247       this.appendChild(analysisContainer_);
    248 
    249       this.rightControls.appendChild(this.createHelpButton_());
    250 
    251       // Bookkeeping.
    252       this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
    253       document.addEventListener('keypress', this.onKeypress_.bind(this), true);
    254     },
    255 
    256     createHelpButton_: function() {
    257       var dlg = new tracing.Overlay();
    258       dlg.classList.add('timeline-view-help-overlay');
    259 
    260       var showHelpEl = document.createElement('div');
    261       showHelpEl.className = 'timeline-button timeline-view-help-button';
    262       showHelpEl.textContent = '?';
    263 
    264       var helpTextEl = document.createElement('div');
    265       helpTextEl.style.whiteSpace = 'pre';
    266       helpTextEl.style.fontFamily = 'monospace';
    267 
    268       function onClick() {
    269         dlg.visible = true;
    270         helpTextEl.textContent = this.timeline_.keyHelp;
    271         document.addEventListener('keydown', onKey, true);
    272       }
    273 
    274       function onKey(e) {
    275         if (!dlg.visible)
    276           return;
    277 
    278         if (e.keyCode == 27 || e.keyCode == '?'.charCodeAt(0)) {
    279           e.preventDefault();
    280           document.removeEventListener('keydown', onKey);
    281           dlg.visible = false;
    282         }
    283       }
    284       showHelpEl.addEventListener('click', onClick.bind(this));
    285 
    286       dlg.appendChild(helpTextEl);
    287 
    288       return showHelpEl;
    289     },
    290 
    291     get leftControls() {
    292       return this.leftControlsEl_;
    293     },
    294 
    295     get rightControls() {
    296       return this.rightControlsEl_;
    297     },
    298 
    299     get title() {
    300       return this.titleEl_.textContent.substring(
    301         this.titleEl_.textContent.length - 2);
    302     },
    303 
    304     set title(text) {
    305       this.titleEl_.textContent = text + ':';
    306     },
    307 
    308     set traceData(traceData) {
    309       this.model = new tracing.TimelineModel(traceData);
    310     },
    311 
    312     get model() {
    313       return this.timelineModel_;
    314     },
    315 
    316     set model(model) {
    317       this.timelineModel_ = model;
    318 
    319       // remove old timeline
    320       this.timelineContainer_.textContent = '';
    321 
    322       // create new timeline if needed
    323       if (this.timelineModel_.minTimestamp !== undefined) {
    324         if (this.timeline_) {
    325           this.timeline_.viewportTrack.detach();
    326           this.timeline_.detach();
    327         }
    328         this.timeline_ = new tracing.Timeline();
    329         this.timeline_.model = this.timelineModel_;
    330         this.timeline_.focusElement =
    331             this.focusElement_ ? this.focusElement_ : this.parentElement;
    332         this.insertBefore(this.timeline_.viewportTrack, this.timelineContainer_);
    333         this.timelineContainer_.appendChild(this.timeline_);
    334         this.timeline_.addEventListener('selectionChange',
    335                                         this.onSelectionChangedBoundToThis_);
    336 
    337         this.findCtl_.controller.timeline = this.timeline_;
    338         this.onSelectionChanged_();
    339       } else {
    340         this.timeline_ = undefined;
    341         this.findCtl_.controller.timeline = undefined;
    342       }
    343     },
    344 
    345     get timeline() {
    346       return this.timeline_;
    347     },
    348 
    349     /**
    350      * Sets the element whose focus state will determine whether
    351      * to respond to keybaord input.
    352      */
    353     set focusElement(value) {
    354       this.focusElement_ = value;
    355       if (this.timeline_)
    356         this.timeline_.focusElement = value;
    357     },
    358 
    359     /**
    360      * @return {Element} The element whose focused state determines
    361      * whether to respond to keyboard inputs.
    362      * Defaults to the parent element.
    363      */
    364     get focusElement() {
    365       if (this.focusElement_)
    366         return this.focusElement_;
    367       return this.parentElement;
    368     },
    369 
    370     /**
    371      * @return {boolean} Whether the current timeline is attached to the
    372      * document.
    373      */
    374     get isAttachedToDocument_() {
    375       var cur = this;
    376       while (cur.parentNode)
    377         cur = cur.parentNode;
    378       return cur == this.ownerDocument;
    379     },
    380 
    381     get listenToKeys_() {
    382       if (!this.isAttachedToDocument_)
    383         return;
    384       if (!this.focusElement_)
    385         return true;
    386       if (this.focusElement.tabIndex >= 0)
    387         return document.activeElement == this.focusElement;
    388       return true;
    389     },
    390 
    391     onKeypress_: function(e) {
    392       if (!this.listenToKeys_)
    393         return;
    394 
    395       if (event.keyCode == '/'.charCodeAt(0)) { // / key
    396         this.findCtl_.focus();
    397         event.preventDefault();
    398         return;
    399       } else if (e.keyCode == '?'.charCodeAt(0)) {
    400         this.querySelector('.timeline-view-help-button').click();
    401         e.preventDefault();
    402       }
    403     },
    404 
    405     beginFind: function() {
    406       if (this.findInProgress_)
    407         return;
    408       this.findInProgress_ = true;
    409       var dlg = TimelineFindControl();
    410       dlg.controller = new TimelineFindController();
    411       dlg.controller.timeline = this.timeline;
    412       dlg.visible = true;
    413       dlg.addEventListener('close', function() {
    414         this.findInProgress_ = false;
    415       }.bind(this));
    416       dlg.addEventListener('findNext', function() {
    417       });
    418       dlg.addEventListener('findPrevious', function() {
    419       });
    420     },
    421 
    422     onSelectionChanged_: function(e) {
    423       var oldScrollTop = this.timelineContainer_.scrollTop;
    424       this.analysisEl_.selection = this.timeline_.selection;
    425       this.timelineContainer_.scrollTop = oldScrollTop;
    426     }
    427   };
    428 
    429   return {
    430     TimelineFindControl: TimelineFindControl,
    431     TimelineFindController: TimelineFindController,
    432     TimelineView: TimelineView
    433   };
    434 });
    435