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() {
this.updateEventCounts_();
g_browser.sourceTracker.addObserver(this);
g_browser.sourceTracker.addSourceEntryObserver(this);
}
// ID for special HTML element in category_tabs.html
......
......@@ -5,6 +5,7 @@
<a href="#import" id=tab-handle-import>Import</a>
<a href="#proxy" id=tab-handle-proxy>Proxy</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="#sockets" id=tab-handle-sockets>Sockets</a>
<a href="#spdy" id=tab-handle-spdy>SPDY</a>
......
......@@ -5,35 +5,18 @@
var DetailsView = (function() {
'use strict';
// We inherit from VerticalSplitView.
var superClass = VerticalSplitView;
// We inherit from DivView.
var superClass = DivView;
/**
* The DetailsView handles the tabbed view that displays either the "log" or
* "timeline" view. This class keeps track of what the current view is, and
* invalidates the specific view each time the selected data has changed.
* The DetailsView displays the "log" view. This class keeps track of the
* selected SourceEntries, and repaints when they change.
*
* @constructor
*/
function DetailsView(tabHandlesContainerId,
logTabId,
timelineTabId,
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);
function DetailsView(boxId) {
superClass.call(this, boxId);
this.sourceEntries_ = [];
}
// The delay between updates to repaint.
......@@ -43,17 +26,32 @@ var DetailsView = (function() {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
/**
* Updates the data this view is using.
*/
setData: function(currentData) {
setData: function(sourceEntries) {
// Make a copy of the array (in case the caller mutates it), and sort it
// by the source ID.
var sortedCurrentData = createSortedCopy(currentData);
this.sourceEntries_ = createSortedCopy(sourceEntries);
// TODO(eroman): Should not access private members of TabSwitcherView.
for (var i = 0; i < this.tabSwitcher_.tabs_.length; ++i)
this.tabSwitcher_.tabs_[i].contentView.setData(sortedCurrentData);
// 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 = '';
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() {
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;
})();
......@@ -100,7 +100,7 @@ var DnsView = (function() {
// Date the cache was logged. This will be either now, when actively
// logging data, or the date the log dump was created.
var logDate;
if (MainView.getInstance().isViewingLoadedLog()) {
if (MainView.isViewingLoadedLog()) {
if (typeof ClientInfo.numericDate == 'number') {
logDate = new Date(ClientInfo.numericDate);
} else {
......
......@@ -99,33 +99,3 @@ found in the LICENSE file.
#events-view-source-list-tbody .source_NONE {
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 @@
<!-- Splitter Box: This is a handle to resize the vertical divider -->
<div id=events-view-splitter-box class=vertical-splitter></div>
<!-- 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-timeline-box></div>
......@@ -46,16 +46,12 @@ var EventsView = (function() {
new DivView(EventsView.MIDDLE_BOX_ID),
new DivView(EventsView.BOTTOM_BAR_ID));
this.detailsView_ = new DetailsView(EventsView.TAB_HANDLES_CONTAINER_ID,
EventsView.LOG_TAB_ID,
EventsView.TIMELINE_TAB_ID,
EventsView.DETAILS_LOG_BOX_ID,
EventsView.DETAILS_TIMELINE_BOX_ID);
this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID);
this.splitterView_ = new ResizableVerticalSplitView(
leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID));
g_browser.sourceTracker.addObserver(this);
g_browser.sourceTracker.addSourceEntryObserver(this);
this.tableBody_ = $(EventsView.TBODY_ID);
......@@ -102,11 +98,7 @@ var EventsView = (function() {
EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id';
EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source';
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_TIMELINE_BOX_ID = 'events-view-details-timeline-box';
EventsView.TOPBAR_ID = 'events-view-filter-box';
EventsView.MIDDLE_BOX_ID = 'events-view-source-list';
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.
<head i18n-values="dir:textdirection;">
<link rel="stylesheet" href="main.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="tab_switcher_view.css">
<script src="chrome://resources/js/util.js"></script>
......@@ -43,6 +44,7 @@ found in the LICENSE file.
<include src="test_view.html"/>
<include src="hsts_view.html"/>
<include src="events_view.html"/>
<include src="timeline_view.html"/>
<include src="logs_view.html"/>
<script src="chrome://resources/js/i18n_template.js"></script>
......
......@@ -23,8 +23,11 @@
<include src="events_view.js"/>
<include src="details_view.js"/>
<include src="source_entry.js"/>
<include src="horizontal_scrollbar_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_grouper.js"/>
<include src="proxy_view.js"/>
......
......@@ -23,7 +23,7 @@ body {
background: #bfbfbf;
border-left: 1px inset black;
border-right: 1px solid black;
position:absolute;
position: absolute;
width: 8px;
cursor: col-resize;
user-select: none;
......
......@@ -44,14 +44,15 @@ var MainView = (function() {
function 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
// observers.
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.categoryTabSwitcher_ = new TabSwitcherView();
var tabs = this.categoryTabSwitcher_;
......@@ -82,6 +83,8 @@ var MainView = (function() {
false, true);
tabs.addTab(EventsView.TAB_HANDLE_ID, EventsView.getInstance(),
false, true);
tabs.addTab(TimelineView.TAB_HANDLE_ID, TimelineView.getInstance(),
false, true);
tabs.addTab(DnsView.TAB_HANDLE_ID, DnsView.getInstance(),
false, true);
tabs.addTab(SocketsView.TAB_HANDLE_ID, SocketsView.getInstance(),
......@@ -127,8 +130,6 @@ var MainView = (function() {
// Select the initial view based on the current URL.
window.onhashchange();
g_browser.addConstantsObserver(new ConstantsObserver());
// Tell the browser that we are ready to start receiving log events.
g_browser.sendReady();
}
......@@ -143,6 +144,14 @@ var MainView = (function() {
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 = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
......@@ -163,9 +172,9 @@ var MainView = (function() {
* @param {String} fileName The name of the log file that has been loaded.
*/
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_FILE_ID), true);
......@@ -177,16 +186,8 @@ var MainView = (function() {
g_browser.sourceTracker.setSecurityStripping(false);
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&...".
* Puts the parameters in an object, and passes the resulting object to
......
......@@ -37,7 +37,7 @@ var ProxyView = (function() {
// Register to receive proxy information as it changes.
g_browser.addProxySettingsObserver(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
......
......@@ -198,7 +198,7 @@ var SourceEntry = (function() {
*/
getEndTime: function() {
if (!this.isInactive_) {
return (new Date()).getTime();
return timeutil.getCurrentTime();
} else {
var endTicks = this.entries_[this.entries_.length - 1].time;
return timeutil.convertTimeTicksToDate(endTicks).getTime();
......
......@@ -13,7 +13,12 @@ var SourceTracker = (function() {
* @constructor
*/
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
// displayed events. When true, such information should be hidden from
......@@ -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.
*/
onReceivedLogEntries: function(logEntries) {
......@@ -153,8 +158,12 @@ var SourceTracker = (function() {
}
this.capturedEvents_ = this.capturedEvents_.concat(logEntries);
for (var i = 0; i < this.observers_.length; ++i)
this.observers_[i].onSourceEntriesUpdated(updatedSourceEntries);
for (var i = 0; i < this.sourceEntryObservers_.length; ++i) {
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() {
}
this.capturedEvents_ = newEventList;
for (var i = 0; i < this.observers_.length; ++i)
this.observers_[i].onSourceEntriesDeleted(sourceEntryIds);
for (var i = 0; i < this.sourceEntryObservers_.length; ++i)
this.sourceEntryObservers_[i].onSourceEntriesDeleted(sourceEntryIds);
},
/**
......@@ -188,8 +197,10 @@ var SourceTracker = (function() {
*/
deleteAllSourceEntries: function() {
this.clearEntries_();
for (var i = 0; i < this.observers_.length; ++i)
this.observers_[i].onAllSourceEntriesDeleted();
for (var i = 0; i < this.sourceEntryObservers_.length; ++i)
this.sourceEntryObservers_[i].onAllSourceEntriesDeleted();
for (var i = 0; i < this.logEntryObservers_.length; ++i)
this.logEntryObservers_[i].onAllLogEntriesDeleted();
},
/**
......@@ -198,9 +209,9 @@ var SourceTracker = (function() {
*/
setSecurityStripping: function(enableSecurityStripping) {
this.enableSecurityStripping_ = enableSecurityStripping;
for (var i = 0; i < this.observers_.length; ++i) {
if (this.observers_[i].onSecurityStrippingChanged)
this.observers_[i].onSecurityStrippingChanged();
for (var i = 0; i < this.sourceEntryObservers_.length; ++i) {
if (this.sourceEntryObservers_[i].onSecurityStrippingChanged)
this.sourceEntryObservers_[i].onSecurityStrippingChanged();
}
},
......@@ -213,17 +224,28 @@ var SourceTracker = (function() {
},
/**
* Adds a listener of log entries. |observer| will be called back when new
* log data arrives, source entries are deleted, or security stripping
* changes through:
* Adds a listener of SourceEntries. |observer| will be called back when
* SourceEntries are added or modified, source entries are deleted, or
* security stripping changes:
*
* observer.onSourceEntriesUpdated(sourceEntries)
* observer.deleteSourceEntries(sourceEntryIds)
* ovserver.deleteAllSourceEntries()
* observer.onSourceEntriesDeleted(sourceEntryIds)
* ovserver.onAllSourceEntriesDeleted()
* observer.onSecurityStrippingChanged()
*/
addObserver: function(observer) {
this.observers_.push(observer);
addSourceEntryObserver: function(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() {
dtCell: dtCell,
resultCell: resultCell,
passFailCell: passFailCell,
startTime: (new Date()).getTime()
startTime: timeutil.getCurrentTime()
};
addTextNode(experimentCell, 'Fetch ' + experiment.url);
......@@ -129,7 +129,7 @@ var TestView = (function() {
onCompletedConnectionTestExperiment: function(experiment, result) {
var r = this.currentExperimentRow_;
var endTime = (new Date()).getTime();
var endTime = timeutil.getCurrentTime();
r.dtCell.innerHTML = '';
addTextNode(r.dtCell, (endTime - r.startTime));
......
......@@ -32,8 +32,18 @@ var timeutil = (function() {
return new Date(timeStampMs);
}
/**
* Returns the current time.
*
* @returns {number} Milliseconds since the Unix epoch.
*/
function getCurrentTime() {
return (new Date()).getTime();
}
return {
setTimeTickOffset: setTimeTickOffset,
convertTimeTicksToDate: convertTimeTicksToDate
convertTimeTicksToDate: convertTimeTicksToDate,
getCurrentTime: getCurrentTime
};
})();
This diff is collapsed.
This diff is collapsed.
/*
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() {
/**
* 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 |
......@@ -47,23 +48,34 @@ var TopMidBottomView = (function() {
superClass.prototype.setGeometry.call(this, left, top, width, height);
// Calculate the vertical split points.
var topbarHeight = this.topView_.getHeight();
var bottombarHeight = this.bottomView_.getHeight();
var topbarHeight = 0;
if (this.topView_)
topbarHeight = this.topView_.getHeight();
var bottombarHeight = 0;
if (this.bottomView_)
bottombarHeight = this.bottomView_.getHeight();
var middleboxHeight = height - (topbarHeight + bottombarHeight);
if (middleboxHeight < 0)
middleboxHeight = 0;
// Position the boxes using calculated split points.
this.topView_.setGeometry(left, top, width, topbarHeight);
this.midView_.setGeometry(left, this.topView_.getBottom(),
width, middleboxHeight);
this.bottomView_.setGeometry(left, this.midView_.getBottom(),
width, bottombarHeight);
if (this.topView_)
this.topView_.setGeometry(left, top, width, topbarHeight);
this.midView_.setGeometry(left, top + topbarHeight, width,
middleboxHeight);
if (this.bottomView_) {
this.bottomView_.setGeometry(left, top + topbarHeight + middleboxHeight,
width, bottombarHeight);
}
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
this.topView_.show(isVisible);
if (this.topView_)
this.topView_.show(isVisible);
this.midView_.show(isVisible);
this.bottomView_.show(isVisible);
if (this.bottomView_)
this.bottomView_.show(isVisible);
}
};
......
......@@ -228,6 +228,7 @@ void NetInternalsTest::SetUpInProcessBrowserTestFixture() {
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/test_view.js")));
AddLibrary(FilePath(FILE_PATH_LITERAL("net_internals/timeline_view.js")));
}
void NetInternalsTest::SetUpOnMainThread() {
......@@ -301,6 +302,48 @@ IN_PROC_BROWSER_TEST_F(NetInternalsTest, 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
////////////////////////////////////////////////////////////////////////////////
......
......@@ -19,6 +19,7 @@ netInternalsTest.test('netInternalsExportImportDump', function() {
import: true,
proxy: true,
events: true,
timeline: true,
dns: true,
sockets: true,
spdy: true,
......
......@@ -20,6 +20,7 @@ netInternalsTest.test('netInternalsTourTabs', function() {
import: true,
proxy: true,
events: true,
timeline: true,
dns: true,
sockets: true,
spdy: true,
......
......@@ -35,6 +35,7 @@ var netInternalsTest = (function() {
import: ImportView.TAB_HANDLE_ID,
proxy: ProxyView.TAB_HANDLE_ID,
events: EventsView.TAB_HANDLE_ID,
timeline: TimelineView.TAB_HANDLE_ID,
dns: DnsView.TAB_HANDLE_ID,
sockets: SocketsView.TAB_HANDLE_ID,
spdy: SpdyView.TAB_HANDLE_ID,
......@@ -317,6 +318,15 @@ var netInternalsTest = (function() {
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
* again.
......@@ -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'.
* @param {node}: node The node to check.
......@@ -399,6 +432,72 @@ var netInternalsTest = (function() {
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.
return {
test: test,
......@@ -408,8 +507,13 @@ var netInternalsTest = (function() {
isDisplayed: isDisplayed,
runTest: runTest,
switchToView: switchToView,
TaskQueue: TaskQueue,
Task: Task,
TaskQueue: TaskQueue
Source: Source,
Event: Event,
CreateBeginEvent: CreateBeginEvent,
CreateEndEvent: CreateEndEvent,
CreateMatchingEndEvent: CreateMatchingEndEvent
};
})();
......
This diff is collapsed.
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