• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 View visualizes TRACE_EVENT events using the
9 * tracing.Timeline component and adds in selection summary and control buttons.
10 */
11base.requireStylesheet('timeline_view');
12
13base.require('timeline_track_view');
14base.require('timeline_analysis_view');
15base.require('category_filter_dialog');
16base.require('filter');
17base.require('find_control');
18base.require('overlay');
19base.require('importer.trace_event_importer');
20base.require('importer.linux_perf_importer');
21base.require('importer.v8_log_importer');
22base.require('settings');
23
24base.exportTo('tracing', function() {
25
26  /**
27   * View
28   * @constructor
29   * @extends {HTMLDivElement}
30   */
31  var TimelineView = tracing.ui.define('div');
32
33  TimelineView.prototype = {
34    __proto__: HTMLDivElement.prototype,
35
36    decorate: function() {
37      this.classList.add('view');
38
39      // Create individual elements.
40      this.titleEl_ = document.createElement('div');
41      this.titleEl_.textContent = 'Tracing: ';
42      this.titleEl_.className = 'title';
43
44      this.controlDiv_ = document.createElement('div');
45      this.controlDiv_.className = 'control';
46
47      this.leftControlsEl_ = document.createElement('div');
48      this.leftControlsEl_.className = 'controls';
49      this.rightControlsEl_ = document.createElement('div');
50      this.rightControlsEl_.className = 'controls';
51
52      var spacingEl = document.createElement('div');
53      spacingEl.className = 'spacer';
54
55      this.timelineContainer_ = document.createElement('div');
56      this.timelineContainer_.className = 'container';
57
58      var analysisContainer_ = document.createElement('div');
59      analysisContainer_.className = 'analysis-container';
60
61      this.analysisEl_ = new tracing.TimelineAnalysisView();
62
63      this.dragEl_ = new DragHandle();
64      this.dragEl_.target = analysisContainer_;
65
66      this.findCtl_ = new tracing.FindControl();
67      this.findCtl_.controller = new tracing.FindController();
68
69      this.importErrorsButton_ = this.createImportErrorsButton_();
70      this.categoryFilterButton_ = this.createCategoryFilterButton_();
71      this.categoryFilterButton_.callback =
72          this.updateCategoryFilterFromSettings_.bind(this);
73      this.metadataButton_ = this.createMetadataButton_();
74
75      // Connect everything up.
76      this.rightControls.appendChild(this.importErrorsButton_);
77      this.rightControls.appendChild(this.categoryFilterButton_);
78      this.rightControls.appendChild(this.metadataButton_);
79      this.rightControls.appendChild(this.findCtl_);
80      this.controlDiv_.appendChild(this.titleEl_);
81      this.controlDiv_.appendChild(this.leftControlsEl_);
82      this.controlDiv_.appendChild(spacingEl);
83      this.controlDiv_.appendChild(this.rightControlsEl_);
84      this.appendChild(this.controlDiv_);
85
86      this.appendChild(this.timelineContainer_);
87      this.appendChild(this.dragEl_);
88
89      analysisContainer_.appendChild(this.analysisEl_);
90      this.appendChild(analysisContainer_);
91
92      this.rightControls.appendChild(this.createHelpButton_());
93
94      // Bookkeeping.
95      this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
96      document.addEventListener('keypress', this.onKeypress_.bind(this), true);
97    },
98
99    createImportErrorsButton_: function() {
100      var dlg = new tracing.ui.Overlay();
101      dlg.classList.add('view-import-errors-overlay');
102      dlg.autoClose = true;
103
104      var showEl = document.createElement('div');
105      showEl.className = 'button view-import-errors-button 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 = 'button 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.CategoryFilterDialog();
154        dlg.categories = that.model.categories;
155        dlg.settings = that.settings;
156        dlg.settings_key = 'categories';
157        dlg.settingUpdatedCallback = callback;
158        dlg.visible = true;
159      }
160
161      function updateVisibility() {
162        if (that.model)
163          showEl.style.display = '';
164        else
165          showEl.style.display = 'none';
166      }
167      updateVisibility();
168      that.addEventListener('modelChange', updateVisibility);
169
170      showEl.addEventListener('click', onClick.bind(this));
171      return showEl;
172    },
173
174    createHelpButton_: function() {
175      var dlg = new tracing.ui.Overlay();
176      dlg.classList.add('view-help-overlay');
177      dlg.autoClose = true;
178      dlg.additionalCloseKeyCodes.push('?'.charCodeAt(0));
179
180      var showEl = document.createElement('div');
181      showEl.className = 'button view-help-button';
182      showEl.textContent = '?';
183
184      var helpTextEl = document.createElement('div');
185      helpTextEl.style.whiteSpace = 'pre';
186      helpTextEl.style.fontFamily = 'monospace';
187      dlg.appendChild(helpTextEl);
188
189      function onClick(e) {
190        dlg.visible = true;
191        if (this.timeline_)
192          helpTextEl.textContent = this.timeline_.keyHelp;
193        else
194          helpTextEl.textContent = 'No content loaded. For interesting help,' +
195              ' load something.';
196
197        // Stop event so it doesn't trigger new click listener on document.
198        e.stopPropagation();
199        return false;
200      }
201
202      showEl.addEventListener('click', onClick.bind(this));
203
204      return showEl;
205    },
206
207    createMetadataButton_: function() {
208      var dlg = new tracing.ui.Overlay();
209      dlg.classList.add('view-metadata-overlay');
210      dlg.autoClose = true;
211
212      var showEl = document.createElement('div');
213      showEl.className = 'button view-metadata-button 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, undefined, ' '));
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.Model(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.bounds.isEmpty;
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.TimelineTrackView();
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('.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.FindControl();
390      dlg.controller = new tracing.FindController();
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.Selection();
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.CategoryFilter(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 DragHandle = tracing.ui.define('div');
438
439  DragHandle.prototype = {
440    __proto__: HTMLDivElement.prototype,
441
442    decorate: function() {
443      this.className = '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