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 if (this.startTime_ == null && sourceEntries.length > 0) { 81 var logEntries = sourceEntries[0].getLogEntries(); 82 this.startTime_ = timeutil.convertTimeTicksToTime(logEntries[0].time); 83 // Initial scale factor. 84 this.scaleFactor_ = 0.1; 85 } 86 for (var i = 0; i < sourceEntries.length; ++i) { 87 var sourceEntry = sourceEntries[i]; 88 var id = sourceEntry.getSourceId(); 89 if (sourceEntry.getSourceType() == EventSourceType.URL_REQUEST) { 90 var row = this.sourceIdToRowMap_[id]; 91 if (!row) { 92 var newRow = new WaterfallRow(this, sourceEntry); 93 this.sourceIdToRowMap_[id] = newRow; 94 } else { 95 row.onSourceUpdated(); 96 } 97 } 98 } 99 this.scrollInfoTable_(); 100 this.positionBarTable_(); 101 this.updateTimeScale_(this.scaleFactor_); 102 }, 103 104 onAllSourceEntriesDeleted: function() { 105 this.initializeSourceList_(); 106 }, 107 108 onLoadLogFinish: function(data) { 109 return true; 110 }, 111 112 getScaleFactor: function() { 113 return this.scaleFactor_; 114 }, 115 116 getStartTime: function() { 117 return this.startTime_; 118 }, 119 120 setGeometry: function(left, top, width, height) { 121 superClass.prototype.setGeometry.call(this, left, top, width, height); 122 this.scrollInfoTable_(); 123 }, 124 125 show: function(isVisible) { 126 superClass.prototype.show.call(this, isVisible); 127 if (isVisible) { 128 this.scrollInfoTable_(); 129 } 130 }, 131 132 /** 133 * Initializes the list of source entries. If source entries are already 134 * being displayed, removes them all in the process. 135 */ 136 initializeSourceList_: function() { 137 this.sourceIdToRowMap_ = {}; 138 $(WaterfallView.BAR_TBODY_ID).innerHTML = ''; 139 $(WaterfallView.INFO_TBODY_ID).innerHTML = ''; 140 this.startTime_ = null; 141 this.scaleFactor_ = null; 142 }, 143 144 /** 145 * Changes scroll position of the window such that horizontally, everything 146 * within the specified range fits into the user's viewport. 147 */ 148 adjustToWindow_: function(windowStart, windowEnd) { 149 var waterfallLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth + 150 $(WaterfallView.INFO_TABLE_ID).offsetLeft + 151 $(WaterfallView.ID_HEADER_ID).offsetWidth; 152 var maxWidth = $(WaterfallView.MAIN_BOX_ID).offsetWidth - waterfallLeft; 153 var totalDuration = 0; 154 if (windowEnd != -1) { 155 totalDuration = windowEnd - windowStart; 156 } else { 157 for (var id in this.sourceIdToRowMap_) { 158 var row = this.sourceIdToRowMap_[id]; 159 var rowDuration = row.getEndTime() - this.startTime_; 160 if (totalDuration < rowDuration && !row.hide) { 161 totalDuration = rowDuration; 162 } 163 } 164 } 165 if (totalDuration <= 0) { 166 return; 167 } 168 this.scaleAll_(maxWidth / totalDuration); 169 $(WaterfallView.MAIN_BOX_ID).scrollLeft = 170 windowStart * this.scaleFactor_; 171 }, 172 173 /** Updates the time tick indicators. */ 174 updateTimeScale_: function(scaleFactor) { 175 var timePerTick = 1; 176 var minTickDistance = 20; 177 178 $(WaterfallView.TIME_SCALE_HEADER_ID).innerHTML = ''; 179 180 // Holder provides environment to prevent wrapping. 181 var timeTickRow = addNode($(WaterfallView.TIME_SCALE_HEADER_ID), 'div'); 182 timeTickRow.classList.add('waterfall-view-time-scale-row'); 183 184 var availableWidth = $(WaterfallView.BAR_TBODY_ID).clientWidth; 185 var tickDistance = scaleFactor * timePerTick; 186 187 while (tickDistance < minTickDistance) { 188 timePerTick = timePerTick * 10; 189 tickDistance = scaleFactor * timePerTick; 190 } 191 192 var tickCount = availableWidth / tickDistance; 193 for (var i = 0; i < tickCount; ++i) { 194 var timeCell = addNode(timeTickRow, 'div'); 195 setNodeWidth(timeCell, tickDistance); 196 timeCell.classList.add('waterfall-view-time-scale'); 197 timeCell.title = i * timePerTick + ' to ' + 198 (i + 1) * timePerTick + ' ms'; 199 // Red marker for every 5th bar. 200 if (i % 5 == 0) { 201 timeCell.classList.add('waterfall-view-time-scale-special'); 202 } 203 } 204 }, 205 206 /** 207 * Scales all existing rows by scaleFactor. 208 */ 209 scaleAll_: function(scaleFactor) { 210 this.scaleFactor_ = scaleFactor; 211 for (var id in this.sourceIdToRowMap_) { 212 var row = this.sourceIdToRowMap_[id]; 213 row.updateRow(); 214 } 215 this.updateTimeScale_(scaleFactor); 216 }, 217 218 scrollToZoom_: function(event) { 219 // To use scrolling to control zoom, hold down the alt key and scroll. 220 if ('wheelDelta' in event && event.altKey) { 221 event.preventDefault(); 222 var zoomFactor = Math.pow(MOUSE_WHEEL_ZOOM_RATE, 223 event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK); 224 225 var waterfallLeft = $(WaterfallView.ID_HEADER_ID).offsetWidth + 226 $(WaterfallView.URL_HEADER_ID).offsetWidth; 227 var oldCursorPosition = event.pageX + 228 $(WaterfallView.MAIN_BOX_ID).scrollLeft; 229 var oldCursorPositionInTable = oldCursorPosition - waterfallLeft; 230 231 this.scaleAll_(this.scaleFactor_ * zoomFactor); 232 233 // Shifts the view when scrolling. newScroll could be less than 0 or 234 // more than the maximum scroll position, but both cases are handled 235 // by the inbuilt scrollLeft implementation. 236 var newScroll = 237 oldCursorPositionInTable * zoomFactor - event.pageX + waterfallLeft; 238 $(WaterfallView.MAIN_BOX_ID).scrollLeft = newScroll; 239 } 240 }, 241 242 /** 243 * Positions the bar table such that it is in line with the right edge of 244 * the info table. 245 */ 246 positionBarTable_: function() { 247 var offsetLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth + 248 $(WaterfallView.INFO_TABLE_ID).offsetLeft; 249 $(WaterfallView.BAR_TABLE_ID).style.left = offsetLeft + 'px'; 250 }, 251 252 /** 253 * Moves the info table when the page is scrolled vertically, ensuring that 254 * the correct information is displayed on the page, and that no elements 255 * are blocked unnecessarily. 256 */ 257 scrollInfoTable_: function(event) { 258 $(WaterfallView.INFO_TABLE_ID).style.top = 259 $(WaterfallView.MAIN_BOX_ID).offsetTop + 260 $(WaterfallView.BAR_TABLE_ID).offsetTop - 261 $(WaterfallView.MAIN_BOX_ID).scrollTop + 'px'; 262 263 if ($(WaterfallView.INFO_TABLE_ID).offsetHeight > 264 $(WaterfallView.MAIN_BOX_ID).clientHeight) { 265 var scroll = $(WaterfallView.MAIN_BOX_ID).scrollTop; 266 var bottomClip = 267 $(WaterfallView.MAIN_BOX_ID).clientHeight - 268 $(WaterfallView.BAR_TABLE_ID).offsetTop + 269 $(WaterfallView.MAIN_BOX_ID).scrollTop; 270 // Clips the information table such that it does not cover the scroll 271 // bars or the controls bar. 272 $(WaterfallView.INFO_TABLE_ID).style.clip = 'rect(' + scroll + 273 'px auto ' + bottomClip + 'px auto)'; 274 } 275 }, 276 277 /** Parses user input, then calls adjustToWindow to shift that into view. */ 278 setStartEndTimes_: function() { 279 var windowStart = parseInt($(WaterfallView.START_TIME_ID).value); 280 var windowEnd = parseInt($(WaterfallView.END_TIME_ID).value); 281 if ($(WaterfallView.END_TIME_ID).value == '') { 282 windowEnd = -1; 283 } 284 if ($(WaterfallView.START_TIME_ID).value == '') { 285 windowStart = 0; 286 } 287 this.adjustToWindow_(windowStart, windowEnd); 288 }, 289 290 291 }; 292 293 return WaterfallView; 294 })(); 295