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 this.initializeSourceList_(); 33 } 34 35 WaterfallView.TAB_ID = 'tab-handle-waterfall'; 36 WaterfallView.TAB_NAME = 'Waterfall'; 37 WaterfallView.TAB_HASH = '#waterfall'; 38 39 // IDs for special HTML elements in events_waterfall_view.html. 40 WaterfallView.MAIN_BOX_ID = 'waterfall-view-tab-content'; 41 WaterfallView.TBODY_ID = 'waterfall-view-source-list-tbody'; 42 WaterfallView.SCALE_ID = 'waterfall-view-adjust-to-window'; 43 WaterfallView.ID_HEADER_ID = 'waterfall-view-source-id'; 44 WaterfallView.SOURCE_HEADER_ID = 'waterfall-view-source-header'; 45 WaterfallView.TIME_SCALE_HEADER_ID = 'waterfall-view-time-scale-labels'; 46 WaterfallView.TIME_RANGE_ID = 'waterfall-view-time-range-submit'; 47 WaterfallView.START_TIME_ID = 'waterfall-view-start-input'; 48 WaterfallView.END_TIME_ID = 'waterfall-view-end-input'; 49 50 // The number of units mouse wheel deltas increase for each tick of the 51 // wheel. 52 var MOUSE_WHEEL_UNITS_PER_CLICK = 120; 53 54 // Amount we zoom for one vertical tick of the mouse wheel, as a ratio. 55 var MOUSE_WHEEL_ZOOM_RATE = 1.25; 56 // Amount we scroll for one horizontal tick of the mouse wheel, in pixels. 57 var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK; 58 59 cr.addSingletonGetter(WaterfallView); 60 61 WaterfallView.prototype = { 62 // Inherit the superclass's methods. 63 __proto__: superClass.prototype, 64 65 /** 66 * Creates new WaterfallRows for URL Requests when the sourceEntries are 67 * updated if they do not already exist. 68 * Updates pre-existing WaterfallRows that correspond to updated sources. 69 */ 70 onSourceEntriesUpdated: function(sourceEntries) { 71 if (this.startTime_ == null && sourceEntries.length > 0) { 72 var logEntries = sourceEntries[0].getLogEntries(); 73 this.startTime_ = timeutil.convertTimeTicksToTime(logEntries[0].time); 74 // Initial scale factor. 75 this.scaleFactor_ = 0.1; 76 this.eventsList_ = this.createEventsList_(); 77 } 78 for (var i = 0; i < sourceEntries.length; ++i) { 79 var sourceEntry = sourceEntries[i]; 80 var id = sourceEntry.getSourceId(); 81 for (var j = 0; j < this.eventsList_.length; ++j) { 82 var eventObject = this.eventsList_[j]; 83 if (sourceEntry.getSourceType() == eventObject.mainEvent) { 84 var row = this.sourceIdToRowMap_[id]; 85 if (!row) { 86 var newRow = new WaterfallRow( 87 this, sourceEntry, eventObject.importantEventTypes); 88 if (newRow == undefined) { 89 console.log(sourceEntry); 90 } 91 this.sourceIdToRowMap_[id] = newRow; 92 } else { 93 row.onSourceUpdated(); 94 } 95 } 96 } 97 } 98 this.updateTimeScale_(this.scaleFactor_); 99 }, 100 101 onAllSourceEntriesDeleted: function() { 102 this.initializeSourceList_(); 103 }, 104 105 onLoadLogFinish: function(data) { 106 return true; 107 }, 108 109 getScaleFactor: function() { 110 return this.scaleFactor_; 111 }, 112 113 getStartTime: function() { 114 return this.startTime_; 115 }, 116 117 /** 118 * Initializes the list of source entries. If source entries are already 119 * being displayed, removes them all in the process. 120 */ 121 initializeSourceList_: function() { 122 this.sourceIdToRowMap_ = {}; 123 $(WaterfallView.TBODY_ID).innerHTML = ''; 124 this.startTime_ = null; 125 this.scaleFactor_ = null; 126 }, 127 128 /** 129 * Changes scroll position of the window such that horizontally, everything 130 * within the specified range fits into the user's viewport. 131 * TODO(viona): Find a way to get rid of the magic number. 132 */ 133 adjustToWindow_: function(windowStart, windowEnd) { 134 var maxWidth = $(WaterfallView.MAIN_BOX_ID).offsetWidth - 50; 135 $(WaterfallView.TBODY_ID).width = maxWidth + 'px'; 136 var totalDuration = 0; 137 if (windowEnd != -1) { 138 totalDuration = windowEnd - windowStart; 139 } else { 140 for (var id in this.sourceIdToRowMap_) { 141 var row = this.sourceIdToRowMap_[id]; 142 var rowDuration = row.getEndTime() - this.startTime_; 143 if (totalDuration < rowDuration && !row.hide) { 144 totalDuration = rowDuration; 145 } 146 } 147 } 148 if (totalDuration <= 0) { 149 return; 150 } 151 this.scaleAll_(maxWidth / totalDuration); 152 var waterfallLeft = $(WaterfallView.TIME_SCALE_HEADER_ID).offsetLeft; 153 $(WaterfallView.MAIN_BOX_ID).scrollLeft = 154 windowStart * this.scaleFactor_ + waterfallLeft; 155 }, 156 157 // Updates the time tick indicators. 158 updateTimeScale_: function(scaleFactor) { 159 var timePerTick = 1; 160 var minTickDistance = 20; 161 162 $(WaterfallView.TIME_SCALE_HEADER_ID).innerHTML = ''; 163 164 // Holder provides environment to prevent wrapping. 165 var timeTickRow = addNode($(WaterfallView.TIME_SCALE_HEADER_ID), 'div'); 166 timeTickRow.classList.add('waterfall-view-time-scale-row'); 167 168 var availableWidth = $(WaterfallView.TBODY_ID).clientWidth; 169 var tickDistance = scaleFactor * timePerTick; 170 171 while (tickDistance < minTickDistance) { 172 timePerTick = timePerTick * 10; 173 tickDistance = scaleFactor * timePerTick; 174 } 175 176 var tickCount = availableWidth / tickDistance; 177 for (var i = 0; i < tickCount; ++i) { 178 var timeCell = addNode(timeTickRow, 'div'); 179 setNodeWidth(timeCell, tickDistance); 180 timeCell.classList.add('waterfall-view-time-scale'); 181 timeCell.title = i * timePerTick + ' to ' + 182 (i + 1) * timePerTick + ' ms'; 183 // Red marker for every 5th bar. 184 if (i % 5 == 0) { 185 timeCell.classList.add('waterfall-view-time-scale-special'); 186 } 187 } 188 }, 189 190 /** 191 * Scales all existing rows by scaleFactor. 192 */ 193 scaleAll_: function(scaleFactor) { 194 this.scaleFactor_ = scaleFactor; 195 for (var id in this.sourceIdToRowMap_) { 196 var row = this.sourceIdToRowMap_[id]; 197 row.updateRow(); 198 } 199 this.updateTimeScale_(scaleFactor); 200 }, 201 202 scrollToZoom_: function(event) { 203 // To use scrolling to control zoom, hold down the alt key and scroll. 204 if ('wheelDelta' in event && event.altKey) { 205 var zoomFactor = Math.pow(MOUSE_WHEEL_ZOOM_RATE, 206 event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK); 207 208 var waterfallLeft = $(WaterfallView.TIME_SCALE_HEADER_ID).offsetLeft; 209 var oldCursorPosition = event.pageX + 210 $(WaterfallView.MAIN_BOX_ID).scrollLeft; 211 var oldCursorPositionInTable = oldCursorPosition - waterfallLeft; 212 213 this.scaleAll_(this.scaleFactor_ * zoomFactor); 214 215 // Shifts the view when scrolling. newScroll could be less than 0 or 216 // more than the maximum scroll position, but both cases are handled 217 // by the inbuilt scrollLeft implementation. 218 var newScroll = 219 oldCursorPositionInTable * zoomFactor - event.pageX + waterfallLeft; 220 $(WaterfallView.MAIN_BOX_ID).scrollLeft = newScroll; 221 } 222 }, 223 224 // Parses user input, then calls adjustToWindow to shift that into view. 225 setStartEndTimes_: function() { 226 var windowStart = parseInt($(WaterfallView.START_TIME_ID).value); 227 var windowEnd = parseInt($(WaterfallView.END_TIME_ID).value); 228 if ($(WaterfallView.END_TIME_ID).value == '') { 229 windowEnd = -1; 230 } 231 if ($(WaterfallView.START_TIME_ID).value == '') { 232 windowStart = 0; 233 } 234 this.adjustToWindow_(windowStart, windowEnd); 235 }, 236 237 // Provides a structure that can be used to define source entry types and 238 // the log entries that are of interest. 239 createEventsList_: function() { 240 var eventsList = []; 241 // Creating list of events. 242 // TODO(viona): This is hard-coded. Consider user-input. 243 // Also, work on getting socket information inlined in the 244 // relevant URL_REQUEST events. 245 var urlRequestEvents = [ 246 EventType.HTTP_STREAM_REQUEST, 247 EventType.HTTP_STREAM_REQUEST_BOUND_TO_JOB, 248 EventType.HTTP_TRANSACTION_READ_HEADERS 249 ]; 250 251 eventsList.push(this.createEventTypeObjects_( 252 EventSourceType.URL_REQUEST, urlRequestEvents)); 253 254 var httpStreamJobEvents = [EventType.PROXY_SERVICE]; 255 eventsList.push(this.createEventTypeObjects_( 256 EventSourceType.HTTP_STREAM_JOB, httpStreamJobEvents)); 257 258 return eventsList; 259 }, 260 261 // Creates objects that can be used to define source entry types and 262 // the log entries that are of interest. 263 createEventTypeObjects_: function(mainEventType, eventTypesList) { 264 var eventTypeObject = { 265 mainEvent: mainEventType, 266 importantEventTypes: eventTypesList 267 }; 268 return eventTypeObject; 269 }, 270 }; 271 272 return WaterfallView; 273 })(); 274