blob: 970055ab916a946081d5575b1bd4ef7003011560 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview View visualizes TRACE_EVENT events using the
* tracing.Timeline component and adds in selection summary and control buttons.
*/
base.requireStylesheet('timeline_view');
base.require('timeline_track_view');
base.require('timeline_analysis_view');
base.require('category_filter_dialog');
base.require('filter');
base.require('find_control');
base.require('overlay');
base.require('importer.trace_event_importer');
base.require('importer.linux_perf_importer');
base.require('importer.v8_log_importer');
base.require('settings');
base.exportTo('tracing', function() {
/**
* View
* @constructor
* @extends {HTMLDivElement}
*/
var TimelineView = tracing.ui.define('div');
TimelineView.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
this.classList.add('view');
// Create individual elements.
this.titleEl_ = document.createElement('div');
this.titleEl_.textContent = 'Tracing: ';
this.titleEl_.className = 'title';
this.controlDiv_ = document.createElement('div');
this.controlDiv_.className = 'control';
this.leftControlsEl_ = document.createElement('div');
this.leftControlsEl_.className = 'controls';
this.rightControlsEl_ = document.createElement('div');
this.rightControlsEl_.className = 'controls';
var spacingEl = document.createElement('div');
spacingEl.className = 'spacer';
this.timelineContainer_ = document.createElement('div');
this.timelineContainer_.className = 'container';
var analysisContainer_ = document.createElement('div');
analysisContainer_.className = 'analysis-container';
this.analysisEl_ = new tracing.TimelineAnalysisView();
this.dragEl_ = new DragHandle();
this.dragEl_.target = analysisContainer_;
this.findCtl_ = new tracing.FindControl();
this.findCtl_.controller = new tracing.FindController();
this.importErrorsButton_ = this.createImportErrorsButton_();
this.categoryFilterButton_ = this.createCategoryFilterButton_();
this.categoryFilterButton_.callback =
this.updateCategoryFilterFromSettings_.bind(this);
this.metadataButton_ = this.createMetadataButton_();
// Connect everything up.
this.rightControls.appendChild(this.importErrorsButton_);
this.rightControls.appendChild(this.categoryFilterButton_);
this.rightControls.appendChild(this.metadataButton_);
this.rightControls.appendChild(this.findCtl_);
this.controlDiv_.appendChild(this.titleEl_);
this.controlDiv_.appendChild(this.leftControlsEl_);
this.controlDiv_.appendChild(spacingEl);
this.controlDiv_.appendChild(this.rightControlsEl_);
this.appendChild(this.controlDiv_);
this.appendChild(this.timelineContainer_);
this.appendChild(this.dragEl_);
analysisContainer_.appendChild(this.analysisEl_);
this.appendChild(analysisContainer_);
this.rightControls.appendChild(this.createHelpButton_());
// Bookkeeping.
this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
document.addEventListener('keypress', this.onKeypress_.bind(this), true);
},
createImportErrorsButton_: function() {
var dlg = new tracing.ui.Overlay();
dlg.classList.add('view-import-errors-overlay');
dlg.autoClose = true;
var showEl = document.createElement('div');
showEl.className = 'button view-import-errors-button view-info-button';
showEl.textContent = 'Import errors!';
var textEl = document.createElement('div');
textEl.className = 'info-button-text import-errors-dialog-text';
var containerEl = document.createElement('div');
containerEl.className = 'info-button-container' +
'import-errors-dialog';
containerEl.textContent = 'Errors occurred during import:';
containerEl.appendChild(textEl);
dlg.appendChild(containerEl);
var that = this;
function onClick() {
dlg.visible = true;
textEl.textContent = that.model.importErrors.join('\n');
}
showEl.addEventListener('click', onClick.bind(this));
function updateVisibility() {
if (that.model &&
that.model.importErrors.length)
showEl.style.display = '';
else
showEl.style.display = 'none';
}
updateVisibility();
that.addEventListener('modelChange', updateVisibility);
return showEl;
},
createCategoryFilterButton_: function() {
// Set by the embedder of the help button that we create in this function.
var callback;
var showEl = document.createElement('div');
showEl.className = 'button view-info-button';
showEl.textContent = 'Categories';
showEl.__defineSetter__('callback', function(value) {
callback = value;
});
var that = this;
function onClick() {
var dlg = new tracing.CategoryFilterDialog();
dlg.categories = that.model.categories;
dlg.settings = that.settings;
dlg.settings_key = 'categories';
dlg.settingUpdatedCallback = callback;
dlg.visible = true;
}
function updateVisibility() {
if (that.model)
showEl.style.display = '';
else
showEl.style.display = 'none';
}
updateVisibility();
that.addEventListener('modelChange', updateVisibility);
showEl.addEventListener('click', onClick.bind(this));
return showEl;
},
createHelpButton_: function() {
var dlg = new tracing.ui.Overlay();
dlg.classList.add('view-help-overlay');
dlg.autoClose = true;
dlg.additionalCloseKeyCodes.push('?'.charCodeAt(0));
var showEl = document.createElement('div');
showEl.className = 'button view-help-button';
showEl.textContent = '?';
var helpTextEl = document.createElement('div');
helpTextEl.style.whiteSpace = 'pre';
helpTextEl.style.fontFamily = 'monospace';
dlg.appendChild(helpTextEl);
function onClick(e) {
dlg.visible = true;
if (this.timeline_)
helpTextEl.textContent = this.timeline_.keyHelp;
else
helpTextEl.textContent = 'No content loaded. For interesting help,' +
' load something.';
// Stop event so it doesn't trigger new click listener on document.
e.stopPropagation();
return false;
}
showEl.addEventListener('click', onClick.bind(this));
return showEl;
},
createMetadataButton_: function() {
var dlg = new tracing.ui.Overlay();
dlg.classList.add('view-metadata-overlay');
dlg.autoClose = true;
var showEl = document.createElement('div');
showEl.className = 'button view-metadata-button view-info-button';
showEl.textContent = 'Metadata';
var textEl = document.createElement('div');
textEl.className = 'info-button-text metadata-dialog-text';
var containerEl = document.createElement('div');
containerEl.className = 'info-button-container metadata-dialog';
containerEl.textContent = 'Metadata Info:';
containerEl.appendChild(textEl);
dlg.appendChild(containerEl);
var that = this;
function onClick() {
dlg.visible = true;
var metadataStrings = [];
var model = that.model;
for (var data in model.metadata) {
metadataStrings.push(JSON.stringify(model.metadata[data].name) +
': ' + JSON.stringify(model.metadata[data].value, undefined, ' '));
}
textEl.textContent = metadataStrings.join('\n');
}
showEl.addEventListener('click', onClick.bind(this));
function updateVisibility() {
if (that.model &&
that.model.metadata.length)
showEl.style.display = '';
else
showEl.style.display = 'none';
}
updateVisibility();
that.addEventListener('modelChange', updateVisibility);
return showEl;
},
get leftControls() {
return this.leftControlsEl_;
},
get rightControls() {
return this.rightControlsEl_;
},
get title() {
return this.titleEl_.textContent.substring(
this.titleEl_.textContent.length - 2);
},
set title(text) {
this.titleEl_.textContent = text + ':';
},
set traceData(traceData) {
this.model = new tracing.Model(traceData);
},
get model() {
if (this.timeline_)
return this.timeline_.model;
return undefined;
},
set model(model) {
var modelInstanceChanged = model != this.model;
var modelValid = model && !model.bounds.isEmpty;
// Remove old timeline if the model has completely changed.
if (modelInstanceChanged) {
this.timelineContainer_.textContent = '';
if (this.timeline_) {
this.timeline_.removeEventListener(
'selectionChange', this.onSelectionChangedBoundToThis_);
this.timeline_.detach();
this.timeline_ = undefined;
this.findCtl_.controller.timeline = undefined;
}
}
// Create new timeline if needed.
if (modelValid && !this.timeline_) {
this.timeline_ = new tracing.TimelineTrackView();
this.timeline_.focusElement =
this.focusElement_ ? this.focusElement_ : this.parentElement;
this.timelineContainer_.appendChild(this.timeline_);
this.findCtl_.controller.timeline = this.timeline_;
this.timeline_.addEventListener(
'selectionChange', this.onSelectionChangedBoundToThis_);
this.updateCategoryFilterFromSettings_();
}
// Set the model.
if (modelValid)
this.timeline_.model = model;
base.dispatchSimpleEvent(this, 'modelChange');
// Do things that are selection specific
if (modelInstanceChanged)
this.onSelectionChanged_();
},
get timeline() {
return this.timeline_;
},
get settings() {
if (!this.settings_)
this.settings_ = new base.Settings();
return this.settings_;
},
/**
* Sets the element whose focus state will determine whether
* to respond to keybaord input.
*/
set focusElement(value) {
this.focusElement_ = value;
if (this.timeline_)
this.timeline_.focusElement = value;
},
/**
* @return {Element} The element whose focused state determines
* whether to respond to keyboard inputs.
* Defaults to the parent element.
*/
get focusElement() {
if (this.focusElement_)
return this.focusElement_;
return this.parentElement;
},
/**
* @return {boolean} Whether the current timeline is attached to the
* document.
*/
get isAttachedToDocument_() {
var cur = this;
while (cur.parentNode)
cur = cur.parentNode;
return cur == this.ownerDocument;
},
get listenToKeys_() {
if (!this.isAttachedToDocument_)
return;
if (!this.focusElement_)
return true;
if (this.focusElement.tabIndex >= 0)
return document.activeElement == this.focusElement;
return true;
},
onKeypress_: function(e) {
if (!this.listenToKeys_)
return;
if (event.keyCode == '/'.charCodeAt(0)) { // / key
this.findCtl_.focus();
event.preventDefault();
return;
} else if (e.keyCode == '?'.charCodeAt(0)) {
this.querySelector('.view-help-button').click();
e.preventDefault();
}
},
beginFind: function() {
if (this.findInProgress_)
return;
this.findInProgress_ = true;
var dlg = tracing.FindControl();
dlg.controller = new tracing.FindController();
dlg.controller.timeline = this.timeline;
dlg.visible = true;
dlg.addEventListener('close', function() {
this.findInProgress_ = false;
}.bind(this));
dlg.addEventListener('findNext', function() {
});
dlg.addEventListener('findPrevious', function() {
});
},
onSelectionChanged_: function(e) {
var oldScrollTop = this.timelineContainer_.scrollTop;
var selection = this.timeline_ ?
this.timeline_.selection :
new tracing.Selection();
this.analysisEl_.selection = selection;
this.timelineContainer_.scrollTop = oldScrollTop;
},
updateCategoryFilterFromSettings_: function() {
if (!this.timeline_)
return;
// Get the disabled categories from settings.
var categories = this.settings.keys('categories');
var disabledCategories = [];
for (var i = 0; i < categories.length; i++) {
if (this.settings.get(categories[i], 'true', 'categories') == 'false')
disabledCategories.push(categories[i]);
}
this.timeline_.categoryFilter =
new tracing.CategoryFilter(disabledCategories);
}
};
/**
* Timeline Drag Handle
* Detects when user clicks handle determines new height of container based
* on user's vertical mouse move and resizes the target.
* @constructor
* @extends {HTMLDivElement}
* You will need to set target to be the draggable element
*/
var DragHandle = tracing.ui.define('div');
DragHandle.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
this.className = 'drag-handle';
this.lastMousePosY = 0;
this.dragAnalysis = this.dragAnalysis.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.addEventListener('mousedown', this.onMouseDown);
},
dragAnalysis: function(e) {
// Compute the difference in height position.
var dy = this.lastMousePosY - e.clientY;
// If style is not set, start off with computed height.
if (!this.target.style.height)
this.target.style.height = window.getComputedStyle(this.target).height;
// Calculate new height of the container.
this.target.style.height = parseInt(this.target.style.height) + dy + 'px';
this.lastMousePosY = e.clientY;
},
onMouseDown: function(e) {
this.lastMousePosY = e.clientY;
document.addEventListener('mousemove', this.dragAnalysis);
document.addEventListener('mouseup', this.onMouseUp);
e.stopPropagation();
return false;
},
onMouseUp: function(e) {
document.removeEventListener('mousemove', this.dragAnalysis);
document.removeEventListener('mouseup', this.onMouseUp);
}
};
return {
TimelineView: TimelineView
};
});