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 */ 11 base.requireStylesheet('timeline_view'); 12 13 base.require('timeline_track_view'); 14 base.require('timeline_analysis_view'); 15 base.require('category_filter_dialog'); 16 base.require('filter'); 17 base.require('find_control'); 18 base.require('overlay'); 19 base.require('importer.trace_event_importer'); 20 base.require('importer.linux_perf_importer'); 21 base.require('importer.v8_log_importer'); 22 base.require('settings'); 23 24 base.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