1 // Copyright 2013 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 // TODO(viona): Write a README document/instructions. 6 7 /** This view displays the event waterfall. */ 8 var WaterfallView = (function() { 9 'use strict'; 10 11 // We inherit from DivView. 12 var superClass = DivView; 13 14 /** 15 * @constructor 16 */ 17 function WaterfallView() { 18 assertFirstConstructorCall(WaterfallView); 19 20 // Call superclass's constructor. 21 superClass.call(this, WaterfallView.MAIN_BOX_ID); 22 23 SourceTracker.getInstance().addSourceEntryObserver(this); 24 25 // For adjusting the range of view. 26 $(WaterfallView.SCALE_ID).addEventListener( 27 'click', this.setStartEndTimes_.bind(this), true); 28 29 $(WaterfallView.MAIN_BOX_ID).addEventListener( 30 'mousewheel', this.scrollToZoom_.bind(this), true); 31 32 $(WaterfallView.MAIN_BOX_ID).addEventListener( 33 'scroll', this.scrollInfoTable_.bind(this), true); 34 35 this.initializeSourceList_(); 36 37 window.onload = this.scrollInfoTable_(); 38 } 39 40 WaterfallView.TAB_ID = 'tab-handle-waterfall'; 41 WaterfallView.TAB_NAME = 'Waterfall'; 42 WaterfallView.TAB_HASH = '#waterfall'; 43 44 // IDs for special HTML elements in events_waterfall_view.html. 45 WaterfallView.MAIN_BOX_ID = 'waterfall-view-tab-content'; 46 WaterfallView.BAR_TABLE_ID = 'waterfall-view-time-bar-table'; 47 WaterfallView.BAR_TBODY_ID = 'waterfall-view-time-bar-tbody'; 48 WaterfallView.SCALE_ID = 'waterfall-view-adjust-to-window'; 49 WaterfallView.TIME_SCALE_HEADER_ID = 'waterfall-view-time-scale-labels'; 50 WaterfallView.TIME_RANGE_ID = 'waterfall-view-time-range-submit'; 51 WaterfallView.START_TIME_ID = 'waterfall-view-start-input'; 52 WaterfallView.END_TIME_ID = 'waterfall-view-end-input'; 53 WaterfallView.INFO_TABLE_ID = 'waterfall-view-information-table'; 54 WaterfallView.INFO_TBODY_ID = 'waterfall-view-information-tbody'; 55 WaterfallView.CONTROLS_ID = 'waterfall-view-controls'; 56 WaterfallView.ID_HEADER_ID = 'waterfall-view-id-header'; 57 WaterfallView.URL_HEADER_ID = 'waterfall-view-url-header'; 58 59 // The number of units mouse wheel deltas increase for each tick of the 60 // wheel. 61 var MOUSE_WHEEL_UNITS_PER_CLICK = 120; 62 63 // Amount we zoom for one vertical tick of the mouse wheel, as a ratio. 64 var MOUSE_WHEEL_ZOOM_RATE = 1.25; 65 // Amount we scroll for one horizontal tick of the mouse wheel, in pixels. 66 var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK; 67 68 cr.addSingletonGetter(WaterfallView); 69 70 WaterfallView.prototype = { 71 // Inherit the superclass's methods. 72 __proto__: superClass.prototype, 73 74 /** 75 * Creates new WaterfallRows for URL Requests when the sourceEntries are 76 * updated if they do not already exist. 77 * Updates pre-existing WaterfallRows that correspond to updated sources. 78 */ 79 onSourceEntriesUpdated: function(sourceEntries) { 80 for (var i = 0; i < sourceEntries.length; ++i) { 81 var sourceEntry = sourceEntries[i]; 82 var id = sourceEntry.getSourceId(); 83 if (sourceEntry.getSourceType() == EventSourceType.URL_REQUEST) { 84 var row = this.sourceIdToRowMap_[id]; 85 if (!row) { 86 var newRow = new WaterfallRow(this, sourceEntry); 87 this.sourceIdToRowMap_[id] = newRow; 88 } else { 89 row.onSourceUpdated(); 90 } 91 } 92 } 93 this.scrollInfoTable_(); 94 this.positionBarTable_(); 95 this.updateTimeScale_(this.scaleFactor_); 96 }, 97 98 onAllSourceEntriesDeleted: function() { 99 this.initializeSourceList_(); 100 }, 101 102 onLoadLogFinish: function(data) { 103 return true; 104 }, 105 106 getScaleFactor: function() { 107 return this.scaleFactor_; 108 }, 109 110 setGeometry: function(left, top, width, height) { 111 superClass.prototype.setGeometry.call(this, left, top, width, height); 112 this.scrollInfoTable_(); 113 }, 114 115 show: function(isVisible) { 116 superClass.prototype.show.call(this, isVisible); 117 if (isVisible) { 118 this.scrollInfoTable_(); 119 } 120 }, 121 122 /** 123 * Initializes the list of source entries. If source entries are already 124 * being displayed, removes them all in the process. 125 */ 126 initializeSourceList_: function() { 127 this.sourceIdToRowMap_ = {}; 128 $(WaterfallView.BAR_TBODY_ID).innerHTML = ''; 129 $(WaterfallView.INFO_TBODY_ID).innerHTML = ''; 130 this.scaleFactor_ = 0.1; 131 }, 132 133 /** 134 * Changes scroll position of the window such that horizontally, everything 135 * within the specified range fits into the user's viewport. 136 */ 137 adjustToWindow_: function(windowStart, windowEnd) { 138 var waterfallLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth + 139 $(WaterfallView.INFO_TABLE_ID).offsetLeft + 140 $(WaterfallView.ID_HEADER_ID).offsetWidth; 141 var maxWidth = $(WaterfallView.MAIN_BOX_ID).offsetWidth - waterfallLeft; 142 var totalDuration = 0; 143 if (windowEnd != -1) { 144 totalDuration = windowEnd - windowStart; 145 } else { 146 for (var id in this.sourceIdToRowMap_) { 147 var row = this.sourceIdToRowMap_[id]; 148 var rowDuration = row.getEndTicks(); 149 if (totalDuration < rowDuration && !row.hide) { 150 totalDuration = rowDuration; 151 } 152 } 153 } 154 if (totalDuration <= 0) { 155 return; 156 } 157 this.scaleAll_(maxWidth / totalDuration); 158 $(WaterfallView.MAIN_BOX_ID).scrollLeft = 159 windowStart * this.scaleFactor_; 160 }, 161 162 /** Updates the time tick indicators. */ 163 updateTimeScale_: function(scaleFactor) { 164 var timePerTick = 1; 165 var minTickDistance = 20; 166 167 $(WaterfallView.TIME_SCALE_HEADER_ID).innerHTML = ''; 168 169 // Holder provides environment to prevent wrapping. 170 var timeTickRow = addNode($(WaterfallView.TIME_SCALE_HEADER_ID), 'div'); 171 timeTickRow.classList.add('waterfall-view-time-scale-row'); 172 173 var availableWidth = $(WaterfallView.BAR_TBODY_ID).clientWidth; 174 var tickDistance = scaleFactor * timePerTick; 175 176 while (tickDistance < minTickDistance) { 177 timePerTick = timePerTick * 10; 178 tickDistance = scaleFactor * timePerTick; 179 } 180 181 var tickCount = availableWidth / tickDistance; 182 for (var i = 0; i < tickCount; ++i) { 183 var timeCell = addNode(timeTickRow, 'div'); 184 setNodeWidth(timeCell, tickDistance); 185 timeCell.classList.add('waterfall-view-time-scale'); 186 timeCell.title = i * timePerTick + ' to ' + 187 (i + 1) * timePerTick + ' ms'; 188 // Red marker for every 5th bar. 189 if (i % 5 == 0) { 190 timeCell.classList.add('waterfall-view-time-scale-special'); 191 } 192 } 193 }, 194 195 /** 196 * Scales all existing rows by scaleFactor. 197 */ 198 scaleAll_: function(scaleFactor) { 199 this.scaleFactor_ = scaleFactor; 200 for (var id in this.sourceIdToRowMap_) { 201 var row = this.sourceIdToRowMap_[id]; 202 row.updateRow(); 203 } 204 this.updateTimeScale_(scaleFactor); 205 }, 206 207 scrollToZoom_: function(event) { 208 // To use scrolling to control zoom, hold down the alt key and scroll. 209 if ('wheelDelta' in event && event.altKey) { 210 event.preventDefault(); 211 var zoomFactor = Math.pow(MOUSE_WHEEL_ZOOM_RATE, 212 event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK); 213 214 var waterfallLeft = $(WaterfallView.ID_HEADER_ID).offsetWidth + 215 $(WaterfallView.URL_HEADER_ID).offsetWidth; 216 var oldCursorPosition = event.pageX + 217 $(WaterfallView.MAIN_BOX_ID).scrollLeft; 218 var oldCursorPositionInTable = oldCursorPosition - waterfallLeft; 219 220 this.scaleAll_(this.scaleFactor_ * zoomFactor); 221 222 // Shifts the view when scrolling. newScroll could be less than 0 or 223 // more than the maximum scroll position, but both cases are handled 224 // by the inbuilt scrollLeft implementation. 225 var newScroll = 226 oldCursorPositionInTable * zoomFactor - event.pageX + waterfallLeft; 227 $(WaterfallView.MAIN_BOX_ID).scrollLeft = newScroll; 228 } 229 }, 230 231 /** 232 * Positions the bar table such that it is in line with the right edge of 233 * the info table. 234 */ 235 positionBarTable_: function() { 236 var offsetLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth + 237 $(WaterfallView.INFO_TABLE_ID).offsetLeft; 238 $(WaterfallView.BAR_TABLE_ID).style.left = offsetLeft + 'px'; 239 }, 240 241 /** 242 * Moves the info table when the page is scrolled vertically, ensuring that 243 * the correct information is displayed on the page, and that no elements 244 * are blocked unnecessarily. 245 */ 246 scrollInfoTable_: function(event) { 247 $(WaterfallView.INFO_TABLE_ID).style.top = 248 $(WaterfallView.MAIN_BOX_ID).offsetTop + 249 $(WaterfallView.BAR_TABLE_ID).offsetTop - 250 $(WaterfallView.MAIN_BOX_ID).scrollTop + 'px'; 251 252 if ($(WaterfallView.INFO_TABLE_ID).offsetHeight > 253 $(WaterfallView.MAIN_BOX_ID).clientHeight) { 254 var scroll = $(WaterfallView.MAIN_BOX_ID).scrollTop; 255 var bottomClip = 256 $(WaterfallView.MAIN_BOX_ID).clientHeight - 257 $(WaterfallView.BAR_TABLE_ID).offsetTop + 258 $(WaterfallView.MAIN_BOX_ID).scrollTop; 259 // Clips the information table such that it does not cover the scroll 260 // bars or the controls bar. 261 $(WaterfallView.INFO_TABLE_ID).style.clip = 'rect(' + scroll + 262 'px auto ' + bottomClip + 'px auto)'; 263 } 264 }, 265 266 /** Parses user input, then calls adjustToWindow to shift that into view. */ 267 setStartEndTimes_: function() { 268 var windowStart = parseInt($(WaterfallView.START_TIME_ID).value); 269 var windowEnd = parseInt($(WaterfallView.END_TIME_ID).value); 270 if ($(WaterfallView.END_TIME_ID).value == '') { 271 windowEnd = -1; 272 } 273 if ($(WaterfallView.START_TIME_ID).value == '') { 274 windowStart = 0; 275 } 276 this.adjustToWindow_(windowStart, windowEnd); 277 }, 278 279 280 }; 281 282 return WaterfallView; 283 })(); 284