Commit e3210f6f authored by mmenke@chromium.org's avatar mmenke@chromium.org

Add a timeline view to about:net-internals.

It shows a graph of open/active sockets, open URL requests,
open DNS requests, up/down bandwidth, and disk cache
bandwidth.

BUG=99386
TEST=NetInternalsTest.NetInternalsTimelineView*

Review URL: http://codereview.chromium.org/8474001

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@110553 0039d316-1c4b-4281-b951-d872f2087c98
parent 3ed32a81
...@@ -36,7 +36,7 @@ var CaptureView = (function() { ...@@ -36,7 +36,7 @@ var CaptureView = (function() {
this.updateEventCounts_(); this.updateEventCounts_();
g_browser.sourceTracker.addObserver(this); g_browser.sourceTracker.addSourceEntryObserver(this);
} }
// ID for special HTML element in category_tabs.html // ID for special HTML element in category_tabs.html
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<a href="#import" id=tab-handle-import>Import</a> <a href="#import" id=tab-handle-import>Import</a>
<a href="#proxy" id=tab-handle-proxy>Proxy</a> <a href="#proxy" id=tab-handle-proxy>Proxy</a>
<a href="#events" id=tab-handle-events>Events</a> <a href="#events" id=tab-handle-events>Events</a>
<a href="#timeline" id=tab-handle-timeline>Timeline</a>
<a href="#dns" id=tab-handle-dns>DNS</a> <a href="#dns" id=tab-handle-dns>DNS</a>
<a href="#sockets" id=tab-handle-sockets>Sockets</a> <a href="#sockets" id=tab-handle-sockets>Sockets</a>
<a href="#spdy" id=tab-handle-spdy>SPDY</a> <a href="#spdy" id=tab-handle-spdy>SPDY</a>
......
...@@ -5,35 +5,18 @@ ...@@ -5,35 +5,18 @@
var DetailsView = (function() { var DetailsView = (function() {
'use strict'; 'use strict';
// We inherit from VerticalSplitView. // We inherit from DivView.
var superClass = VerticalSplitView; var superClass = DivView;
/** /**
* The DetailsView handles the tabbed view that displays either the "log" or * The DetailsView displays the "log" view. This class keeps track of the
* "timeline" view. This class keeps track of what the current view is, and * selected SourceEntries, and repaints when they change.
* invalidates the specific view each time the selected data has changed.
* *
* @constructor * @constructor
*/ */
function DetailsView(tabHandlesContainerId, function DetailsView(boxId) {
logTabId, superClass.call(this, boxId);
timelineTabId, this.sourceEntries_ = [];
logBoxId,
timelineBoxId) {
var tabSwitcher = new TabSwitcherView();
superClass.call(this, new DivView(tabHandlesContainerId), tabSwitcher);
this.tabSwitcher_ = tabSwitcher;
this.logView_ = new DetailsLogView(logBoxId);
this.timelineView_ = new DetailsTimelineView(timelineBoxId);
this.tabSwitcher_.addTab(logTabId, this.logView_, true, true);
this.tabSwitcher_.addTab(timelineTabId, this.timelineView_, true, true);
// Default to the log view.
this.tabSwitcher_.switchToTab(logTabId, null);
} }
// The delay between updates to repaint. // The delay between updates to repaint.
...@@ -43,17 +26,32 @@ var DetailsView = (function() { ...@@ -43,17 +26,32 @@ var DetailsView = (function() {
// Inherit the superclass's methods. // Inherit the superclass's methods.
__proto__: superClass.prototype, __proto__: superClass.prototype,
/** setData: function(sourceEntries) {
* Updates the data this view is using.
*/
setData: function(currentData) {
// Make a copy of the array (in case the caller mutates it), and sort it // Make a copy of the array (in case the caller mutates it), and sort it
// by the source ID. // by the source ID.
var sortedCurrentData = createSortedCopy(currentData); this.sourceEntries_ = createSortedCopy(sourceEntries);
// TODO(eroman): Should not access private members of TabSwitcherView. // Repaint the view.
for (var i = 0; i < this.tabSwitcher_.tabs_.length; ++i) if (this.isVisible() && !this.outstandingRepaint_) {
this.tabSwitcher_.tabs_[i].contentView.setData(sortedCurrentData); this.outstandingRepaint_ = true;
window.setTimeout(this.repaint.bind(this),
REPAINT_TIMEOUT_MS);
}
},
repaint: function() {
this.outstandingRepaint_ = false;
this.getNode().innerHTML = '';
PaintLogView(this.sourceEntries_, this.getNode());
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
if (isVisible) {
this.repaint();
} else {
this.getNode().innerHTML = '';
}
} }
}; };
...@@ -65,108 +63,5 @@ var DetailsView = (function() { ...@@ -65,108 +63,5 @@ var DetailsView = (function() {
return sortedArray; return sortedArray;
} }
//---------------------------------------------------------------------------
var DetailsSubView = (function() {
// We inherit from DivView.
var superClass = DivView;
/**
* Base class for the Log view and Timeline view.
*
* @constructor
*/
function DetailsSubView(boxId) {
superClass.call(this, boxId);
this.sourceEntries_ = [];
}
DetailsSubView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
setData: function(sourceEntries) {
this.sourceEntries_ = sourceEntries;
// Repaint the view.
if (this.isVisible() && !this.outstandingRepaint_) {
this.outstandingRepaint_ = true;
window.setTimeout(this.repaint.bind(this),
REPAINT_TIMEOUT_MS);
}
},
repaint: function() {
this.outstandingRepaint_ = false;
this.getNode().innerHTML = '';
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
if (isVisible) {
this.repaint();
} else {
this.getNode().innerHTML = '';
}
}
};
return DetailsSubView;
})();
//---------------------------------------------------------------------------
var DetailsLogView = (function() {
// We inherit from DetailsSubView.
var superClass = DetailsSubView;
/**
* Subview that is displayed in the log tab.
* @constructor
*/
function DetailsLogView(boxId) {
superClass.call(this, boxId);
}
DetailsLogView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
repaint: function() {
superClass.prototype.repaint.call(this);
PaintLogView(this.sourceEntries_, this.getNode());
}
};
return DetailsLogView;
})();
//---------------------------------------------------------------------------
var DetailsTimelineView = (function() {
// We inherit from DetailsSubView.
var superClass = DetailsSubView;
/**
* Subview that is displayed in the timeline tab.
* @constructor
*/
function DetailsTimelineView(boxId) {
superClass.call(this, boxId);
}
DetailsTimelineView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
repaint: function() {
superClass.prototype.repaint.call(this);
PaintTimelineView(this.sourceEntries_, this.getNode());
}
};
return DetailsTimelineView;
})();
return DetailsView; return DetailsView;
})(); })();
...@@ -100,7 +100,7 @@ var DnsView = (function() { ...@@ -100,7 +100,7 @@ var DnsView = (function() {
// Date the cache was logged. This will be either now, when actively // Date the cache was logged. This will be either now, when actively
// logging data, or the date the log dump was created. // logging data, or the date the log dump was created.
var logDate; var logDate;
if (MainView.getInstance().isViewingLoadedLog()) { if (MainView.isViewingLoadedLog()) {
if (typeof ClientInfo.numericDate == 'number') { if (typeof ClientInfo.numericDate == 'number') {
logDate = new Date(ClientInfo.numericDate); logDate = new Date(ClientInfo.numericDate);
} else { } else {
......
...@@ -99,33 +99,3 @@ found in the LICENSE file. ...@@ -99,33 +99,3 @@ found in the LICENSE file.
#events-view-source-list-tbody .source_NONE { #events-view-source-list-tbody .source_NONE {
color: red; color: red;
} }
.events-view-tab-switcher {
margin-top: 10px;
margin-left: 10px;
}
.events-view-tab-switcher th {
background: rgb(229,236,249);
cursor: pointer;
background-clip: border-box;
border-top-left-radius: 5px 5px;
border-top-right-radius: 5px 5px;
padding-left: 4px;
padding-top: 4px;
padding-right: 4px;
font-size: 12px;
margin-left: 30px;
}
.events-view-tab-switcher th.selected, .events-view-tab-switcher-line {
background: rgb(195,217,255);
}
.events-view-tab-switcher-line {
height: 10px;
}
#events-view-details-tab-handles {
border: 1px solid white;
}
...@@ -33,15 +33,4 @@ ...@@ -33,15 +33,4 @@
<!-- Splitter Box: This is a handle to resize the vertical divider --> <!-- Splitter Box: This is a handle to resize the vertical divider -->
<div id=events-view-splitter-box class=vertical-splitter></div> <div id=events-view-splitter-box class=vertical-splitter></div>
<!-- Details box: This is the panel on the right which shows information --> <!-- Details box: This is the panel on the right which shows information -->
<div id=events-view-details-tab-handles>
<table class=events-view-tab-switcher cellspacing=0>
<tr>
<th id=events-view-details-log-tab>Log</th>
<td>&nbsp;</td>
<th id=events-view-details-timeline-tab>Timeline</th>
</tr>
</table>
<div class=events-view-tab-switcher-line></div>
</div>
<div id=events-view-details-log-box></div> <div id=events-view-details-log-box></div>
<div id=events-view-details-timeline-box></div>
...@@ -46,16 +46,12 @@ var EventsView = (function() { ...@@ -46,16 +46,12 @@ var EventsView = (function() {
new DivView(EventsView.MIDDLE_BOX_ID), new DivView(EventsView.MIDDLE_BOX_ID),
new DivView(EventsView.BOTTOM_BAR_ID)); new DivView(EventsView.BOTTOM_BAR_ID));
this.detailsView_ = new DetailsView(EventsView.TAB_HANDLES_CONTAINER_ID, this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID);
EventsView.LOG_TAB_ID,
EventsView.TIMELINE_TAB_ID,
EventsView.DETAILS_LOG_BOX_ID,
EventsView.DETAILS_TIMELINE_BOX_ID);
this.splitterView_ = new ResizableVerticalSplitView( this.splitterView_ = new ResizableVerticalSplitView(
leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID)); leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID));
g_browser.sourceTracker.addObserver(this); g_browser.sourceTracker.addSourceEntryObserver(this);
this.tableBody_ = $(EventsView.TBODY_ID); this.tableBody_ = $(EventsView.TBODY_ID);
...@@ -102,11 +98,7 @@ var EventsView = (function() { ...@@ -102,11 +98,7 @@ var EventsView = (function() {
EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id'; EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id';
EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source'; EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source';
EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description'; EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description';
EventsView.TAB_HANDLES_CONTAINER_ID = 'events-view-details-tab-handles';
EventsView.LOG_TAB_ID = 'events-view-details-log-tab';
EventsView.TIMELINE_TAB_ID = 'events-view-details-timeline-tab';
EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box'; EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box';
EventsView.DETAILS_TIMELINE_BOX_ID = 'events-view-details-timeline-box';
EventsView.TOPBAR_ID = 'events-view-filter-box'; EventsView.TOPBAR_ID = 'events-view-filter-box';
EventsView.MIDDLE_BOX_ID = 'events-view-source-list'; EventsView.MIDDLE_BOX_ID = 'events-view-source-list';
EventsView.BOTTOM_BAR_ID = 'events-view-action-box'; EventsView.BOTTOM_BAR_ID = 'events-view-action-box';
......
// Copyright (c) 2011 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.
/**
* This view consists of two nested divs. The outer one has a horizontal
* scrollbar and the inner one has a height of 1 pixel and a width set to
* allow an appropriate scroll range. The view reports scroll events to
* a callback specified on construction.
*
* All this funkiness is necessary because there is no HTML scroll control.
* TODO(mmenke): Consider implementing our own scrollbar directly.
*/
var HorizontalScrollbarView = (function() {
'use strict';
// We inherit from DivView.
var superClass = DivView;
/**
* @constructor
*/
function HorizontalScrollbarView(divId, innerDivId, callback) {
superClass.call(this, divId);
this.callback_ = callback;
this.innerDiv_ = $(innerDivId);
$(divId).onscroll = this.onScroll_.bind(this);
// The current range and position of the scrollbar. Because DOM updates
// are asynchronous, the current state cannot be read directly from the DOM
// after updating the range.
this.range_ = 0;
this.position_ = 0;
// The DOM updates asynchronously, so sometimes we need a timer to update
// the current scroll position after resizing the scrollbar.
this.updatePositionTimerId_ = null;
}
HorizontalScrollbarView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
setGeometry: function(left, top, width, height) {
superClass.prototype.setGeometry.call(this, left, top, width, height);
this.setRange(this.range_);
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
},
/**
* Sets the range of the scrollbar. The scrollbar can have a value
* anywhere from 0 to |range|, inclusive. The width of the drag area
* on the scrollbar will generally be based on the width of the scrollbar
* relative to the size of |range|, so if the scrollbar is about the size
* of the thing we're scrolling, we get fairly nice behavior.
*
* If |range| is less than the original position, |position_| is set to
* |range|. Otherwise, it is not modified.
*/
setRange: function(range) {
this.range_ = range;
setNodeWidth(this.innerDiv_, this.getWidth() + range);
if (range < this.position_)
this.position_ = range;
this.setPosition(this.position_);
},
/**
* Sets the position of the scrollbar. |position| must be between 0 and
* |range_|, inclusive.
*/
setPosition: function(position) {
this.position_ = position;
this.updatePosition_();
},
/**
* Updates the visible position of the scrollbar to be |position_|.
* On failure, calls itself again after a timeout. This is needed because
* setRange does not synchronously update the DOM.
*/
updatePosition_: function() {
// Clear the timer if we have one, so we don't have two timers running at
// once. This is safe even if we were just called from the timer, in
// which case clearTimeout will silently fail.
if (this.updatePositionTimerId_ !== null) {
window.clearTimeout(this.updatePositionTimerId_);
this.updatePositionTimerId_ = null;
}
this.getNode().scrollLeft = this.position_;
if (this.getNode().scrollLeft != this.position_) {
this.updatePositionTimerId_ =
window.setTimeout(this.updatePosition_.bind(this));
}
},
getRange: function() {
return this.range_;
},
getPosition: function() {
return this.position_;
},
onScroll_: function() {
// If we're waiting to update the range, ignore messages from the
// scrollbar.
if (this.updatePositionTimerId_ !== null)
return;
var newPosition = this.getNode().scrollLeft;
if (newPosition == this.position_)
return;
this.position_ = newPosition;
this.callback_();
}
};
return HorizontalScrollbarView;
})();
...@@ -8,6 +8,7 @@ found in the LICENSE file. ...@@ -8,6 +8,7 @@ found in the LICENSE file.
<head i18n-values="dir:textdirection;"> <head i18n-values="dir:textdirection;">
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="events_view.css"> <link rel="stylesheet" href="events_view.css">
<link rel="stylesheet" href="timeline_view.css">
<link rel="stylesheet" href="logs_view.css"> <link rel="stylesheet" href="logs_view.css">
<link rel="stylesheet" href="tab_switcher_view.css"> <link rel="stylesheet" href="tab_switcher_view.css">
<script src="chrome://resources/js/util.js"></script> <script src="chrome://resources/js/util.js"></script>
...@@ -43,6 +44,7 @@ found in the LICENSE file. ...@@ -43,6 +44,7 @@ found in the LICENSE file.
<include src="test_view.html"/> <include src="test_view.html"/>
<include src="hsts_view.html"/> <include src="hsts_view.html"/>
<include src="events_view.html"/> <include src="events_view.html"/>
<include src="timeline_view.html"/>
<include src="logs_view.html"/> <include src="logs_view.html"/>
<script src="chrome://resources/js/i18n_template.js"></script> <script src="chrome://resources/js/i18n_template.js"></script>
......
...@@ -23,8 +23,11 @@ ...@@ -23,8 +23,11 @@
<include src="events_view.js"/> <include src="events_view.js"/>
<include src="details_view.js"/> <include src="details_view.js"/>
<include src="source_entry.js"/> <include src="source_entry.js"/>
<include src="horizontal_scrollbar_view.js"/>
<include src="top_mid_bottom_view.js"/> <include src="top_mid_bottom_view.js"/>
<include src="timeline_view_painter.js"/> <include src="timeline_data_series.js"/>
<include src="timeline_graph_view.js"/>
<include src="timeline_view.js"/>
<include src="log_view_painter.js"/> <include src="log_view_painter.js"/>
<include src="log_grouper.js"/> <include src="log_grouper.js"/>
<include src="proxy_view.js"/> <include src="proxy_view.js"/>
......
...@@ -23,7 +23,7 @@ body { ...@@ -23,7 +23,7 @@ body {
background: #bfbfbf; background: #bfbfbf;
border-left: 1px inset black; border-left: 1px inset black;
border-right: 1px solid black; border-right: 1px solid black;
position:absolute; position: absolute;
width: 8px; width: 8px;
cursor: col-resize; cursor: col-resize;
user-select: none; user-select: none;
......
...@@ -44,14 +44,15 @@ var MainView = (function() { ...@@ -44,14 +44,15 @@ var MainView = (function() {
function MainView() { function MainView() {
assertFirstConstructorCall(MainView); assertFirstConstructorCall(MainView);
// Tracks if we're viewing a loaded log file, so views can behave
// appropriately.
this.isViewingLoadedLog_ = false;
// This must be initialized before the tabs, so they can register as // This must be initialized before the tabs, so they can register as
// observers. // observers.
g_browser = BrowserBridge.getInstance(); g_browser = BrowserBridge.getInstance();
// This must be the first constants observer, so other constants observers
// can safely use the globals, rather than depending on walking through
// the constants themselves.
g_browser.addConstantsObserver(new ConstantsObserver());
// This view is a left (resizable) navigation bar. // This view is a left (resizable) navigation bar.
this.categoryTabSwitcher_ = new TabSwitcherView(); this.categoryTabSwitcher_ = new TabSwitcherView();
var tabs = this.categoryTabSwitcher_; var tabs = this.categoryTabSwitcher_;
...@@ -82,6 +83,8 @@ var MainView = (function() { ...@@ -82,6 +83,8 @@ var MainView = (function() {
false, true); false, true);
tabs.addTab(EventsView.TAB_HANDLE_ID, EventsView.getInstance(), tabs.addTab(EventsView.TAB_HANDLE_ID, EventsView.getInstance(),
false, true); false, true);
tabs.addTab(TimelineView.TAB_HANDLE_ID, TimelineView.getInstance(),
false, true);
tabs.addTab(DnsView.TAB_HANDLE_ID, DnsView.getInstance(), tabs.addTab(DnsView.TAB_HANDLE_ID, DnsView.getInstance(),
false, true); false, true);
tabs.addTab(SocketsView.TAB_HANDLE_ID, SocketsView.getInstance(), tabs.addTab(SocketsView.TAB_HANDLE_ID, SocketsView.getInstance(),
...@@ -127,8 +130,6 @@ var MainView = (function() { ...@@ -127,8 +130,6 @@ var MainView = (function() {
// Select the initial view based on the current URL. // Select the initial view based on the current URL.
window.onhashchange(); window.onhashchange();
g_browser.addConstantsObserver(new ConstantsObserver());
// Tell the browser that we are ready to start receiving log events. // Tell the browser that we are ready to start receiving log events.
g_browser.sendReady(); g_browser.sendReady();
} }
...@@ -143,6 +144,14 @@ var MainView = (function() { ...@@ -143,6 +144,14 @@ var MainView = (function() {
cr.addSingletonGetter(MainView); cr.addSingletonGetter(MainView);
// Tracks if we're viewing a loaded log file, so views can behave
// appropriately. Global so safe to call during construction.
var isViewingLoadedLog = false;
MainView.isViewingLoadedLog = function() {
return isViewingLoadedLog;
};
MainView.prototype = { MainView.prototype = {
// Inherit the superclass's methods. // Inherit the superclass's methods.
__proto__: superClass.prototype, __proto__: superClass.prototype,
...@@ -163,9 +172,9 @@ var MainView = (function() { ...@@ -163,9 +172,9 @@ var MainView = (function() {
* @param {String} fileName The name of the log file that has been loaded. * @param {String} fileName The name of the log file that has been loaded.
*/ */
onLoadLogFile: function(fileName) { onLoadLogFile: function(fileName) {
this.isViewingLoadedLog_ = true; isViewingLoadedLog = true;
// Swap out the status bar to indicate we have loaded from a file. // Swap out the status bar to indicate we have loaded from a file.
setNodeDisplay($(MainView.STATUS_VIEW_FOR_CAPTURE_ID), false); setNodeDisplay($(MainView.STATUS_VIEW_FOR_CAPTURE_ID), false);
setNodeDisplay($(MainView.STATUS_VIEW_FOR_FILE_ID), true); setNodeDisplay($(MainView.STATUS_VIEW_FOR_FILE_ID), true);
...@@ -177,16 +186,8 @@ var MainView = (function() { ...@@ -177,16 +186,8 @@ var MainView = (function() {
g_browser.sourceTracker.setSecurityStripping(false); g_browser.sourceTracker.setSecurityStripping(false);
g_browser.disable(); g_browser.disable();
}, },
/**
* Returns true if we're viewing a loaded log file.
*/
isViewingLoadedLog: function() {
return this.isViewingLoadedLog_;
}
}; };
/* /*
* Takes the current hash in form of "#tab&param1=value1&param2=value2&...". * Takes the current hash in form of "#tab&param1=value1&param2=value2&...".
* Puts the parameters in an object, and passes the resulting object to * Puts the parameters in an object, and passes the resulting object to
......
...@@ -37,7 +37,7 @@ var ProxyView = (function() { ...@@ -37,7 +37,7 @@ var ProxyView = (function() {
// Register to receive proxy information as it changes. // Register to receive proxy information as it changes.
g_browser.addProxySettingsObserver(this, true); g_browser.addProxySettingsObserver(this, true);
g_browser.addBadProxiesObserver(this, true); g_browser.addBadProxiesObserver(this, true);
g_browser.sourceTracker.addObserver(this); g_browser.sourceTracker.addSourceEntryObserver(this);
} }
// ID for special HTML element in category_tabs.html // ID for special HTML element in category_tabs.html
......
...@@ -198,7 +198,7 @@ var SourceEntry = (function() { ...@@ -198,7 +198,7 @@ var SourceEntry = (function() {
*/ */
getEndTime: function() { getEndTime: function() {
if (!this.isInactive_) { if (!this.isInactive_) {
return (new Date()).getTime(); return timeutil.getCurrentTime();
} else { } else {
var endTicks = this.entries_[this.entries_.length - 1].time; var endTicks = this.entries_[this.entries_.length - 1].time;
return timeutil.convertTimeTicksToDate(endTicks).getTime(); return timeutil.convertTimeTicksToDate(endTicks).getTime();
......
...@@ -13,7 +13,12 @@ var SourceTracker = (function() { ...@@ -13,7 +13,12 @@ var SourceTracker = (function() {
* @constructor * @constructor
*/ */
function SourceTracker() { function SourceTracker() {
this.observers_ = []; // Observers that are sent all events as they happen. This allows for easy
// watching for particular events.
this.logEntryObservers_ = [];
// Observers that only want to receive lists of updated SourceEntries.
this.sourceEntryObservers_ = [];
// True when cookies and authentication information should be removed from // True when cookies and authentication information should be removed from
// displayed events. When true, such information should be hidden from // displayed events. When true, such information should be hidden from
...@@ -115,7 +120,7 @@ var SourceTracker = (function() { ...@@ -115,7 +120,7 @@ var SourceTracker = (function() {
}, },
/** /**
* Sends each entry to all log observers, and updates |capturedEvents_|. * Sends each entry to all observers and updates |capturedEvents_|.
* Also assigns unique ids to log entries without a source. * Also assigns unique ids to log entries without a source.
*/ */
onReceivedLogEntries: function(logEntries) { onReceivedLogEntries: function(logEntries) {
...@@ -153,8 +158,12 @@ var SourceTracker = (function() { ...@@ -153,8 +158,12 @@ var SourceTracker = (function() {
} }
this.capturedEvents_ = this.capturedEvents_.concat(logEntries); this.capturedEvents_ = this.capturedEvents_.concat(logEntries);
for (var i = 0; i < this.observers_.length; ++i) for (var i = 0; i < this.sourceEntryObservers_.length; ++i) {
this.observers_[i].onSourceEntriesUpdated(updatedSourceEntries); this.sourceEntryObservers_[i].onSourceEntriesUpdated(
updatedSourceEntries);
}
for (var i = 0; i < this.logEntryObservers_.length; ++i)
this.logEntryObservers_[i].onReceivedLogEntries(logEntries);
}, },
/** /**
...@@ -179,8 +188,8 @@ var SourceTracker = (function() { ...@@ -179,8 +188,8 @@ var SourceTracker = (function() {
} }
this.capturedEvents_ = newEventList; this.capturedEvents_ = newEventList;
for (var i = 0; i < this.observers_.length; ++i) for (var i = 0; i < this.sourceEntryObservers_.length; ++i)
this.observers_[i].onSourceEntriesDeleted(sourceEntryIds); this.sourceEntryObservers_[i].onSourceEntriesDeleted(sourceEntryIds);
}, },
/** /**
...@@ -188,8 +197,10 @@ var SourceTracker = (function() { ...@@ -188,8 +197,10 @@ var SourceTracker = (function() {
*/ */
deleteAllSourceEntries: function() { deleteAllSourceEntries: function() {
this.clearEntries_(); this.clearEntries_();
for (var i = 0; i < this.observers_.length; ++i) for (var i = 0; i < this.sourceEntryObservers_.length; ++i)
this.observers_[i].onAllSourceEntriesDeleted(); this.sourceEntryObservers_[i].onAllSourceEntriesDeleted();
for (var i = 0; i < this.logEntryObservers_.length; ++i)
this.logEntryObservers_[i].onAllLogEntriesDeleted();
}, },
/** /**
...@@ -198,9 +209,9 @@ var SourceTracker = (function() { ...@@ -198,9 +209,9 @@ var SourceTracker = (function() {
*/ */
setSecurityStripping: function(enableSecurityStripping) { setSecurityStripping: function(enableSecurityStripping) {
this.enableSecurityStripping_ = enableSecurityStripping; this.enableSecurityStripping_ = enableSecurityStripping;
for (var i = 0; i < this.observers_.length; ++i) { for (var i = 0; i < this.sourceEntryObservers_.length; ++i) {
if (this.observers_[i].onSecurityStrippingChanged) if (this.sourceEntryObservers_[i].onSecurityStrippingChanged)
this.observers_[i].onSecurityStrippingChanged(); this.sourceEntryObservers_[i].onSecurityStrippingChanged();
} }
}, },
...@@ -213,17 +224,28 @@ var SourceTracker = (function() { ...@@ -213,17 +224,28 @@ var SourceTracker = (function() {
}, },
/** /**
* Adds a listener of log entries. |observer| will be called back when new * Adds a listener of SourceEntries. |observer| will be called back when
* log data arrives, source entries are deleted, or security stripping * SourceEntries are added or modified, source entries are deleted, or
* changes through: * security stripping changes:
* *
* observer.onSourceEntriesUpdated(sourceEntries) * observer.onSourceEntriesUpdated(sourceEntries)
* observer.deleteSourceEntries(sourceEntryIds) * observer.onSourceEntriesDeleted(sourceEntryIds)
* ovserver.deleteAllSourceEntries() * ovserver.onAllSourceEntriesDeleted()
* observer.onSecurityStrippingChanged() * observer.onSecurityStrippingChanged()
*/ */
addObserver: function(observer) { addSourceEntryObserver: function(observer) {
this.observers_.push(observer); this.sourceEntryObservers_.push(observer);
},
/**
* Adds a listener of log entries. |observer| will be called back when new
* log data arrives or all entries are deleted:
*
* observer.onReceivedLogEntries(entries)
* ovserver.onAllLogEntriesDeleted()
*/
addLogEntryObserver: function(observer) {
this.logEntryObservers_.push(observer);
} }
}; };
......
...@@ -102,7 +102,7 @@ var TestView = (function() { ...@@ -102,7 +102,7 @@ var TestView = (function() {
dtCell: dtCell, dtCell: dtCell,
resultCell: resultCell, resultCell: resultCell,
passFailCell: passFailCell, passFailCell: passFailCell,
startTime: (new Date()).getTime() startTime: timeutil.getCurrentTime()
}; };
addTextNode(experimentCell, 'Fetch ' + experiment.url); addTextNode(experimentCell, 'Fetch ' + experiment.url);
...@@ -129,7 +129,7 @@ var TestView = (function() { ...@@ -129,7 +129,7 @@ var TestView = (function() {
onCompletedConnectionTestExperiment: function(experiment, result) { onCompletedConnectionTestExperiment: function(experiment, result) {
var r = this.currentExperimentRow_; var r = this.currentExperimentRow_;
var endTime = (new Date()).getTime(); var endTime = timeutil.getCurrentTime();
r.dtCell.innerHTML = ''; r.dtCell.innerHTML = '';
addTextNode(r.dtCell, (endTime - r.startTime)); addTextNode(r.dtCell, (endTime - r.startTime));
......
...@@ -32,8 +32,18 @@ var timeutil = (function() { ...@@ -32,8 +32,18 @@ var timeutil = (function() {
return new Date(timeStampMs); return new Date(timeStampMs);
} }
/**
* Returns the current time.
*
* @returns {number} Milliseconds since the Unix epoch.
*/
function getCurrentTime() {
return (new Date()).getTime();
}
return { return {
setTimeTickOffset: setTimeTickOffset, setTimeTickOffset: setTimeTickOffset,
convertTimeTicksToDate: convertTimeTicksToDate convertTimeTicksToDate: convertTimeTicksToDate,
getCurrentTime: getCurrentTime
}; };
})(); })();
// Copyright (c) 2011 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.
/**
* Different data types that each require their own labelled axis.
*/
var TimelineDataType = {
SOURCE_COUNT: 0,
BYTES_PER_SECOND: 1
};
/**
* A TimelineDataSeries collects an ordered series of (time, value) pairs,
* and converts them to graph points. It also keeps track of its color and
* current visibility state. DataSeries are solely responsible for tracking
* data, and do not send notifications on state changes.
*
* Abstract class, doesn't implement onReceivedLogEntry.
*/
var TimelineDataSeries = (function() {
'use strict';
/**
* @constructor
*/
function TimelineDataSeries(dataType) {
// List of DataPoints in chronological order.
this.dataPoints_ = [];
// Data type of the DataSeries. This is used to scale all values with
// the same units in the same way.
this.dataType_ = dataType;
// Default color. Should always be overridden prior to display.
this.color_ = 'red';
// Whether or not the data series should be drawn.
this.isVisible_ = false;
this.cacheStartTime_ = null;
this.cacheStepSize_ = 0;
this.cacheValues_ = [];
}
TimelineDataSeries.prototype = {
/**
* Adds a DataPoint to |this| with the specified time and value.
* DataPoints are assumed to be received in chronological order.
*/
addPoint: function(timeTicks, value) {
var time = timeutil.convertTimeTicksToDate(timeTicks).getTime();
this.dataPoints_.push(new DataPoint(time, value));
},
isVisible: function() {
return this.isVisible_;
},
show: function(isVisible) {
this.isVisible_ = isVisible;
},
getColor: function() {
return this.color_;
},
setColor: function(color) {
this.color_ = color;
},
getDataType: function() {
return this.dataType_;
},
/**
* Returns a list containing the values of the data series at |count|
* points, starting at |startTime|, and |stepSize| milliseconds apart.
* Caches values, so showing/hiding individual data series is fast, and
* derived data series can be efficiently computed, if we add any.
*/
getValues: function(startTime, stepSize, count) {
// Use cached values, if we can.
if (this.cacheStartTime_ == startTime &&
this.cacheStepSize_ == stepSize &&
this.cacheValues_.length == count) {
return this.cacheValues_;
}
// Do all the work.
this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count);
this.cacheStartTime_ = startTime;
this.cacheStepSize_ = stepSize;
return this.cacheValues_;
},
/**
* Does all the work of getValues when we can't use cached data.
*
* The default implementation just uses the |value| of the most recently
* seen DataPoint before each time, but other DataSeries may use some
* form of interpolation.
* TODO(mmenke): Consider returning the maximum value over each interval
* to create graphs more stable with respect to zooming.
*/
getValuesInternal_: function(startTime, stepSize, count) {
var values = [];
var nextPoint = 0;
var currentValue = 0;
var time = startTime;
for (var i = 0; i < count; ++i) {
while (nextPoint < this.dataPoints_.length &&
this.dataPoints_[nextPoint].time < time) {
currentValue = this.dataPoints_[nextPoint].value;
++nextPoint;
}
values[i] = currentValue;
time += stepSize;
}
return values;
}
};
/**
* A single point in a data series. Each point has a time, in the form of
* milliseconds since the Unix epoch, and a numeric value.
* @constructor
*/
function DataPoint(time, value) {
this.time = time;
this.value = value;
}
return TimelineDataSeries;
})();
/**
* Tracks how many sources of the given type have seen a begin
* event of type |eventType| more recently than an end event.
*/
var SourceCountDataSeries = (function() {
'use strict';
var superClass = TimelineDataSeries;
/**
* @constructor
*/
function SourceCountDataSeries(sourceType, eventType) {
superClass.call(this, TimelineDataType.SOURCE_COUNT);
this.sourceType_ = sourceType;
this.eventType_ = eventType;
// Map of sources for which we've seen a begin event more recently than an
// end event. Each such source has a value of "true". All others are
// undefined.
this.activeSources_ = {};
// Number of entries in |activeSources_|.
this.activeCount_ = 0;
}
SourceCountDataSeries.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
onReceivedLogEntry: function(entry) {
if (entry.source.type != this.sourceType_ ||
entry.type != this.eventType_) {
return;
}
if (entry.phase == LogEventPhase.PHASE_BEGIN) {
this.onBeginEvent(entry.source.id, entry.time);
return;
}
if (entry.phase == LogEventPhase.PHASE_END)
this.onEndEvent(entry.source.id, entry.time);
},
/**
* Called when the source with the specified id begins doing whatever we
* care about. If it's not already an active source, we add it to the map
* and add a data point.
*/
onBeginEvent: function(id, time) {
if (this.activeSources_[id])
return;
this.activeSources_[id] = true;
++this.activeCount_;
this.addPoint(time, this.activeCount_);
},
/**
* Called when the source with the specified id stops doing whatever we
* care about. If it's an active source, we remove it from the map and add
* a data point.
*/
onEndEvent: function(id, time) {
if (!this.activeSources_[id])
return;
delete this.activeSources_[id];
--this.activeCount_;
this.addPoint(time, this.activeCount_);
}
};
return SourceCountDataSeries;
})();
/**
* Tracks the number of sockets currently in use. Needs special handling of
* SSL sockets, so can't just use a normal SourceCountDataSeries.
*/
var SocketsInUseDataSeries = (function() {
'use strict';
var superClass = SourceCountDataSeries;
/**
* @constructor
*/
function SocketsInUseDataSeries() {
superClass.call(this, LogSourceType.SOCKET, LogEventType.SOCKET_IN_USE);
}
SocketsInUseDataSeries.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
onReceivedLogEntry: function(entry) {
// SSL sockets have two nested SOCKET_IN_USE events. This is needed to
// mark SSL sockets as unused after SSL negotiation.
if (entry.type == LogEventType.SSL_CONNECT &&
entry.phase == LogEventPhase.PHASE_END) {
this.onEndEvent(entry.source.id, entry.time);
return;
}
superClass.prototype.onReceivedLogEntry.call(this, entry);
}
};
return SocketsInUseDataSeries;
})();
/**
* Tracks approximate data rate using individual data transfer events.
* Abstract class, doesn't implement onReceivedLogEntry.
*/
var TransferRateDataSeries = (function() {
'use strict';
var superClass = TimelineDataSeries;
/**
* @constructor
*/
function TransferRateDataSeries() {
superClass.call(this, TimelineDataType.BYTES_PER_SECOND);
}
TransferRateDataSeries.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
/**
* Returns the average data rate over each interval, only taking into
* account transfers that occurred within each interval.
* TODO(mmenke): Do something better.
*/
getValuesInternal_: function(startTime, stepSize, count) {
// Find the first DataPoint after |startTime| - |stepSize|.
var nextPoint = 0;
while (nextPoint < this.dataPoints_.length &&
this.dataPoints_[nextPoint].time < startTime - stepSize) {
++nextPoint;
}
var values = [];
var time = startTime;
for (var i = 0; i < count; ++i) {
// Calculate total bytes transferred from |time| - |stepSize|
// to |time|. We look at the transfers before |time| to give
// us generally non-varying values for a given time.
var transferred = 0;
while (nextPoint < this.dataPoints_.length &&
this.dataPoints_[nextPoint].time < time) {
transferred += this.dataPoints_[nextPoint].value;
++nextPoint;
}
// Calculate bytes per second.
values[i] = 1000 * transferred / stepSize;
time += stepSize;
}
return values;
}
};
return TransferRateDataSeries;
})();
/**
* Tracks TCP and UDP transfer rate.
*/
var NetworkTransferRateDataSeries = (function() {
'use strict';
var superClass = TransferRateDataSeries;
/**
* |tcpEvent| and |udpEvent| are the event types for data transfers using
* TCP and UDP, respectively.
* @constructor
*/
function NetworkTransferRateDataSeries(tcpEvent, udpEvent) {
superClass.call(this);
this.tcpEvent_ = tcpEvent;
this.udpEvent_ = udpEvent;
}
NetworkTransferRateDataSeries.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
onReceivedLogEntry: function(entry) {
if (entry.type != this.tcpEvent_ && entry.type != this.udpEvent_)
return;
this.addPoint(entry.time, entry.params.byte_count);
},
};
return NetworkTransferRateDataSeries;
})();
/**
* Tracks disk cache read or write rate. Doesn't include clearing, opening,
* or dooming entries, as they don't have clear size values.
*/
var DiskCacheTransferRateDataSeries = (function() {
'use strict';
var superClass = TransferRateDataSeries;
/**
* @constructor
*/
function DiskCacheTransferRateDataSeries(eventType) {
superClass.call(this);
this.eventType_ = eventType;
}
DiskCacheTransferRateDataSeries.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
onReceivedLogEntry: function(entry) {
if (entry.source.type != LogSourceType.DISK_CACHE_ENTRY ||
entry.type != this.eventType_ ||
entry.phase != LogEventPhase.PHASE_END) {
return;
}
// The disk cache has a lot of 0-length writes, when truncating entries.
// Ignore those.
if (entry.params.bytes_copied != 0)
this.addPoint(entry.time, entry.params.bytes_copied);
}
};
return DiskCacheTransferRateDataSeries;
})();
// Copyright (c) 2011 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.
/**
* A TimelineGraphView displays a timeline graph on a canvas element.
*/
var TimelineGraphView = (function() {
'use strict';
// We inherit from TopMidBottomView.
var superClass = TopMidBottomView;
// Default starting scale factor, in terms of milliseconds per pixel.
var DEFAULT_SCALE = 1000;
// Maximum number of labels placed vertically along the sides of the graph.
var MAX_VERTICAL_LABELS = 6;
// Vertical spacing between labels and between the graph and labels.
var LABEL_VERTICAL_SPACING = 4;
// Horizontal spacing between vertically placed labels and the edges of the
// graph.
var LABEL_HORIZONTAL_SPACING = 3;
// Horizintal spacing between two horitonally placed labels along the bottom
// of the graph.
var LABEL_LABEL_HORIZONTAL_SPACING = 25;
// Length of ticks, in pixels, next to y-axis labels. The x-axis only has
// one set of labels, so it can use lines instead.
var Y_AXIS_TICK_LENGTH = 10;
// 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;
// Number of pixels to scroll per pixel the mouse is dragged.
var MOUSE_WHEEL_DRAG_RATE = 3;
var GRID_COLOR = '#CCC';
var TEXT_COLOR = '#000';
var BACKGROUND_COLOR = '#FFF';
// Which side of the canvas y-axis labels should go on, for a given Graph.
// TODO(mmenke): Figure out a reasonable way to handle more than 2 sets
// of labels.
var LabelAlign = {
LEFT: 0,
RIGHT: 1
};
/**
* @constructor
*/
function TimelineGraphView(divId, canvasId, scrollbarId, scrollbarInnerId) {
this.scrollbar_ = new HorizontalScrollbarView(scrollbarId,
scrollbarInnerId,
this.onScroll_.bind(this));
// Call superclass's constructor.
superClass.call(this, null, new DivView(divId), this.scrollbar_);
this.graphDiv_ = $(divId);
this.canvas_ = $(canvasId);
this.canvas_.onmousewheel = this.onMouseWheel_.bind(this);
this.canvas_.onmousedown = this.onMouseDown_.bind(this);
this.canvas_.onmousemove = this.onMouseMove_.bind(this);
this.canvas_.onmouseup = this.onMouseUp_.bind(this);
this.canvas_.onmouseout = this.onMouseUp_.bind(this);
// Used for click and drag scrolling of graph. Drag-zooming not supported,
// for a more stable scrolling experience.
this.isDragging_ = false;
this.dragX_ = 0;
// Set the range and scale of the graph. Times are in milliseconds since
// the Unix epoch.
// All measurements we have must be after this time.
this.startTime_ = 0;
// The current rightmost position of the graph is always at most this.
// We may have some later events. When actively capturing new events, it's
// updated on a timer.
this.endTime_ = 1;
// Current scale, in terms of milliseconds per pixel. Each column of
// pixels represents a point in time |scale_| milliseconds after the
// previous one. We only display times that are of the form
// |startTime_| + K * |scale_| to avoid jittering, and the rightmost
// pixel that we can display has a time <= |endTime_|. Non-integer values
// are allowed.
this.scale_ = DEFAULT_SCALE;
this.graphs_ = [];
// Initialize the scrollbar.
this.updateScrollbarRange_(true);
}
// Smallest allowed scaling factor.
TimelineGraphView.MIN_SCALE = 5;
TimelineGraphView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
setGeometry: function(left, top, width, height) {
superClass.prototype.setGeometry.call(this, left, top, width, height);
// The size of the canvas can only be set by using its |width| and
// |height| properties, which do not take padding into account, so we
// need to use them ourselves.
var style = getComputedStyle(this.canvas_);
var horizontalPadding = parseInt(style.paddingRight) +
parseInt(style.paddingLeft);
var verticalPadding = parseInt(style.paddingTop) +
parseInt(style.paddingBottom);
var width = parseInt(this.graphDiv_.style.width) - horizontalPadding;
// For unknown reasons, there's an extra 3 pixels border between the
// bottom of the canvas and the bottom margin of the enclosing div.
var height = parseInt(this.graphDiv_.style.height) - verticalPadding - 3;
// Protect against degenerates.
if (width < 10)
width = 10;
if (height < 10)
height = 10;
this.canvas_.width = width;
this.canvas_.height = height;
// Use the same font style for the canvas as we use elsewhere.
// Has to be updated every resize.
this.canvas_.getContext('2d').font = getComputedStyle(this.canvas_).font;
this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
this.repaint();
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
if (isVisible)
this.repaint();
},
// Returns the total length of the graph, in pixels.
getLength_ : function() {
var timeRange = this.endTime_ - this.startTime_;
// Math.floor is used to ignore the last partial area, of length less
// than |scale_|.
return Math.floor(timeRange / this.scale_);
},
/**
* Returns true if the graph is scrolled all the way to the right.
*/
graphScrolledToRightEdge_: function() {
return this.scrollbar_.getPosition() == this.scrollbar_.getRange();
},
/**
* Update the range of the scrollbar. If |resetPosition| is true, also
* sets the slider to point at the rightmost position and triggers a
* repaint.
*/
updateScrollbarRange_: function(resetPosition) {
var scrollbarRange = this.getLength_() - this.canvas_.width;
if (scrollbarRange < 0)
scrollbarRange = 0;
// If we've decreased the range to less than the current scroll position,
// we need to move the scroll position.
if (this.scrollbar_.getPosition() > scrollbarRange)
resetPosition = true;
this.scrollbar_.setRange(scrollbarRange);
if (resetPosition) {
this.scrollbar_.setPosition(scrollbarRange);
this.repaint();
}
},
/**
* Sets the date range displayed on the graph, switches to the default
* scale factor, and moves the scrollbar all the way to the right.
*/
setDateRange: function(startDate, endDate) {
this.startTime_ = startDate.getTime();
this.endTime_ = endDate.getTime();
// Safety check.
if (this.endTime_ <= this.startTime_)
this.startTime_ = this.endTime_ - 1;
this.scale_ = DEFAULT_SCALE;
this.updateScrollbarRange_(true);
},
/**
* Updates the end time at the right of the graph to be the current time.
* Specifically, updates the scrollbar's range, and if the scrollbar is
* all the way to the right, keeps it all the way to the right. Otherwise,
* leaves the view as-is and doesn't redraw anything.
*/
updateEndDate: function() {
this.endTime_ = timeutil.getCurrentTime();
this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
},
getStartDate: function() {
return new Date(this.startTime_);
},
/**
* Scrolls the graph horizontally by the specified amount.
*/
horizontalScroll_: function(delta) {
var newPosition = this.scrollbar_.getPosition() + Math.round(delta);
// Make sure the new position is in the right range.
if (newPosition < 0) {
newPosition = 0;
} else if (newPosition > this.scrollbar_.getRange()) {
newPosition = this.scrollbar_.getRange();
}
if (this.scrollbar_.getPosition() == newPosition)
return;
this.scrollbar_.setPosition(newPosition);
this.onScroll_();
},
/**
* Zooms the graph by the specified amount.
*/
zoom_: function(ratio) {
var oldScale = this.scale_;
this.scale_ *= ratio;
if (this.scale_ < TimelineGraphView.MIN_SCALE)
this.scale_ = TimelineGraphView.MIN_SCALE;
if (this.scale_ == oldScale)
return;
// If we were at the end of the range before, remain at the end of the
// range.
if (this.graphScrolledToRightEdge_()) {
this.updateScrollbarRange_(true);
return;
}
// Otherwise, do our best to maintain the old position. We use the
// position at the far right of the graph for consistency.
var oldMaxTime =
oldScale * (this.scrollbar_.getPosition() + this.canvas_.width);
var newMaxTime = Math.round(oldMaxTime / this.scale_);
var newPosition = newMaxTime - this.canvas_.width;
// Update range and scroll position.
this.updateScrollbarRange_(false);
this.horizontalScroll_(newPosition - this.scrollbar_.getPosition());
},
onMouseWheel_: function(event) {
event.preventDefault();
this.horizontalScroll_(
MOUSE_WHEEL_SCROLL_RATE *
-event.wheelDeltaX / MOUSE_WHEEL_UNITS_PER_CLICK);
this.zoom_(Math.pow(MOUSE_WHEEL_ZOOM_RATE,
-event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK));
},
onMouseDown_: function(event) {
event.preventDefault();
this.isDragging_ = true;
this.dragX_ = event.clientX;
},
onMouseMove_: function(event) {
if (!this.isDragging_)
return;
event.preventDefault();
this.horizontalScroll_(
MOUSE_WHEEL_DRAG_RATE * (event.clientX - this.dragX_));
this.dragX_ = event.clientX;
},
onMouseUp_: function(event) {
this.isDragging_ = false;
},
onScroll_: function() {
this.repaint();
},
/**
* Replaces the current TimelineDataSeries with |dataSeries|.
*/
setDataSeries: function(dataSeries) {
// Simplest just to recreate the Graphs.
this.graphs_ = [];
this.graphs_[TimelineDataType.BYTES_PER_SECOND] =
new Graph(TimelineDataType.BYTES_PER_SECOND, LabelAlign.RIGHT);
this.graphs_[TimelineDataType.SOURCE_COUNT] =
new Graph(TimelineDataType.SOURCE_COUNT, LabelAlign.LEFT);
for (var i = 0; i < dataSeries.length; ++i)
this.graphs_[dataSeries[i].getDataType()].addDataSeries(dataSeries[i]);
this.repaint();
},
/**
* Draws the graph on |canvas_|.
*/
repaint: function() {
this.repaintTimerRunning_ = false;
if (!this.isVisible())
return;
var width = this.canvas_.width;
var height = this.canvas_.height;
var context = this.canvas_.getContext('2d');
// Clear the canvas.
context.fillStyle = BACKGROUND_COLOR;
context.fillRect(0, 0, width, height);
// Try to get font height in pixels. Needed for layout.
var fontHeightString = context.font.match(/([0-9]+)px/)[1];
var fontHeight = parseInt(fontHeightString);
// Safety check, to avoid drawing anything too ugly.
if (fontHeightString.length == 0 || fontHeight <= 0 ||
fontHeight * 4 > height || width < 50) {
return;
}
// Save current transformation matrix so we can restore it later.
context.save();
// The center of an HTML canvas pixel is technically at (0.5, 0.5). This
// makes near straight lines look bad, due to anti-aliasing. This
// translation reduces the problem a little.
context.translate(0.5, 0.5);
// Figure out what time values to display.
var position = this.scrollbar_.getPosition();
// If the entire time range is being displayed, align the right edge of
// the graph to the end of the time range.
if (this.scrollbar_.getRange() == 0)
position = this.getLength_() - this.canvas_.width;
var visibleStartTime = this.startTime_ + position * this.scale_;
// Make space at the bottom of the graph for the time labels, and then
// draw the labels.
height -= fontHeight + LABEL_VERTICAL_SPACING;
this.drawTimeLabels(context, width, height, visibleStartTime);
// Draw outline of the main graph area.
context.strokeStyle = GRID_COLOR;
context.strokeRect(0, 0, width - 1, height - 1);
// Layout graphs and have them draw their tick marks.
for (var i = 0; i < this.graphs_.length; ++i) {
this.graphs_[i].layout(width, height, fontHeight, visibleStartTime,
this.scale_);
this.graphs_[i].drawTicks(context);
}
// Draw the lines of all graphs, and then draw their labels.
for (var i = 0; i < this.graphs_.length; ++i)
this.graphs_[i].drawLines(context);
for (var i = 0; i < this.graphs_.length; ++i)
this.graphs_[i].drawLabels(context);
// Restore original transformation matrix.
context.restore();
},
/**
* Draw time labels below the graph. Takes in start time as an argument
* since it may not be |startTime_|, when we're displaying the entire
* time range.
*/
drawTimeLabels: function(context, width, height, startTime) {
var textHeight = height + LABEL_VERTICAL_SPACING;
// Text for a time string to use in determining how far apart
// to place text labels.
var sampleText = (new Date(startTime)).toLocaleTimeString();
// The desired spacing for text labels.
var targetSpacing = context.measureText(sampleText).width +
LABEL_LABEL_HORIZONTAL_SPACING;
// The allowed time step values between adjacent labels. Anything much
// over a couple minutes isn't terribly realistic, given how much memory
// we use, and how slow a lot of the net-internals code is.
var timeStepValues = [
1000, // 1 second
1000 * 5,
1000 * 30,
1000 * 60, // 1 minute
1000 * 60 * 5,
1000 * 60 * 30,
1000 * 60 * 60, // 1 hour
1000 * 60 * 60 * 5
];
// Find smallest time step value that gives us at least |targetSpacing|,
// if any.
var timeStep = null;
for (var i = 0; i < timeStepValues.length; ++i) {
if (timeStepValues[i] / this.scale_ >= targetSpacing) {
timeStep = timeStepValues[i];
break;
}
}
// If no such value, give up.
if (!timeStep)
return;
// Find the time for the first label. This time is a perfect multiple of
// timeStep because of how UTC times work.
var time = Math.ceil(startTime / timeStep) * timeStep;
context.textBaseline = 'top';
context.textAlign = 'center';
context.fillStyle = TEXT_COLOR;
context.strokeStyle = GRID_COLOR;
// Draw labels and vertical grid lines.
while (true) {
var x = Math.round((time - startTime) / this.scale_);
if (x >= width)
break;
var text = (new Date(time)).toLocaleTimeString();
context.fillText(text, x, textHeight);
context.beginPath();
context.lineTo(x, 0);
context.lineTo(x, height);
context.stroke();
time += timeStep;
}
}
};
/**
* A Graph is responsible for drawing all the TimelineDataSeries that have
* the same data type. Graphs are responsible for scaling the values, laying
* out labels, and drawing both labels and lines for its data series.
*/
var Graph = (function() {
/**
* |dataType| is the DataType that will be shared by all its DataSeries.
* |labelAlign| is the LabelAlign value indicating whether the labels
* should be aligned to the right of left of the graph.
* @constructor
*/
function Graph(dataType, labelAlign) {
this.dataType_ = dataType;
this.dataSeries_ = [];
this.labelAlign_ = labelAlign;
// Cached properties of the graph, set in layout.
this.width_ = 0;
this.height_ = 0;
this.fontHeight_ = 0;
this.startTime_ = 0;
this.scale_ = 0;
// At least the highest value in the displayed range of the graph.
// Used for scaling and setting labels. Set in layoutLabels.
this.max_ = 0;
// Cached text of equally spaced labels. Set in layoutLabels.
this.labels_ = [];
}
/**
* A Label is the label at a particular position along the y-axis.
* @constructor
*/
function Label(height, text) {
this.height = height;
this.text = text;
}
Graph.prototype = {
addDataSeries: function(dataSeries) {
this.dataSeries_.push(dataSeries);
},
/**
* Returns a list of all the values that should be displayed for a given
* data series, using the current graph layout.
*/
getValues: function(dataSeries) {
if (!dataSeries.isVisible())
return null;
return dataSeries.getValues(this.startTime_, this.scale_, this.width_);
},
/**
* Updates the graph's layout. In particular, both the max value and
* label positions are updated. Must be called before calling any of the
* drawing functions.
*/
layout: function(width, height, fontHeight, startTime, scale) {
this.width_ = width;
this.height_ = height;
this.fontHeight_ = fontHeight;
this.startTime_ = startTime;
this.scale_ = scale;
// Find largest value.
var max = 0;
for (var i = 0; i < this.dataSeries_.length; ++i) {
var values = this.getValues(this.dataSeries_[i]);
if (!values)
continue;
for (var j = 0; j < values.length; ++j) {
if (values[j] > max)
max = values[j];
}
}
this.layoutLabels_(max);
},
/**
* Lays out labels and sets |max_|, taking the time units into
* consideration. |maxValue| is the actual maximum value, and
* |max_| will be set to the value of the largest label, which
* will be at least |maxValue|.
*/
layoutLabels_: function(maxValue) {
if (this.dataType_ != TimelineDataType.BYTES_PER_SECOND) {
this.layoutLabelsBasic_(maxValue, 0);
return;
}
// Special handling for data rates.
// Find appropriate units to use.
var units = ['B/s', 'kB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s'];
// Units to use for labels. 0 is bytes, 1 is kilobytes, etc.
// We start with kilobytes, and work our way up.
var unit = 1;
// Update |maxValue| to be in the right units.
var maxValue = maxValue / 1024;
while (units[unit + 1] && maxValue >= 999) {
maxValue /= 1024;
++unit;
}
// Calculate labels.
this.layoutLabelsBasic_(maxValue, 1);
// Append units to labels.
for (var i = 0; i < this.labels_.length; ++i)
this.labels_[i] += ' ' + units[unit];
// Convert |max_| back to bytes, so it can be used when scaling values
// for display.
this.max_ *= Math.pow(1024, unit);
},
/**
* Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the
* maximum number of decimal digits allowed. The minimum allowed
* difference between two adjacent labels is 10^-|maxDecimalDigits|.
*/
layoutLabelsBasic_: function(maxValue, maxDecimalDigits) {
this.labels_ = [];
// No labels if |maxValue| is 0.
if (maxValue == 0) {
this.max_ = maxValue;
return;
}
// The maximum number of equally spaced labels allowed. |fontHeight_|
// is doubled because the top two labels are both drawn in the same
// gap.
var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;
// The + 1 is for the top label.
var maxLabels = 1 + this.height_ / minLabelSpacing;
if (maxLabels < 2) {
maxLabels = 2;
} else if (maxLabels > MAX_VERTICAL_LABELS) {
maxLabels = MAX_VERTICAL_LABELS;
}
// Initial try for step size between conecutive labels.
var stepSize = Math.pow(10, -maxDecimalDigits);
// Number of digits to the right of the decimal of |stepSize|.
// Used for formating label strings.
var stepSizeDecimalDigits = maxDecimalDigits;
// Pick a reasonable step size.
while (true) {
// If we use a step size of |stepSize| between labels, we'll need:
//
// Math.ceil(maxValue / stepSize) + 1
//
// labels. The + 1 is because we need labels at both at 0 and at
// the top of the graph.
// Check if we can use steps of size |stepSize|.
if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels)
break;
// Check |stepSize| * 2.
if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) {
stepSize *= 2;
break;
}
// Check |stepSize| * 5.
if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) {
stepSize *= 5;
break;
}
stepSize *= 10;
if (stepSizeDecimalDigits > 0)
--stepSizeDecimalDigits;
}
// Set the max so it's an exact multiple of the chosen step size.
this.max_ = Math.ceil(maxValue / stepSize) * stepSize;
// Create labels.
for (var label = this.max_; label >= 0; label -= stepSize)
this.labels_.push(label.toFixed(stepSizeDecimalDigits));
},
/**
* Draws tick marks for each of the labels in |labels_|.
*/
drawTicks: function(context) {
var x1;
var x2;
if (this.labelAlign_ == LabelAlign.RIGHT) {
x1 = this.width_ - 1;
x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;
} else {
x1 = 0;
x2 = Y_AXIS_TICK_LENGTH;
}
context.fillStyle = GRID_COLOR;
context.beginPath();
for (var i = 1; i < this.labels_.length - 1; ++i) {
// The rounding is needed to avoid ugly 2-pixel wide anti-aliased
// lines.
var y = Math.round(this.height_ * i / (this.labels_.length - 1));
context.moveTo(x1, y);
context.lineTo(x2, y);
}
context.stroke();
},
/**
* Draws a graph line for each of the data series.
*/
drawLines: function(context) {
// Factor by which to scale all values to convert them to a number from
// 0 to height - 1.
var scale = 0;
var bottom = this.height_ - 1;
if (this.max_)
scale = bottom / this.max_;
// Draw in reverse order, so earlier data series are drawn on top of
// subsequent ones.
for (var i = this.dataSeries_.length - 1; i >= 0; --i) {
var values = this.getValues(this.dataSeries_[i]);
if (!values)
continue;
context.strokeStyle = this.dataSeries_[i].getColor();
context.beginPath();
for (var x = 0; x < values.length; ++x) {
// The rounding is needed to avoid ugly 2-pixel wide anti-aliased
// horizontal lines.
context.lineTo(x, bottom - Math.round(values[x] * scale));
}
context.stroke();
}
},
/**
* Draw labels in |labels_|.
*/
drawLabels: function(context) {
if (this.labels_.length == 0)
return;
var x;
if (this.labelAlign_ == LabelAlign.RIGHT) {
x = this.width_ - LABEL_HORIZONTAL_SPACING;
} else {
// Find the width of the widest label.
var maxTextWidth = 0;
for (var i = 0; i < this.labels_.length; ++i) {
var textWidth = context.measureText(this.labels_[i]).width;
if (maxTextWidth < textWidth)
maxTextWidth = textWidth;
}
x = maxTextWidth + LABEL_HORIZONTAL_SPACING;
}
// Set up the context.
context.fillStyle = TEXT_COLOR;
context.textAlign = 'right';
// Draw top label, which is the only one that appears below its tick
// mark.
context.textBaseline = 'top';
context.fillText(this.labels_[0], x, 0);
// Draw all the other labels.
context.textBaseline = 'bottom';
var step = (this.height_ - 1) / (this.labels_.length - 1);
for (var i = 1; i < this.labels_.length; ++i)
context.fillText(this.labels_[i], x, step * i);
}
};
return Graph;
})();
return TimelineGraphView;
})();
/*
Copyright (c) 2011 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.
*/
#timeline-view-selection-div {
overflow-x: hidden;
overflow-y: auto;
}
#timeline-view-selection-div li {
list-style: none;
white-space: nowrap;
}
.timeline-view-text {
color: black;
}
#timeline-view-open-sockets {
color: #A0A;
}
#timeline-view-in-use-sockets {
color: #F3C;
}
#timeline-view-url-requests {
color: black;
}
#timeline-view-dns-requests {
color: #6BB;
}
#timeline-view-bytes-received {
color: #0B0;
}
#timeline-view-bytes-sent {
color: red;
}
#timeline-view-disk-cache-bytes-read {
color: #00F;
}
#timeline-view-disk-cache-bytes-written {
color: #999;
}
/* Need the id in this rule to override the above color rules. */
#timeline-view-selection-div .timeline-view-hidden {
color: white;
}
#timeline-view-graph-div {
background-color: white;
}
#timeline-view-graph-canvas {
padding: 10px 10px 2px 10px;
cursor: pointer;
}
#timeline-view-scrollbar-div {
overflow-y: hidden;
overflow-x: scroll;
}
#timeline-view-scrollbar-inner-div {
height: 1px;
}
<div id=timeline-view-selection-div>
<ul>
<li id=timeline-view-open-sockets><input type=checkbox checked />
&#9608; <span class=timeline-view-text>Open sockets</span>
</input></li>
<li id=timeline-view-in-use-sockets><input type=checkbox checked />
&#9608; <span class=timeline-view-text>In use sockets</span>
</input></li>
<li id=timeline-view-url-requests><input type=checkbox checked />
&#9608; <span class=timeline-view-text>URL requests</span>
</input></li>
<li id=timeline-view-dns-requests><input type=checkbox checked />
&#9608; <span class=timeline-view-text>DNS requests</span>
</input></li>
<li id=timeline-view-bytes-received><input type=checkbox checked />
&#9608; <span class=timeline-view-text>Bytes received</span>
</input></li>
<li id=timeline-view-bytes-sent><input type=checkbox checked />
&#9608; <span class=timeline-view-text>Bytes sent</span>
</input></li>
<li id=timeline-view-disk-cache-bytes-read><input type=checkbox checked />
&#9608; <span class=timeline-view-text>Disk cache bytes read</span>
</input></li>
<li id=timeline-view-disk-cache-bytes-written><input type=checkbox />
&#9608; <span class=timeline-view-text>Disk cache bytes written</span>
</input></li>
</ul>
</div>
<div id=timeline-view-vertical-splitter class=vertical-splitter></div>
<div id=timeline-view-graph-div>
<canvas id=timeline-view-graph-canvas>
</canvas>
</div>
<div id=timeline-view-scrollbar-div>
<div id=timeline-view-scrollbar-inner-div>
</div>
</div>
// Copyright (c) 2011 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.
/**
* TimelineView displays a zoomable and scrollable graph of a number of values
* over time. The TimelineView class itself is responsible primarily for
* updating the TimelineDataSeries its GraphView displays.
*/
var TimelineView = (function() {
'use strict';
// We inherit from ResizableVerticalSplitView.
var superClass = ResizableVerticalSplitView;
/**
* @constructor
*/
function TimelineView() {
assertFirstConstructorCall(TimelineView);
this.graphView_ = new TimelineGraphView(
TimelineView.GRAPH_DIV_ID,
TimelineView.GRAPH_CANVAS_ID,
TimelineView.SCROLLBAR_DIV_ID,
TimelineView.SCROLLBAR_INNER_DIV_ID);
// Call superclass's constructor.
superClass.call(this,
new DivView(TimelineView.SELECTION_DIV_ID),
this.graphView_,
new DivView(TimelineView.VERTICAL_SPLITTER_ID));
this.setLeftSplit(250);
// Interval id returned by window.setInterval for update timer.
this.updateIntervalId_ = null;
// List of DataSeries. These are shared with the TimelineGraphView. The
// TimelineView updates their state, the TimelineGraphView reads their
// state and draws them.
this.dataSeries_ = [];
// DataSeries depend on some of the global constants, so they're only
// created once constants have been received. We also use this message to
// recreate DataSeries when log files are being loaded.
g_browser.addConstantsObserver(this);
// We observe new log entries to determine the range of the graph, and pass
// them on to each DataSource. We initialize the graph range to initially
// include all events, but after that, we only update it to be the current
// time on a timer.
g_browser.sourceTracker.addLogEntryObserver(this);
this.graphRangeInitialized_ = false;
}
// ID for special HTML element in category_tabs.html
TimelineView.TAB_HANDLE_ID = 'tab-handle-timeline';
// IDs for special HTML elements in timeline_view.html
TimelineView.GRAPH_DIV_ID = 'timeline-view-graph-div';
TimelineView.GRAPH_CANVAS_ID = 'timeline-view-graph-canvas';
TimelineView.VERTICAL_SPLITTER_ID = 'timeline-view-vertical-splitter';
TimelineView.SELECTION_DIV_ID = 'timeline-view-selection-div';
TimelineView.SCROLLBAR_DIV_ID = 'timeline-view-scrollbar-div';
TimelineView.SCROLLBAR_INNER_DIV_ID = 'timeline-view-scrollbar-inner-div';
TimelineView.OPEN_SOCKETS_ID = 'timeline-view-open-sockets';
TimelineView.IN_USE_SOCKETS_ID = 'timeline-view-in-use-sockets';
TimelineView.URL_REQUESTS_ID = 'timeline-view-url-requests';
TimelineView.DNS_REQUESTS_ID = 'timeline-view-dns-requests';
TimelineView.BYTES_RECEIVED_ID = 'timeline-view-bytes-received';
TimelineView.BYTES_SENT_ID = 'timeline-view-bytes-sent';
TimelineView.DISK_CACHE_BYTES_READ_ID =
'timeline-view-disk-cache-bytes-read';
TimelineView.DISK_CACHE_BYTES_WRITTEN_ID =
'timeline-view-disk-cache-bytes-written';
// Class used for hiding the colored squares next to the labels for the
// lines.
TimelineView.HIDDEN_CLASS = 'timeline-view-hidden';
cr.addSingletonGetter(TimelineView);
// Frequency with which we increase update the end date to be the current
// time, when actively capturing events.
var UPDATE_INTERVAL_MS = 2000;
TimelineView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
setGeometry: function(left, top, width, height) {
superClass.prototype.setGeometry.call(this, left, top, width, height);
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
// If we're hidden or viewing a log file, make sure no interval is
// running.
if (!isVisible || MainView.isViewingLoadedLog()) {
this.setUpdateEndDateInterval_(0);
return;
}
// Otherwise, update the visible range on a timer.
this.setUpdateEndDateInterval_(UPDATE_INTERVAL_MS);
this.updateEndDate_();
},
/**
* Starts calling the GraphView's updateEndDate function every |intervalMs|
* milliseconds. If |intervalMs| is 0, stops calling the function.
*/
setUpdateEndDateInterval_: function(intervalMs) {
if (this.updateIntervalId_ !== null) {
window.clearInterval(this.updateIntervalId_);
this.updateIntervalId_ = null;
}
if (intervalMs > 0) {
this.updateIntervalId_ =
window.setInterval(this.updateEndDate_.bind(this), intervalMs);
}
},
updateEndDate_: function() {
this.graphView_.updateEndDate();
},
onLoadLogFinish: function(data) {
this.setUpdateEndDateInterval_(0);
return true;
},
/**
* Updates the visibility state of |dataSeries| to correspond to the
* current checked state of |checkBox|. Also updates the class of
* |listItem| based on the new visibility state.
*/
updateDataSeriesVisibility_: function(dataSeries, listItem, checkBox) {
dataSeries.show(checkBox.checked);
changeClassName(listItem, TimelineView.HIDDEN_CLASS, !checkBox.checked);
},
dataSeriesClicked_: function(dataSeries, listItem, checkBox) {
this.updateDataSeriesVisibility_(dataSeries, listItem, checkBox);
this.graphView_.repaint();
},
/**
* Adds the specified DataSeries to |dataSeries_|, and hooks up
* |listItemId|'s checkbox and color to correspond to the current state
* of the given DataSeries.
*/
addDataSeries_: function(dataSeries, listItemId) {
this.dataSeries_.push(dataSeries);
var listItem = $(listItemId);
var checkBox = $(listItemId).querySelector('input');
// Make sure |listItem| is visible, and then use its color for the
// DataSource.
changeClassName(listItem, TimelineView.HIDDEN_CLASS, false);
dataSeries.setColor(getComputedStyle(listItem).color);
this.updateDataSeriesVisibility_(dataSeries, listItem, checkBox);
checkBox.onclick = this.dataSeriesClicked_.bind(this, dataSeries,
listItem, checkBox);
},
/**
* Recreate all DataSeries. Global constants must have been set before
* this is called.
*/
createDataSeries_: function() {
this.graphRangeInitialized_ = false;
this.dataSeries_ = [];
this.addDataSeries_(new SourceCountDataSeries(
LogSourceType.SOCKET,
LogEventType.SOCKET_ALIVE),
TimelineView.OPEN_SOCKETS_ID);
this.addDataSeries_(new SocketsInUseDataSeries(),
TimelineView.IN_USE_SOCKETS_ID);
this.addDataSeries_(new SourceCountDataSeries(
LogSourceType.URL_REQUEST,
LogEventType.REQUEST_ALIVE),
TimelineView.URL_REQUESTS_ID);
this.addDataSeries_(new SourceCountDataSeries(
LogSourceType.HOST_RESOLVER_IMPL_REQUEST,
LogEventType.HOST_RESOLVER_IMPL_REQUEST),
TimelineView.DNS_REQUESTS_ID);
this.addDataSeries_(new NetworkTransferRateDataSeries(
LogEventType.SOCKET_BYTES_RECEIVED,
LogEventType.UDP_BYTES_RECEIVED),
TimelineView.BYTES_RECEIVED_ID);
this.addDataSeries_(new NetworkTransferRateDataSeries(
LogEventType.SOCKET_BYTES_SENT,
LogEventType.UDP_BYTES_SENT),
TimelineView.BYTES_SENT_ID);
this.addDataSeries_(new DiskCacheTransferRateDataSeries(),
TimelineView.DISK_CACHE_BYTES_READ_ID);
this.addDataSeries_(new DiskCacheTransferRateDataSeries(),
TimelineView.DISK_CACHE_BYTES_WRITTEN_ID);
this.graphView_.setDataSeries(this.dataSeries_);
},
/**
* When we receive the constants, create or recreate the DataSeries.
*/
onReceivedConstants: function(constants) {
this.createDataSeries_();
},
/**
* When log entries are deleted, simpler to recreate the DataSeries, rather
* than clearing them.
*/
onAllLogEntriesDeleted: function() {
this.graphRangeInitialized_ = false;
this.createDataSeries_();
},
onReceivedLogEntries: function(entries) {
// Pass each entry to every DataSeries, one at a time. Not having each
// data series get data directly from the SourceTracker saves us from
// having very un-Javascript-like destructors for when we load new,
// constants and slightly simplifies DataSeries objects.
for (var entry = 0; entry < entries.length; ++entry) {
for (var i = 0; i < this.dataSeries_.length; ++i)
this.dataSeries_[i].onReceivedLogEntry(entries[entry]);
}
// If this is the first non-empty set of entries we've received, or we're
// viewing a loaded log file, we will need to update the date range.
if (this.graphRangeInitialized_ && !MainView.isViewingLoadedLog())
return;
if (entries.length == 0)
return;
// Update the date range.
var startDate;
if (!this.graphRangeInitialized_) {
startDate = timeutil.convertTimeTicksToDate(entries[0].time);
} else {
startDate = this.graphView_.getStartDate();
}
var endDate =
timeutil.convertTimeTicksToDate(entries[entries.length - 1].time);
this.graphView_.setDateRange(startDate, endDate);
this.graphRangeInitialized_ = true;
}
};
return TimelineView;
})();
// Copyright (c) 2011 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.
var PaintTimelineView;
(function() {
'use strict';
PaintTimelineView = function(sourceEntries, node) {
addTextNode(node, 'TODO(eroman): Draw some sort of waterfall.');
addNode(node, 'br');
addNode(node, 'br');
addTextNode(node, 'Selected nodes (' + sourceEntries.length + '):');
addNode(node, 'br');
}
})();
...@@ -10,7 +10,8 @@ var TopMidBottomView = (function() { ...@@ -10,7 +10,8 @@ var TopMidBottomView = (function() {
/** /**
* This view stacks three boxes -- one at the top, one at the bottom, and * This view stacks three boxes -- one at the top, one at the bottom, and
* one that fills the remaining space between those two. * one that fills the remaining space between those two. Either the top
* or the bottom bar may be null.
* *
* +----------------------+ * +----------------------+
* | topbar | * | topbar |
...@@ -47,23 +48,34 @@ var TopMidBottomView = (function() { ...@@ -47,23 +48,34 @@ var TopMidBottomView = (function() {
superClass.prototype.setGeometry.call(this, left, top, width, height); superClass.prototype.setGeometry.call(this, left, top, width, height);
// Calculate the vertical split points. // Calculate the vertical split points.
var topbarHeight = this.topView_.getHeight(); var topbarHeight = 0;
var bottombarHeight = this.bottomView_.getHeight(); if (this.topView_)
topbarHeight = this.topView_.getHeight();
var bottombarHeight = 0;
if (this.bottomView_)
bottombarHeight = this.bottomView_.getHeight();
var middleboxHeight = height - (topbarHeight + bottombarHeight); var middleboxHeight = height - (topbarHeight + bottombarHeight);
if (middleboxHeight < 0)
middleboxHeight = 0;
// Position the boxes using calculated split points. // Position the boxes using calculated split points.
this.topView_.setGeometry(left, top, width, topbarHeight); if (this.topView_)
this.midView_.setGeometry(left, this.topView_.getBottom(), this.topView_.setGeometry(left, top, width, topbarHeight);
width, middleboxHeight); this.midView_.setGeometry(left, top + topbarHeight, width,
this.bottomView_.setGeometry(left, this.midView_.getBottom(), middleboxHeight);
width, bottombarHeight); if (this.bottomView_) {
this.bottomView_.setGeometry(left, top + topbarHeight + middleboxHeight,
width, bottombarHeight);
}
}, },
show: function(isVisible) { show: function(isVisible) {
superClass.prototype.show.call(this, isVisible); superClass.prototype.show.call(this, isVisible);
this.topView_.show(isVisible); if (this.topView_)
this.topView_.show(isVisible);
this.midView_.show(isVisible); this.midView_.show(isVisible);
this.bottomView_.show(isVisible); if (this.bottomView_)
this.bottomView_.show(isVisible);
} }
}; };
......
...@@ -228,6 +228,7 @@ void NetInternalsTest::SetUpInProcessBrowserTestFixture() { ...@@ -228,6 +228,7 @@ void NetInternalsTest::SetUpInProcessBrowserTestFixture() {
AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/main.js"))); AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/main.js")));
AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/prerender_view.js"))); AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/prerender_view.js")));
AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/test_view.js"))); AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/test_view.js")));
AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/timeline_view.js")));
} }
void NetInternalsTest::SetUpOnMainThread() { void NetInternalsTest::SetUpOnMainThread() {
...@@ -301,6 +302,48 @@ IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsExportImportDump) { ...@@ -301,6 +302,48 @@ IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsExportImportDump) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsExportImportDump")); EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsExportImportDump"));
} }
////////////////////////////////////////////////////////////////////////////////
// timeline_view.js
////////////////////////////////////////////////////////////////////////////////
// TODO(mmenke): Add tests for labels and DataSeries.
// Tests setting and updating range.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewRange) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewRange"));
}
// Tests using the scroll bar.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewScrollbar) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewScrollbar"));
}
// Tests case of having no events.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewNoEvents) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewNoEvents"));
}
// Dumps a log file to memory, modifies its events, loads it again, and
// makes sure the range is correctly set and not automatically updated.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewLoadLog) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewLoadLog"));
}
// Zooms out twice, and then zooms in once.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewZoomOut) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewZoomOut"));
}
// Zooms in as much as allowed, and zooms out once.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewZoomIn) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewZoomIn"));
}
// Tests case of all events having the same time.
IN_PROC_BROWSER_TEST_F(NetInternalsTest, NetInternalsTimelineViewDegenerate) {
EXPECT_TRUE(RunJavascriptAsyncTest("netInternalsTimelineViewDegenerate"));
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// dns_view.js // dns_view.js
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
......
...@@ -19,6 +19,7 @@ netInternalsTest.test('netInternalsExportImportDump', function() { ...@@ -19,6 +19,7 @@ netInternalsTest.test('netInternalsExportImportDump', function() {
import: true, import: true,
proxy: true, proxy: true,
events: true, events: true,
timeline: true,
dns: true, dns: true,
sockets: true, sockets: true,
spdy: true, spdy: true,
......
...@@ -20,6 +20,7 @@ netInternalsTest.test('netInternalsTourTabs', function() { ...@@ -20,6 +20,7 @@ netInternalsTest.test('netInternalsTourTabs', function() {
import: true, import: true,
proxy: true, proxy: true,
events: true, events: true,
timeline: true,
dns: true, dns: true,
sockets: true, sockets: true,
spdy: true, spdy: true,
......
...@@ -35,6 +35,7 @@ var netInternalsTest = (function() { ...@@ -35,6 +35,7 @@ var netInternalsTest = (function() {
import: ImportView.TAB_HANDLE_ID, import: ImportView.TAB_HANDLE_ID,
proxy: ProxyView.TAB_HANDLE_ID, proxy: ProxyView.TAB_HANDLE_ID,
events: EventsView.TAB_HANDLE_ID, events: EventsView.TAB_HANDLE_ID,
timeline: TimelineView.TAB_HANDLE_ID,
dns: DnsView.TAB_HANDLE_ID, dns: DnsView.TAB_HANDLE_ID,
sockets: SocketsView.TAB_HANDLE_ID, sockets: SocketsView.TAB_HANDLE_ID,
spdy: SpdyView.TAB_HANDLE_ID, spdy: SpdyView.TAB_HANDLE_ID,
...@@ -317,6 +318,15 @@ var netInternalsTest = (function() { ...@@ -317,6 +318,15 @@ var netInternalsTest = (function() {
task.setTaskQueue_(this); task.setTaskQueue_(this);
}, },
/**
* Adds a Task to the end of the queue. The task will call the provided
* function, and then complete.
* @param {function}: taskFunction The function the task will call.
*/
addFunctionTask: function(taskFunction) {
this.addTask(new CallFunctionTask(taskFunction));
},
/** /**
* Starts running the Tasks in the queue. Once called, may not be called * Starts running the Tasks in the queue. Once called, may not be called
* again. * again.
...@@ -390,6 +400,29 @@ var netInternalsTest = (function() { ...@@ -390,6 +400,29 @@ var netInternalsTest = (function() {
} }
}; };
/**
* A Task that can be added to a TaskQueue. A Task is started with a call to
* the start function, and must call its own onTaskDone when complete.
* @constructor
*/
function CallFunctionTask(taskFunction) {
Task.call(this);
assertEquals('function', typeof taskFunction);
this.taskFunction_ = taskFunction;
}
CallFunctionTask.prototype = {
__proto__: Task.prototype,
/**
* Runs the function and then completes.
*/
start: function() {
this.taskFunction_();
this.onTaskDone();
}
};
/** /**
* Returns true if a node does not have a 'display' property of 'none'. * Returns true if a node does not have a 'display' property of 'none'.
* @param {node}: node The node to check. * @param {node}: node The node to check.
...@@ -399,6 +432,72 @@ var netInternalsTest = (function() { ...@@ -399,6 +432,72 @@ var netInternalsTest = (function() {
return style.getPropertyValue('display') != 'none'; return style.getPropertyValue('display') != 'none';
} }
/**
* Creates a new NetLog source. Note that the id may conflict with events
* received from the browser.
* @param {int}: type The source type.
* @param {int}: id The source id.
* @constructor
*/
function Source(type, id) {
assertNotEquals(getKeyWithValue(LogSourceType, type), '?');
assertGE(id, 0);
this.type = type;
this.id = id;
}
/**
* Creates a new NetLog event.
* @param {Source}: source The source associated with the event.
* @param {int}: type The event id.
* @param {int}: time When the event occurred.
* @param {int}: phase The event phase.
* @param {object}: params The event parameters. May be null.
* @constructor
*/
function Event(source, type, time, phase, params) {
assertNotEquals(getKeyWithValue(LogEventType, type), '?');
assertNotEquals(getKeyWithValue(LogEventPhase, phase), '?');
this.source = source;
this.phase = phase;
this.type = type;
this.time = "" + time;
this.phase = phase;
if (params)
this.params = params;
}
/**
* Creates a new NetLog begin event. Parameters are the same as Event,
* except there's no |phase| argument.
* @see Event
*/
function CreateBeginEvent(source, type, time, params) {
return new Event(source, type, time, LogEventPhase.PHASE_BEGIN, params);
}
/**
* Creates a new NetLog end event. Parameters are the same as Event,
* except there's no |phase| argument.
* @see Event
*/
function CreateEndEvent(source, type, time, params) {
return new Event(source, type, time, LogEventPhase.PHASE_END, params);
}
/**
* Creates a new NetLog end event matching the given begin event.
* @param {Event}: beginEvent The begin event. Returned event will have the
* same source and type.
* @param {int}: time When the event occurred.
* @param {object}: params The event parameters. May be null.
* @see Event
*/
function CreateMatchingEndEvent(beginEvent, time, params) {
return CreateEndEvent(beginEvent.source, beginEvent.type, time, params);
}
// Exported functions. // Exported functions.
return { return {
test: test, test: test,
...@@ -408,8 +507,13 @@ var netInternalsTest = (function() { ...@@ -408,8 +507,13 @@ var netInternalsTest = (function() {
isDisplayed: isDisplayed, isDisplayed: isDisplayed,
runTest: runTest, runTest: runTest,
switchToView: switchToView, switchToView: switchToView,
TaskQueue: TaskQueue,
Task: Task, Task: Task,
TaskQueue: TaskQueue Source: Source,
Event: Event,
CreateBeginEvent: CreateBeginEvent,
CreateEndEvent: CreateEndEvent,
CreateMatchingEndEvent: CreateMatchingEndEvent
}; };
})(); })();
......
// Copyright (c) 2011 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.
// Anonymous namespace
(function() {
// Range of time set on a log once loaded used by sanity checks.
// Used by sanityCheckWithTimeRange.
var startTime = null;
var endTime = null;
var timelineView = TimelineView.getInstance();
var graphView = timelineView.graphView_;
var scrollbar = graphView.scrollbar_;
var canvas = graphView.canvas_;
/**
* A Task that creates a log dump, modifies it so |timeTicks| are all in UTC,
* clears all events from the log, and then adds two new SOCKET events, which
* have the specified start and end times.
*
* Most of these tests start with this task first. This gives us a known
* starting state, and prevents the data from automatically updating.
*
* @param {int} startTime Time of the begin event.
* @param {int} endTime Time of the end event.
* @extends {netInternalsTest.Task}
*/
function LoadLogWithNewEventsTask(startTime, endTime) {
netInternalsTest.Task.call(this);
this.startTime_ = startTime;
this.endTime_ = endTime;
}
LoadLogWithNewEventsTask.prototype = {
__proto__: netInternalsTest.Task.prototype,
/**
* Starts creating a log dump.
*/
start: function() {
logutil.createLogDumpAsync('test', this.onLogDumpCreated.bind(this));
},
/**
* Modifies the log dump and loads it.
*/
onLogDumpCreated: function(logDumpText) {
var logDump = JSON.parse(logDumpText);
logDump.constants.timeTickOffset = '0';
logDump.events = [];
var source = new netInternalsTest.Source(1, LogSourceType.SOCKET);
logDump.events.push(
netInternalsTest.CreateBeginEvent(source, LogEventType.SOCKET_ALIVE,
this.startTime_, null));
logDump.events.push(
netInternalsTest.CreateMatchingEndEvent(logDump.events[0],
this.endTime_, null));
logDumpText = JSON.stringify(logDump);
assertEquals('Log loaded.', logutil.loadLogFile(logDumpText));
endTime = this.endTime_;
startTime = this.startTime_;
if (startTime >= endTime)
--startTime;
sanityCheckWithTimeRange(false);
this.onTaskDone();
}
};
/**
* Checks certain invariant properties of the TimelineGraphView and the
* scroll bar.
*/
function sanityCheck() {
expectLT(graphView.startTime_, graphView.endTime_);
expectLE(0, scrollbar.getPosition());
expectLE(scrollbar.getPosition(), scrollbar.getRange());
}
/**
* Checks what sanityCheck does, but also checks that |startTime| and |endTime|
* are the same as those used by the graph, as well as whether we have a timer
* running to update the graph's end time. To avoid flake, this should only
* be used synchronously relative to when |startTime| and |endTime| were set,
* unless |expectUpdateTimer| is false.
* @param {bool} expectUpdateTimer true if the TimelineView should currently
* have an update end time timer running.
*/
function sanityCheckWithTimeRange(expectUpdateTimer) {
if (!expectUpdateTimer) {
expectEquals(null, timelineView.updateIntervalId_);
} else {
expectNotEquals(null, timelineView.updateIntervalId_);
}
assertNotEquals(startTime, null);
assertNotEquals(endTime, null);
expectEquals(startTime, graphView.startTime_);
expectEquals(endTime, graphView.endTime_);
sanityCheck(false);
}
/**
* Checks what sanityCheck does, but also checks that |startTime| and |endTime|
* are the same as those used by the graph.
*/
function sanityCheckNotUpdating() {
expectEquals(null, timelineView.updateIntervalId_);
sanityCheckWithTimeRange();
}
/**
* Simulates mouse wheel movement over the canvas element.
* @param {number} mouseWheelMovement Amount of movement to simulate.
*/
function mouseZoom(mouseWheelMovement) {
var scrollbarStartedAtEnd =
(scrollbar.getRange() == scrollbar.getPosition());
var event = document.createEvent('WheelEvent');
event.initWebKitWheelEvent(0, mouseWheelMovement, window, 0, 0, 0, 0,
false, false, false, false);
canvas.dispatchEvent(event);
// If the scrollbar started at the end of the range, make sure it ends there
// as well.
if (scrollbarStartedAtEnd)
expectEquals(scrollbar.getRange(), scrollbar.getPosition());
sanityCheck();
}
/**
* Simulates moving the mouse wheel up.
*/
function mouseZoomIn() {
var oldScale = graphView.scale_;
var oldRange = scrollbar.getRange();
mouseZoom(1);
if (oldScale == graphView.scale_) {
expectEquals(oldScale, TimelineGraphView.MIN_SCALE);
} else {
expectLT(graphView.scale_, oldScale);
}
expectGE(scrollbar.getRange(), oldRange);
}
/**
* Simulates moving the mouse wheel down.
*/
function mouseZoomOut() {
var oldScale = graphView.scale_;
var oldRange = scrollbar.getRange();
mouseZoom(-1);
expectGT(graphView.scale_, oldScale);
expectLE(scrollbar.getRange(), oldRange);
}
/**
* Simulates zooming all the way by multiple mouse wheel events.
*/
function mouseZoomAllTheWayIn() {
expectLT(TimelineGraphView.MIN_SCALE, graphView.scale_);
while (graphView.scale_ != TimelineGraphView.MIN_SCALE)
mouseZoomIn();
// Verify that zooming in when already at max zoom works.
mouseZoomIn();
}
/**
* A Task that scrolls the scrollbar by manipulating the DOM, and then waits
* for the scroll to complete. Has to be a task because onscroll and DOM
* manipulations both occur asynchronously.
*
* Not safe to use when other asynchronously running code may try to
* manipulate the scrollbar itself, or adjust the length of the scrollbar.
*
* @param {int} position Position to scroll to.
* @extends {netInternalsTest.Task}
*/
function MouseScrollTask(position) {
netInternalsTest.Task.call(this);
this.position_ = position;
// If the scrollbar's |position| and its node's |scrollLeft| values don't
// currently match, we set this to true and wait for |scrollLeft| to be
// updated, which will trigger an onscroll event.
this.waitingToStart_ = false;
}
MouseScrollTask.prototype = {
__proto__: netInternalsTest.Task.prototype,
start: function() {
this.waitingToStart_ = false;
// If the scrollbar is already in the correct position, do nothing.
if (scrollbar.getNode().scrollLeft == this.position_) {
// We may still have a timer going to adjust the position of the
// scrollbar to some other value. If so, this will clear it.
scrollbar.setPosition(this.position_);
this.onTaskDone();
return;
}
// Replace the onscroll event handler with our own.
this.oldOnScroll_ = scrollbar.getNode().onscroll;
scrollbar.getNode().onscroll = this.onScroll_.bind(this);
if (scrollbar.getNode().scrollLeft != scrollbar.getPosition()) {
this.waitingToStart_ = true;
return;
}
window.setTimeout(this.startScrolling_.bind(this), 0);
},
onScroll_: function(event) {
// Restore the original onscroll function.
scrollbar.getNode().onscroll = this.oldOnScroll_;
// Call the original onscroll function.
this.oldOnScroll_(event);
if (this.waitingToStart_) {
this.start();
return;
}
assertEquals(this.position_, scrollbar.getNode().scrollLeft);
assertEquals(this.position_, scrollbar.getPosition());
sanityCheck();
this.onTaskDone();
},
startScrolling_: function() {
scrollbar.getNode().scrollLeft = this.position_;
}
};
netInternalsTest.test('netInternalsTimelineViewRange', function() {
netInternalsTest.switchToView('timeline');
// Set startTime/endTime for sanity checks.
startTime = graphView.startTime_;
endTime = graphView.endTime_;
sanityCheckWithTimeRange(true);
startTime = 0;
endTime = 10;
graphView.setDateRange(new Date(startTime), new Date(endTime));
sanityCheckWithTimeRange(true);
endTime = (new Date()).getTime();
graphView.updateEndDate();
expectGE(graphView.endTime_, endTime);
sanityCheck();
testDone();
});
netInternalsTest.test('netInternalsTimelineViewScrollbar', function() {
// The range we want the graph to have.
var expectedGraphRange = canvas.width;
function checkGraphRange() {
expectEquals(expectedGraphRange, scrollbar.getRange());
}
var taskQueue = new netInternalsTest.TaskQueue(true);
// Load a log and then switch to the timeline view. The end time is
// calculated so that the range is exactly |expectedGraphRange|.
taskQueue.addTask(
new LoadLogWithNewEventsTask(
55, 55 + graphView.scale_ * (canvas.width + expectedGraphRange)));
taskQueue.addFunctionTask(
netInternalsTest.switchToView.bind(null, 'timeline'));
taskQueue.addFunctionTask(checkGraphRange);
taskQueue.addTask(new MouseScrollTask(0));
taskQueue.addTask(new MouseScrollTask(expectedGraphRange));
taskQueue.addTask(new MouseScrollTask(1));
taskQueue.addTask(new MouseScrollTask(expectedGraphRange - 1));
taskQueue.addFunctionTask(checkGraphRange);
taskQueue.addFunctionTask(sanityCheckWithTimeRange.bind(null, false));
taskQueue.run();
});
netInternalsTest.test('netInternalsTimelineViewLoadLog', function() {
// After loading the log file, the rest of the test runs synchronously.
function testBody() {
netInternalsTest.switchToView('timeline');
sanityCheckWithTimeRange(false);
// Make sure everything's still fine when we switch to another view.
netInternalsTest.switchToView('events');
sanityCheckWithTimeRange(false);
}
// Load a log and then run the rest of the test.
var taskQueue = new netInternalsTest.TaskQueue(true);
taskQueue.addTask(new LoadLogWithNewEventsTask(55, 10055));
taskQueue.addFunctionTask(testBody);
taskQueue.run();
});
netInternalsTest.test('netInternalsTimelineViewZoomOut', function() {
// After loading the log file, the rest of the test runs synchronously.
function testBody() {
netInternalsTest.switchToView('timeline');
mouseZoomOut();
mouseZoomOut();
mouseZoomIn();
sanityCheckWithTimeRange(false);
}
// Load a log and then run the rest of the test.
var taskQueue = new netInternalsTest.TaskQueue(true);
taskQueue.addTask(new LoadLogWithNewEventsTask(55, 10055));
taskQueue.addFunctionTask(testBody);
taskQueue.run();
});
netInternalsTest.test('netInternalsTimelineViewZoomIn', function() {
// After loading the log file, the rest of the test runs synchronously.
function testBody() {
netInternalsTest.switchToView('timeline');
mouseZoomAllTheWayIn();
mouseZoomOut();
sanityCheckWithTimeRange(false);
}
// Load a log and then run the rest of the test.
var taskQueue = new netInternalsTest.TaskQueue(true);
taskQueue.addTask(new LoadLogWithNewEventsTask(55, 10055));
taskQueue.addFunctionTask(testBody);
taskQueue.run();
});
netInternalsTest.test('netInternalsTimelineViewDegenerate', function() {
// After loading the log file, the rest of the test runs synchronously.
function testBody() {
netInternalsTest.switchToView('timeline');
mouseZoomOut();
mouseZoomAllTheWayIn();
mouseZoomOut();
sanityCheckWithTimeRange(false);
}
// Load a log and then run the rest of the test.
var taskQueue = new netInternalsTest.TaskQueue(true);
taskQueue.addTask(new LoadLogWithNewEventsTask(55, 10055));
taskQueue.addFunctionTask(testBody);
taskQueue.run();
});
/**
* Since we don't need to load a log file, this test can run synchronously.
*/
netInternalsTest.test('netInternalsTimelineViewNoEvents', function() {
// Click the events view's delete all button, and then switch to timeline
// view.
netInternalsTest.switchToView('events');
$(EventsView.DELETE_ALL_ID).click();
netInternalsTest.switchToView('timeline');
// Set startTime/endTime for sanity checks.
startTime = graphView.startTime_;
endTime = graphView.endTime_;
sanityCheckWithTimeRange(true);
mouseZoomOut();
sanityCheckWithTimeRange(true);
mouseZoomAllTheWayIn();
sanityCheckWithTimeRange(true);
mouseZoomOut();
sanityCheckWithTimeRange(true);
testDone();
});
})(); // Anonymous namespace
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment