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