| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // TODO(viona): Write a README document/instructions. |
| |
| /** This view displays the event waterfall. */ |
| var WaterfallView = (function() { |
| 'use strict'; |
| |
| // We inherit from DivView. |
| var superClass = DivView; |
| |
| /** |
| * @constructor |
| */ |
| function WaterfallView() { |
| assertFirstConstructorCall(WaterfallView); |
| |
| // Call superclass's constructor. |
| superClass.call(this, WaterfallView.MAIN_BOX_ID); |
| |
| SourceTracker.getInstance().addSourceEntryObserver(this); |
| |
| // For adjusting the range of view. |
| $(WaterfallView.SCALE_ID).addEventListener( |
| 'click', this.setStartEndTimes_.bind(this), true); |
| |
| $(WaterfallView.MAIN_BOX_ID).addEventListener( |
| 'mousewheel', this.scrollToZoom_.bind(this), true); |
| |
| $(WaterfallView.MAIN_BOX_ID).addEventListener( |
| 'scroll', this.scrollInfoTable_.bind(this), true); |
| |
| this.initializeSourceList_(); |
| |
| window.onload = this.scrollInfoTable_(); |
| } |
| |
| WaterfallView.TAB_ID = 'tab-handle-waterfall'; |
| WaterfallView.TAB_NAME = 'Waterfall'; |
| WaterfallView.TAB_HASH = '#waterfall'; |
| |
| // IDs for special HTML elements in events_waterfall_view.html. |
| WaterfallView.MAIN_BOX_ID = 'waterfall-view-tab-content'; |
| WaterfallView.BAR_TABLE_ID = 'waterfall-view-time-bar-table'; |
| WaterfallView.BAR_TBODY_ID = 'waterfall-view-time-bar-tbody'; |
| WaterfallView.SCALE_ID = 'waterfall-view-adjust-to-window'; |
| WaterfallView.TIME_SCALE_HEADER_ID = 'waterfall-view-time-scale-labels'; |
| WaterfallView.TIME_RANGE_ID = 'waterfall-view-time-range-submit'; |
| WaterfallView.START_TIME_ID = 'waterfall-view-start-input'; |
| WaterfallView.END_TIME_ID = 'waterfall-view-end-input'; |
| WaterfallView.INFO_TABLE_ID = 'waterfall-view-information-table'; |
| WaterfallView.INFO_TBODY_ID = 'waterfall-view-information-tbody'; |
| WaterfallView.CONTROLS_ID = 'waterfall-view-controls'; |
| WaterfallView.ID_HEADER_ID = 'waterfall-view-id-header'; |
| WaterfallView.URL_HEADER_ID = 'waterfall-view-url-header'; |
| |
| // The number of units mouse wheel deltas increase for each tick of the |
| // wheel. |
| var MOUSE_WHEEL_UNITS_PER_CLICK = 120; |
| |
| // Amount we zoom for one vertical tick of the mouse wheel, as a ratio. |
| var MOUSE_WHEEL_ZOOM_RATE = 1.25; |
| // Amount we scroll for one horizontal tick of the mouse wheel, in pixels. |
| var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK; |
| |
| cr.addSingletonGetter(WaterfallView); |
| |
| WaterfallView.prototype = { |
| // Inherit the superclass's methods. |
| __proto__: superClass.prototype, |
| |
| /** |
| * Creates new WaterfallRows for URL Requests when the sourceEntries are |
| * updated if they do not already exist. |
| * Updates pre-existing WaterfallRows that correspond to updated sources. |
| */ |
| onSourceEntriesUpdated: function(sourceEntries) { |
| if (this.startTime_ == null && sourceEntries.length > 0) { |
| var logEntries = sourceEntries[0].getLogEntries(); |
| this.startTime_ = timeutil.convertTimeTicksToTime(logEntries[0].time); |
| // Initial scale factor. |
| this.scaleFactor_ = 0.1; |
| } |
| for (var i = 0; i < sourceEntries.length; ++i) { |
| var sourceEntry = sourceEntries[i]; |
| var id = sourceEntry.getSourceId(); |
| if (sourceEntry.getSourceType() == EventSourceType.URL_REQUEST) { |
| var row = this.sourceIdToRowMap_[id]; |
| if (!row) { |
| var newRow = new WaterfallRow(this, sourceEntry); |
| this.sourceIdToRowMap_[id] = newRow; |
| } else { |
| row.onSourceUpdated(); |
| } |
| } |
| } |
| this.scrollInfoTable_(); |
| this.positionBarTable_(); |
| this.updateTimeScale_(this.scaleFactor_); |
| }, |
| |
| onAllSourceEntriesDeleted: function() { |
| this.initializeSourceList_(); |
| }, |
| |
| onLoadLogFinish: function(data) { |
| return true; |
| }, |
| |
| getScaleFactor: function() { |
| return this.scaleFactor_; |
| }, |
| |
| getStartTime: function() { |
| return this.startTime_; |
| }, |
| |
| setGeometry: function(left, top, width, height) { |
| superClass.prototype.setGeometry.call(this, left, top, width, height); |
| this.scrollInfoTable_(); |
| }, |
| |
| show: function(isVisible) { |
| superClass.prototype.show.call(this, isVisible); |
| if (isVisible) { |
| this.scrollInfoTable_(); |
| } |
| }, |
| |
| /** |
| * Initializes the list of source entries. If source entries are already |
| * being displayed, removes them all in the process. |
| */ |
| initializeSourceList_: function() { |
| this.sourceIdToRowMap_ = {}; |
| $(WaterfallView.BAR_TBODY_ID).innerHTML = ''; |
| $(WaterfallView.INFO_TBODY_ID).innerHTML = ''; |
| this.startTime_ = null; |
| this.scaleFactor_ = null; |
| }, |
| |
| /** |
| * Changes scroll position of the window such that horizontally, everything |
| * within the specified range fits into the user's viewport. |
| */ |
| adjustToWindow_: function(windowStart, windowEnd) { |
| var waterfallLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth + |
| $(WaterfallView.INFO_TABLE_ID).offsetLeft + |
| $(WaterfallView.ID_HEADER_ID).offsetWidth; |
| var maxWidth = $(WaterfallView.MAIN_BOX_ID).offsetWidth - waterfallLeft; |
| var totalDuration = 0; |
| if (windowEnd != -1) { |
| totalDuration = windowEnd - windowStart; |
| } else { |
| for (var id in this.sourceIdToRowMap_) { |
| var row = this.sourceIdToRowMap_[id]; |
| var rowDuration = row.getEndTime() - this.startTime_; |
| if (totalDuration < rowDuration && !row.hide) { |
| totalDuration = rowDuration; |
| } |
| } |
| } |
| if (totalDuration <= 0) { |
| return; |
| } |
| this.scaleAll_(maxWidth / totalDuration); |
| $(WaterfallView.MAIN_BOX_ID).scrollLeft = |
| windowStart * this.scaleFactor_; |
| }, |
| |
| /** Updates the time tick indicators. */ |
| updateTimeScale_: function(scaleFactor) { |
| var timePerTick = 1; |
| var minTickDistance = 20; |
| |
| $(WaterfallView.TIME_SCALE_HEADER_ID).innerHTML = ''; |
| |
| // Holder provides environment to prevent wrapping. |
| var timeTickRow = addNode($(WaterfallView.TIME_SCALE_HEADER_ID), 'div'); |
| timeTickRow.classList.add('waterfall-view-time-scale-row'); |
| |
| var availableWidth = $(WaterfallView.BAR_TBODY_ID).clientWidth; |
| var tickDistance = scaleFactor * timePerTick; |
| |
| while (tickDistance < minTickDistance) { |
| timePerTick = timePerTick * 10; |
| tickDistance = scaleFactor * timePerTick; |
| } |
| |
| var tickCount = availableWidth / tickDistance; |
| for (var i = 0; i < tickCount; ++i) { |
| var timeCell = addNode(timeTickRow, 'div'); |
| setNodeWidth(timeCell, tickDistance); |
| timeCell.classList.add('waterfall-view-time-scale'); |
| timeCell.title = i * timePerTick + ' to ' + |
| (i + 1) * timePerTick + ' ms'; |
| // Red marker for every 5th bar. |
| if (i % 5 == 0) { |
| timeCell.classList.add('waterfall-view-time-scale-special'); |
| } |
| } |
| }, |
| |
| /** |
| * Scales all existing rows by scaleFactor. |
| */ |
| scaleAll_: function(scaleFactor) { |
| this.scaleFactor_ = scaleFactor; |
| for (var id in this.sourceIdToRowMap_) { |
| var row = this.sourceIdToRowMap_[id]; |
| row.updateRow(); |
| } |
| this.updateTimeScale_(scaleFactor); |
| }, |
| |
| scrollToZoom_: function(event) { |
| // To use scrolling to control zoom, hold down the alt key and scroll. |
| if ('wheelDelta' in event && event.altKey) { |
| event.preventDefault(); |
| var zoomFactor = Math.pow(MOUSE_WHEEL_ZOOM_RATE, |
| event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK); |
| |
| var waterfallLeft = $(WaterfallView.ID_HEADER_ID).offsetWidth + |
| $(WaterfallView.URL_HEADER_ID).offsetWidth; |
| var oldCursorPosition = event.pageX + |
| $(WaterfallView.MAIN_BOX_ID).scrollLeft; |
| var oldCursorPositionInTable = oldCursorPosition - waterfallLeft; |
| |
| this.scaleAll_(this.scaleFactor_ * zoomFactor); |
| |
| // Shifts the view when scrolling. newScroll could be less than 0 or |
| // more than the maximum scroll position, but both cases are handled |
| // by the inbuilt scrollLeft implementation. |
| var newScroll = |
| oldCursorPositionInTable * zoomFactor - event.pageX + waterfallLeft; |
| $(WaterfallView.MAIN_BOX_ID).scrollLeft = newScroll; |
| } |
| }, |
| |
| /** |
| * Positions the bar table such that it is in line with the right edge of |
| * the info table. |
| */ |
| positionBarTable_: function() { |
| var offsetLeft = $(WaterfallView.INFO_TABLE_ID).offsetWidth + |
| $(WaterfallView.INFO_TABLE_ID).offsetLeft; |
| $(WaterfallView.BAR_TABLE_ID).style.left = offsetLeft + 'px'; |
| }, |
| |
| /** |
| * Moves the info table when the page is scrolled vertically, ensuring that |
| * the correct information is displayed on the page, and that no elements |
| * are blocked unnecessarily. |
| */ |
| scrollInfoTable_: function(event) { |
| $(WaterfallView.INFO_TABLE_ID).style.top = |
| $(WaterfallView.MAIN_BOX_ID).offsetTop + |
| $(WaterfallView.BAR_TABLE_ID).offsetTop - |
| $(WaterfallView.MAIN_BOX_ID).scrollTop + 'px'; |
| |
| if ($(WaterfallView.INFO_TABLE_ID).offsetHeight > |
| $(WaterfallView.MAIN_BOX_ID).clientHeight) { |
| var scroll = $(WaterfallView.MAIN_BOX_ID).scrollTop; |
| var bottomClip = |
| $(WaterfallView.MAIN_BOX_ID).clientHeight - |
| $(WaterfallView.BAR_TABLE_ID).offsetTop + |
| $(WaterfallView.MAIN_BOX_ID).scrollTop; |
| // Clips the information table such that it does not cover the scroll |
| // bars or the controls bar. |
| $(WaterfallView.INFO_TABLE_ID).style.clip = 'rect(' + scroll + |
| 'px auto ' + bottomClip + 'px auto)'; |
| } |
| }, |
| |
| /** Parses user input, then calls adjustToWindow to shift that into view. */ |
| setStartEndTimes_: function() { |
| var windowStart = parseInt($(WaterfallView.START_TIME_ID).value); |
| var windowEnd = parseInt($(WaterfallView.END_TIME_ID).value); |
| if ($(WaterfallView.END_TIME_ID).value == '') { |
| windowEnd = -1; |
| } |
| if ($(WaterfallView.START_TIME_ID).value == '') { |
| windowStart = 0; |
| } |
| this.adjustToWindow_(windowStart, windowEnd); |
| }, |
| |
| |
| }; |
| |
| return WaterfallView; |
| })(); |