summaryrefslogtreecommitdiffstats
path: root/chrome/browser
diff options
context:
space:
mode:
authoreroman@chromium.org <eroman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-03-23 17:47:49 +0000
committereroman@chromium.org <eroman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-03-23 17:47:49 +0000
commitfd9c0d9df665355c14d3c0df6f3c5b0b2059c129 (patch)
tree1d2c2cb1cd339587089ae0f913864ef163cf2778 /chrome/browser
parent3773020c131d144f0e0e1d849227b34d7c4cf6e1 (diff)
downloadchromium_src-fd9c0d9df665355c14d3c0df6f3c5b0b2059c129.zip
chromium_src-fd9c0d9df665355c14d3c0df6f3c5b0b2059c129.tar.gz
chromium_src-fd9c0d9df665355c14d3c0df6f3c5b0b2059c129.tar.bz2
Add an initial implementation of net-internals inspector in javascript.
BUG=37421 Review URL: http://codereview.chromium.org/1088007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@42357 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r--chrome/browser/browser_resources.grd1
-rw-r--r--chrome/browser/dom_ui/net_internals_ui.cc223
-rw-r--r--chrome/browser/resources/net_internals/detailsview.js93
-rw-r--r--chrome/browser/resources/net_internals/index.html87
-rw-r--r--chrome/browser/resources/net_internals/layoutmanager.js151
-rw-r--r--chrome/browser/resources/net_internals/loggrouper.js95
-rw-r--r--chrome/browser/resources/net_internals/logviewpainter.js117
-rw-r--r--chrome/browser/resources/net_internals/main.css134
-rw-r--r--chrome/browser/resources/net_internals/main.js90
-rw-r--r--chrome/browser/resources/net_internals/requestsview.js165
-rw-r--r--chrome/browser/resources/net_internals/sourceentry.js182
-rw-r--r--chrome/browser/resources/net_internals/timelineviewpainter.js20
-rw-r--r--chrome/browser/resources/net_internals/util.js88
13 files changed, 1366 insertions, 80 deletions
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index ce10283..2562a2c 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -65,7 +65,6 @@ without changes to the corresponding grd file. fbt1 -->
<include name="IDR_NOTIFICATION_ICON_HTML" file="resources\notification_icon.html" type="BINDATA" />
<include name="IDR_NOTIFICATION_2LINE_HTML" file="resources\notification_2line.html" type="BINDATA" />
<include name="IDR_NOTIFICATION_1LINE_HTML" file="resources\notification_1line.html" type="BINDATA" />
- <include name="IDR_NET_INTERNALS_HTML" file="resources\net_internals\index.html" type="BINDATA" />
</includes>
</release>
</grit>
diff --git a/chrome/browser/dom_ui/net_internals_ui.cc b/chrome/browser/dom_ui/net_internals_ui.cc
index 5d7567c..c04b30a 100644
--- a/chrome/browser/dom_ui/net_internals_ui.cc
+++ b/chrome/browser/dom_ui/net_internals_ui.cc
@@ -5,17 +5,25 @@
#include "chrome/browser/dom_ui/net_internals_ui.h"
#include "app/resource_bundle.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
#include "base/singleton.h"
#include "base/string_piece.h"
+#include "base/string_util.h"
#include "base/values.h"
+#include "chrome/browser/browser_process.h"
#include "chrome/browser/dom_ui/chrome_url_data_manager.h"
#include "chrome/browser/chrome_thread.h"
+#include "chrome/browser/io_thread.h"
+#include "chrome/browser/net/chrome_net_log.h"
+#include "chrome/common/chrome_paths.h"
#include "chrome/common/url_constants.h"
-#include "grit/browser_resources.h"
-
namespace {
+// TODO(eroman): Bootstrap the net-internals page using the passively logged
+// data.
+
class NetInternalsHTMLSource : public ChromeURLDataManager::DataSource {
public:
NetInternalsHTMLSource();
@@ -72,7 +80,8 @@ class NetInternalsMessageHandler
class NetInternalsMessageHandler::IOThreadImpl
: public base::RefCountedThreadSafe<
NetInternalsMessageHandler::IOThreadImpl,
- ChromeThread::DeleteOnUIThread> {
+ ChromeThread::DeleteOnUIThread>,
+ public ChromeNetLog::Observer {
public:
// Type for methods that can be used as MessageHandler callbacks.
typedef void (IOThreadImpl::*MessageHandler)(const Value*);
@@ -80,8 +89,11 @@ class NetInternalsMessageHandler::IOThreadImpl
// Creates a proxy for |handler| that will live on the IO thread.
// |handler| is a weak pointer, since it is possible for the DOMMessageHandler
// to be deleted on the UI thread while we were executing on the IO thread.
- explicit IOThreadImpl(
- const base::WeakPtr<NetInternalsMessageHandler>& handler);
+ // |io_thread| is the global IOThread (it is passed in as an argument since
+ // we need to grab it from the UI thread).
+ IOThreadImpl(
+ const base::WeakPtr<NetInternalsMessageHandler>& handler,
+ IOThread* io_thread);
~IOThreadImpl();
@@ -91,9 +103,6 @@ class NetInternalsMessageHandler::IOThreadImpl
// on the IO thread.
DOMUI::MessageCallback* CreateCallback(MessageHandler method);
- // Called once the DOMUI has attached to the renderer, on the IO thread.
- void Attach();
-
// Called once the DOMUI has been deleted (i.e. renderer went away), on the
// IO thread.
void Detach();
@@ -102,8 +111,12 @@ class NetInternalsMessageHandler::IOThreadImpl
// Javascript message handlers:
//--------------------------------
- // TODO(eroman): This is temporary!
- void OnTestMessage(const Value* value);
+ // This message is called after the webpage's onloaded handler has fired.
+ // it indicates that the renderer is ready to start receiving captured data.
+ void OnRendererReady(const Value* value);
+
+ // ChromeNetLog::Observer implementation:
+ virtual void OnAddEntry(const net::NetLog::Entry& entry);
private:
class CallbackHelper;
@@ -120,6 +133,12 @@ class NetInternalsMessageHandler::IOThreadImpl
// Pointer to the UI-thread message handler. Only access this from
// the UI thread.
base::WeakPtr<NetInternalsMessageHandler> handler_;
+
+ // The global IOThread, which contains the global NetLog to observer.
+ IOThread* io_thread_;
+
+ // True if we have attached an observer to the NetLog already.
+ bool is_observing_log_;
friend class base::RefCountedThreadSafe<IOThreadImpl>;
};
@@ -169,19 +188,30 @@ NetInternalsHTMLSource::NetInternalsHTMLSource()
void NetInternalsHTMLSource::StartDataRequest(const std::string& path,
bool is_off_the_record,
int request_id) {
- // Serve up the HTML contained in the resource bundle.
- base::StringPiece html(
- ResourceBundle::GetSharedInstance().GetRawDataResource(
- IDR_NET_INTERNALS_HTML));
+ // The provided |path| identifies a file in resources/net_internals/.
+ std::string data_string;
+ FilePath file_path;
+ PathService::Get(chrome::DIR_NET_INTERNALS, &file_path);
+ std::string filename = path.empty() ? "index.html" : path;
+ file_path = file_path.AppendASCII(filename);
+
+ if (!file_util::ReadFileToString(file_path, &data_string)) {
+ LOG(WARNING) << "Could not read resource: " << file_path.value();
+ data_string = StringPrintf(
+ "Failed to read file RESOURCES/net_internals/%s",
+ filename.c_str());
+ }
- scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes);
- html_bytes->data.resize(html.size());
- std::copy(html.begin(), html.end(), html_bytes->data.begin());
+ scoped_refptr<RefCountedBytes> bytes(new RefCountedBytes);
+ bytes->data.resize(data_string.size());
+ std::copy(data_string.begin(), data_string.end(), bytes->data.begin());
- SendResponse(request_id, html_bytes);
+ SendResponse(request_id, bytes);
}
std::string NetInternalsHTMLSource::GetMimeType(const std::string&) const {
+ // TODO(eroman): This is incorrect -- some of the subresources may be
+ // css/javascript.
return "text/html";
}
@@ -203,23 +233,16 @@ NetInternalsMessageHandler::~NetInternalsMessageHandler() {
DOMMessageHandler* NetInternalsMessageHandler::Attach(DOMUI* dom_ui) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
- proxy_ = new IOThreadImpl(this->AsWeakPtr());
-
+ proxy_ = new IOThreadImpl(this->AsWeakPtr(), g_browser_process->io_thread());
DOMMessageHandler* result = DOMMessageHandler::Attach(dom_ui);
-
- // Notify the handler on the IO thread that a renderer is attached.
- ChromeThread::PostTask(ChromeThread::IO, FROM_HERE,
- NewRunnableMethod(proxy_.get(), &IOThreadImpl::Attach));
-
return result;
}
void NetInternalsMessageHandler::RegisterMessages() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
- // TODO(eroman): Register message handlers here.
- dom_ui_->RegisterMessageCallback("testMessage",
- proxy_->CreateCallback(&IOThreadImpl::OnTestMessage));
+ dom_ui_->RegisterMessageCallback("notifyReady",
+ proxy_->CreateCallback(&IOThreadImpl::OnRendererReady));
}
void NetInternalsMessageHandler::CallJavascriptFunction(
@@ -236,8 +259,11 @@ void NetInternalsMessageHandler::CallJavascriptFunction(
////////////////////////////////////////////////////////////////////////////////
NetInternalsMessageHandler::IOThreadImpl::IOThreadImpl(
- const base::WeakPtr<NetInternalsMessageHandler>& handler)
- : handler_(handler) {
+ const base::WeakPtr<NetInternalsMessageHandler>& handler,
+ IOThread* io_thread)
+ : handler_(handler),
+ io_thread_(io_thread),
+ is_observing_log_(false) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI));
}
@@ -252,38 +278,131 @@ NetInternalsMessageHandler::IOThreadImpl::CreateCallback(
return new CallbackHelper(this, method);
}
-void NetInternalsMessageHandler::IOThreadImpl::Attach() {
- DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO));
- // TODO(eroman): Register with network stack to observe events.
-}
-
void NetInternalsMessageHandler::IOThreadImpl::Detach() {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO));
- // TODO(eroman): Unregister with network stack to observe events.
+ // Unregister with network stack to observe events.
+ if (is_observing_log_)
+ io_thread_->globals()->net_log->RemoveObserver(this);
}
-void NetInternalsMessageHandler::IOThreadImpl::OnTestMessage(
+void NetInternalsMessageHandler::IOThreadImpl::OnRendererReady(
const Value* value) {
DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO));
+ DCHECK(!is_observing_log_) << "notifyReady called twice";
+
+ // Register with network stack to observe events.
+ is_observing_log_ = true;
+ io_thread_->globals()->net_log->AddObserver(this);
+
+ // Tell the javascript about the relationship between event type enums and
+ // their symbolic name.
+ {
+ std::vector<net::NetLog::EventType> event_types =
+ net::NetLog::GetAllEventTypes();
+
+ DictionaryValue* dict = new DictionaryValue();
- // TODO(eroman): This is just a temporary method, to see something in
- // action. We expect to have been called with an array
- // containing 1 string, and print it to the screen.
- std::string str;
- if (value && value->GetType() == Value::TYPE_LIST) {
- const ListValue* list_value = static_cast<const ListValue*>(value);
- Value* list_member;
- if (list_value->Get(0, &list_member) &&
- list_member->GetType() == Value::TYPE_STRING) {
- const StringValue* string_value =
- static_cast<const StringValue*>(list_member);
- string_value->GetAsString(&str);
+ for (size_t i = 0; i < event_types.size(); ++i) {
+ const char* name = net::NetLog::EventTypeToString(event_types[i]);
+ dict->SetInteger(ASCIIToWide(name),
+ static_cast<int>(event_types[i]));
}
+
+ CallJavascriptFunction(L"setLogEventTypeConstants", dict);
+ }
+
+ // Tell the javascript about the relationship between event phase enums and
+ // their symbolic name.
+ {
+ DictionaryValue* dict = new DictionaryValue();
+
+ dict->SetInteger(L"PHASE_BEGIN", net::NetLog::PHASE_BEGIN);
+ dict->SetInteger(L"PHASE_END", net::NetLog::PHASE_END);
+ dict->SetInteger(L"PHASE_NONE", net::NetLog::PHASE_NONE);
+
+ CallJavascriptFunction(L"setLogEventPhaseConstants", dict);
+ }
+
+ // Tell the javascript about the relationship between source type enums and
+ // their symbolic name.
+ // TODO(eroman): Don't duplicate the values, it will never stay up to date!
+ {
+ DictionaryValue* dict = new DictionaryValue();
+
+ dict->SetInteger(L"NONE", net::NetLog::SOURCE_NONE);
+ dict->SetInteger(L"URL_REQUEST", net::NetLog::SOURCE_URL_REQUEST);
+ dict->SetInteger(L"SOCKET_STREAM", net::NetLog::SOURCE_SOCKET_STREAM);
+ dict->SetInteger(L"INIT_PROXY_RESOLVER",
+ net::NetLog::SOURCE_INIT_PROXY_RESOLVER);
+ dict->SetInteger(L"CONNECT_JOB", net::NetLog::SOURCE_CONNECT_JOB);
+
+ CallJavascriptFunction(L"setLogSourceTypeConstants", dict);
+ }
+
+ // Tell the javascript about the relationship between entry type enums and
+ // their symbolic name.
+ {
+ DictionaryValue* dict = new DictionaryValue();
+
+ dict->SetInteger(L"TYPE_EVENT", net::NetLog::Entry::TYPE_EVENT);
+ dict->SetInteger(L"TYPE_STRING", net::NetLog::Entry::TYPE_STRING);
+ dict->SetInteger(L"TYPE_ERROR_CODE", net::NetLog::Entry::TYPE_ERROR_CODE);
+
+ CallJavascriptFunction(L"setLogEntryTypeConstants", dict);
+ }
+}
+
+void NetInternalsMessageHandler::IOThreadImpl::OnAddEntry(
+ const net::NetLog::Entry& entry) {
+ DCHECK(is_observing_log_);
+
+ // JSONify the NetLog::Entry.
+ // TODO(eroman): Need a better format for this.
+ DictionaryValue* entry_dict = new DictionaryValue();
+
+ // Set the entry type.
+ {
+ net::NetLog::Entry::Type entry_type = entry.type;
+ if (entry_type == net::NetLog::Entry::TYPE_STRING_LITERAL)
+ entry_type = net::NetLog::Entry::TYPE_STRING;
+ entry_dict->SetInteger(L"type", static_cast<int>(entry_type));
+ }
+
+ // Set the entry time.
+ entry_dict->SetInteger(
+ L"time",
+ static_cast<int>((entry.time - base::TimeTicks()).InMilliseconds()));
+
+ // Set the entry source.
+ DictionaryValue* source_dict = new DictionaryValue();
+ source_dict->SetInteger(L"id", entry.source.id);
+ source_dict->SetInteger(L"type", static_cast<int>(entry.source.type));
+ entry_dict->Set(L"source", source_dict);
+
+ // Set the event info (if it is an event entry).
+ if (entry.type == net::NetLog::Entry::TYPE_EVENT) {
+ DictionaryValue* event_dict = new DictionaryValue();
+ event_dict->SetInteger(L"type", static_cast<int>(entry.event.type));
+ event_dict->SetInteger(L"phase", static_cast<int>(entry.event.phase));
+ entry_dict->Set(L"event", event_dict);
+ }
+
+ // Add the string information (events my have a string too, due to current
+ // hacks).
+ if (entry.type == net::NetLog::Entry::TYPE_STRING || !entry.string.empty()) {
+ entry_dict->SetString(L"string", entry.string);
+ }
+
+ // Treat string literals the same as strings.
+ if (entry.type == net::NetLog::Entry::TYPE_STRING_LITERAL) {
+ entry_dict->SetString(L"string", entry.literal);
+ }
+
+ if (entry.type == net::NetLog::Entry::TYPE_ERROR_CODE) {
+ entry_dict->SetInteger(L"error_code", entry.error_code);
}
- CallJavascriptFunction(
- L"log",
- Value::CreateStringValue("Browser received testMessage: " + str));
+ CallJavascriptFunction(L"onLogEntryAdded", entry_dict);
}
void NetInternalsMessageHandler::IOThreadImpl::DispatchToMessageHandler(
diff --git a/chrome/browser/resources/net_internals/detailsview.js b/chrome/browser/resources/net_internals/detailsview.js
new file mode 100644
index 0000000..271c19f
--- /dev/null
+++ b/chrome/browser/resources/net_internals/detailsview.js
@@ -0,0 +1,93 @@
+// Copyright (c) 2010 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.
+
+/**
+ * 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.
+ *
+ * @constructor
+ */
+function DetailsView(logTabHandleId,
+ timelineTabHandleId,
+ detailsTabContentId) {
+ // The DOM nodes that which contain the tab title.
+ this.tabHandles_ = {};
+
+ this.tabHandles_['timeline'] = document.getElementById(timelineTabHandleId);
+ this.tabHandles_['log'] = document.getElementById(logTabHandleId);
+
+ // The DOM node that contains the currently active tab sheet.
+ this.contentArea_ = document.getElementById(detailsTabContentId);
+
+ // Attach listeners to the "tab handles" so when you click them, it switches
+ // active view.
+
+ var self = this;
+
+ this.tabHandles_['timeline'].onclick = function() {
+ self.switchToView_('timeline');
+ };
+
+ this.tabHandles_['log'].onclick = function() {
+ self.switchToView_('log');
+ };
+
+ this.currentData_ = [];
+
+ // Default to the log view.
+ this.switchToView_('log');
+};
+
+// The delay between updates to repaint.
+DetailsView.REPAINT_TIMEOUT_MS = 50;
+
+/**
+ * Switches to the tab with name |viewName|. (Either 'log' or 'timeline'.
+ */
+DetailsView.prototype.switchToView_ = function(viewName) {
+ if (this.currentView_) {
+ // Remove the selected styling on currently selected tab.
+ changeClassName(this.tabHandles_[this.currentView_], 'selected', false);
+ }
+
+ this.currentView_ = viewName;
+ changeClassName(this.tabHandles_[this.currentView_], 'selected', true);
+ this.repaint_();
+};
+
+/**
+ * Updates the data this view is using.
+ */
+DetailsView.prototype.setData = function(currentData) {
+ // Make a copy of the array (in case the caller mutates it), and sort it
+ // by the source ID.
+ this.currentData_ = DetailsView.createSortedCopy_(currentData);
+
+ // Invalidate the view.
+ if (!this.outstandingRepaint_) {
+ this.outstandingRepaint_ = true;
+ window.setTimeout(this.repaint_.bind(this),
+ DetailsView.REPAINT_TIMEOUT_MS);
+ }
+};
+
+DetailsView.prototype.repaint_ = function() {
+ this.outstandingRepaint_ = false;
+ this.contentArea_.innerHTML = '';
+
+ if (this.currentView_ == 'log') {
+ PaintLogView(this.currentData_, this.contentArea_);
+ } else {
+ PaintTimelineView(this.currentData_, this.contentArea_);
+ }
+};
+
+DetailsView.createSortedCopy_ = function(origArray) {
+ var sortedArray = origArray.slice(0);
+ sortedArray.sort(function(a, b) {
+ return a.getSourceId() - b.getSourceId();
+ });
+ return sortedArray;
+};
diff --git a/chrome/browser/resources/net_internals/index.html b/chrome/browser/resources/net_internals/index.html
index 7aff02c..fab3304 100644
--- a/chrome/browser/resources/net_internals/index.html
+++ b/chrome/browser/resources/net_internals/index.html
@@ -1,32 +1,65 @@
<html>
+<!--
+Copyright (c) 2010 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.
+-->
<head>
- <title>Under construction...</title>
- <script>
-
-// TODO(eroman): This is all temporary...
-
-function sendTestMessageToBrowser() {
- log("Sent message to browser");
- chrome.send('testMessage', [String((new Date()).toLocaleTimeString())]);
-}
-
-function log(msg) {
- var l = document.getElementById('log');
- l.appendChild(document.createTextNode(msg + "\n"));
-}
-
- </script>
+ <link tyle="text/css" rel="stylesheet" href="main.css" />
+ <script src="main.js"></script>
+ <script src="util.js"></script>
+ <script src="requestsview.js"></script>
+ <script src="detailsview.js"></script>
+ <script src="sourceentry.js"></script>
+ <script src="layoutmanager.js"></script>
+ <script src="timelineviewpainter.js"></script>
+ <script src="logviewpainter.js"></script>
+ <script src="loggrouper.js"></script>
</head>
-
-
- <body>
- <p>This is a work in progress. See http://crbug.com/37421 for details.</p>
-
- <input onclick="sendTestMessageToBrowser()"
- value="SendTestMessageToBrowser"
- type=button />
-
- <pre id=log></pre>
-
+ <body onload="onLoaded()">
+ <!-- Filter Box: This the top bar which contains the search box. -->
+ <div id=filterBox>
+ <table width=100% height=100%>
+ <tr>
+ <td width=1%>Filter:</td>
+ <td width=98%><input type="search" incremental id=filterInput /></td>
+ <td width=1% id=filterCount>(1 of 34)</td>
+ </tr>
+ </table>
+ </div>
+ <!-- Requests Box: This the panel on the left which lists the requests -->
+ <div id=requestsBox>
+ <table id=requestsListTable cellspacing=0 cellpadding=0>
+ <thead>
+ <tr>
+ <td><input type=checkbox id=selectAll /></td>
+ <td>ID</td>
+ <td>Source</td>
+ <td>URL</td>
+ </tr>
+ </thead>
+ <!-- Requests table body: This is where request rows go into -->
+ <tbody id=requestsListTableBody></tbody>
+ </table>
+ </div>
+ <!-- Action Box: This is a button bar along the bottom -->
+ <div id=actionBox>
+ <input type=button value="Stop capturing" onclick="alert('TODO')" />
+ <input type=button value="Delete selected" id=deleteSelected />
+ </div>
+ <!-- Splitter Box: This is a handle to resize the vertical divider -->
+ <div id=splitterBox></div>
+ <!-- Details box: This is the panel on the right which shows information -->
+ <div id=detailsBox>
+ <table class=tabSwitcher cellspacing=0>
+ <tr>
+ <th id=detailsLogTabHandle>Log</th>
+ <td class=tabSwitcherSpacer>&nbsp;</td>
+ <th id=detailsTimelineTabHandle>Timeline</th>
+ </tr>
+ </table>
+ <div class=tabSwitcherLine></div>
+ <div id=detailsTabArea></div>
+ </div>
</body>
</html>
diff --git a/chrome/browser/resources/net_internals/layoutmanager.js b/chrome/browser/resources/net_internals/layoutmanager.js
new file mode 100644
index 0000000..0491a63
--- /dev/null
+++ b/chrome/browser/resources/net_internals/layoutmanager.js
@@ -0,0 +1,151 @@
+// Copyright (c) 2010 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.
+
+/**
+ * The class LayoutManager implements a vertically split layout that takes up
+ * the whole window, and provides a draggable bar to resize the left and right
+ * panes. Its elements are layed out as follows:
+ *
+ *
+ * <<-- sizer -->>
+ *
+ * +----------------------++----------------+
+ * | topbar || |
+ * +----------------------+| |
+ * | || |
+ * | || |
+ * | || |
+ * | || |
+ * | middlebox || rightbox |
+ * | || |
+ * | || |
+ * | || |
+ * | || |
+ * | || |
+ * +----------------------++ |
+ * | bottombar || |
+ * +----------------------++----------------+
+ *
+ * The "topbar" and "bottombar" have a fixed height which is determined
+ * by their initial content height. The rest of the boxes fill to occupy the
+ * remaining space.
+ *
+ * The consumer provides DIVs for each of these regions, and the LayoutManager
+ * positions them correctly in the window.
+ *
+ * @constructor
+ */
+function LayoutManager(topbarId,
+ middleboxId,
+ bottombarId,
+ sizerId,
+ rightboxId) {
+ // Lookup the elements.
+ this.topbar_ = document.getElementById(topbarId);
+ this.middlebox_ = document.getElementById(middleboxId);
+ this.bottombar_ = document.getElementById(bottombarId);
+ this.sizer_ = document.getElementById(sizerId);
+ this.rightbox_ = document.getElementById(rightboxId);
+
+ // Make all the elements absolutely positioned.
+ this.topbar_.style.position = "absolute";
+ this.middlebox_.style.position = "absolute";
+ this.bottombar_.style.position = "absolute";
+ this.sizer_.style.position = "absolute";
+ this.rightbox_.style.position = "absolute";
+
+ // Set the initial split position at 50%.
+ setNodeWidth(this.rightbox_, window.innerWidth / 2);
+
+ // Setup the "sizer" so it can be dragged left/right to reposition the
+ // vertical split.
+ this.sizer_.addEventListener("mousedown", this.onDragSizerStart_.bind(this),
+ true);
+
+ // Recalculate the layout whenever the window size changes.
+ window.addEventListener("resize", this.recalculateLayout_.bind(this), true);
+
+ // Do the initial layout .
+ this.recalculateLayout_();
+}
+
+// Minimum width to size panels to, in pixels.
+LayoutManager.MIN_PANEL_WIDTH = 50;
+
+/**
+ * Repositions all of the elements to fit the window.
+ */
+LayoutManager.prototype.recalculateLayout_ = function() {
+ // Calculate the horizontal split points.
+ var totalWidth = window.innerWidth;
+ var rightboxWidth = this.rightbox_.offsetWidth;
+ var sizerWidth = this.sizer_.offsetWidth;
+ var leftPaneWidth = totalWidth - (rightboxWidth + sizerWidth);
+
+ // Don't let the left pane get too small.
+ if (leftPaneWidth < LayoutManager.MIN_PANEL_WIDTH) {
+ leftPaneWidth = LayoutManager.MIN_PANEL_WIDTH;
+ rightboxWidth = totalWidth - (sizerWidth + leftPaneWidth);
+ }
+
+ // Calculate the vertical split points.
+ var totalHeight = window.innerHeight;
+ var topbarHeight = this.topbar_.offsetHeight;
+ var bottombarHeight = this.bottombar_.offsetHeight;
+ var middleboxHeight = totalHeight - (topbarHeight + bottombarHeight);
+
+ // Position the boxes using calculated split points.
+ setNodePosition(this.topbar_, 0, 0,
+ leftPaneWidth, topbarHeight);
+ setNodePosition(this.middlebox_, 0, topbarHeight,
+ leftPaneWidth,
+ middleboxHeight);
+ setNodePosition(this.bottombar_, 0, (topbarHeight + middleboxHeight),
+ leftPaneWidth, bottombarHeight);
+
+ setNodePosition(this.sizer_, leftPaneWidth, 0,
+ sizerWidth, totalHeight);
+ setNodePosition(this.rightbox_, leftPaneWidth + sizerWidth, 0,
+ rightboxWidth, totalHeight);
+};
+
+/**
+ * Called once we have clicked into the sizer. Starts capturing the mouse
+ * position to implement dragging.
+ */
+LayoutManager.prototype.onDragSizerStart_ = function(event) {
+ this.sizerMouseMoveListener_ = this.onDragSizer.bind(this);
+ this.sizerMouseUpListener_ = this.onDragSizerEnd.bind(this);
+
+ window.addEventListener("mousemove", this.sizerMouseMoveListener_, true);
+ window.addEventListener("mouseup", this.sizerMouseUpListener_, true);
+
+ event.preventDefault();
+};
+
+/**
+ * Called when the mouse has moved after dragging started.
+ */
+LayoutManager.prototype.onDragSizer = function(event) {
+ var newWidth = window.innerWidth - event.pageX;
+
+ // Avoid shrinking the right box too much.
+ newWidth = Math.max(newWidth, LayoutManager.MIN_PANEL_WIDTH);
+
+ setNodeWidth(this.rightbox_, newWidth);
+ this.recalculateLayout_();
+};
+
+/**
+ * Called once the mouse has been released, and the dragging is over.
+ */
+LayoutManager.prototype.onDragSizerEnd = function(event) {
+ window.removeEventListener("mousemove", this.sizerMouseMoveListener_, true);
+ window.removeEventListener("mouseup", this.sizerMouseUpListener_, true);
+
+ this.sizerMouseMoveListener_ = null;
+ this.sizerMouseUpListener_ = null;
+
+ event.preventDefault();
+};
diff --git a/chrome/browser/resources/net_internals/loggrouper.js b/chrome/browser/resources/net_internals/loggrouper.js
new file mode 100644
index 0000000..a3cf7a5
--- /dev/null
+++ b/chrome/browser/resources/net_internals/loggrouper.js
@@ -0,0 +1,95 @@
+// Copyright (c) 2010 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.
+
+/**
+ * LogGroupEntry is a wrapper around log entries, which makes it easier to
+ * find the corresponding start/end of events.
+ *
+ * This is used internally by the log and timeline views to pretty print
+ * collections of log entries.
+ *
+ * @fileoverview
+ */
+
+// TODO(eroman): document these methods!
+
+function LogGroupEntry(origEntry, index) {
+ this.orig = origEntry;
+ this.index = index;
+}
+
+LogGroupEntry.prototype.isBegin = function() {
+ return this.orig.type == LogEntryType.TYPE_EVENT &&
+ this.orig.event.phase == LogEventPhase.PHASE_BEGIN;
+};
+
+LogGroupEntry.prototype.isEnd = function() {
+ return this.orig.type == LogEntryType.TYPE_EVENT &&
+ this.orig.event.phase == LogEventPhase.PHASE_END
+};
+
+LogGroupEntry.prototype.getDepth = function() {
+ var depth = 0;
+ var p = this.parentEntry;
+ while (p) {
+ depth += 1;
+ p = p.parentEntry;
+ }
+ return depth;
+};
+
+function findParentIndex(parentStack, eventType) {
+ for (var i = parentStack.length - 1; i >= 0; --i) {
+ if (parentStack[i].orig.event.type == eventType)
+ return i;
+ }
+ return -1;
+}
+
+/**
+ * Returns a list of LogGroupEntrys. This basically wraps the original log
+ * entry, but makes it easier to find the start/end of the event.
+ */
+LogGroupEntry.createArrayFrom = function(origEntries) {
+ var groupedEntries = [];
+
+ // Stack of enclosing PHASE_BEGIN elements.
+ var parentStack = [];
+
+ for (var i = 0; i < origEntries.length; ++i) {
+ var origEntry = origEntries[i];
+
+ var groupEntry = new LogGroupEntry(origEntry, i);
+ groupedEntries.push(groupEntry);
+
+ // If this is the end of an event, match it to the start.
+ if (groupEntry.isEnd()) {
+ // Walk up the parent stack to find the corresponding BEGIN for this END.
+ var parentIndex =
+ findParentIndex(parentStack, groupEntry.orig.event.type);
+
+ if (parentIndex == -1) {
+ // Unmatched end.
+ } else {
+ groupEntry.begin = parentStack[parentIndex];
+
+ // Consider this as the terminator for all open BEGINs up until
+ // parentIndex.
+ for (var j = 0; j < parentStack.length - parentIndex; ++j) {
+ var p = parentStack.pop();
+ p.end = groupEntry;
+ }
+ }
+ }
+
+ // Inherit the current parent.
+ if (parentStack.length > 0)
+ groupEntry.parentEntry = parentStack[parentStack.length - 1];
+
+ if (groupEntry.isBegin())
+ parentStack.push(groupEntry);
+ }
+
+ return groupedEntries;
+}
diff --git a/chrome/browser/resources/net_internals/logviewpainter.js b/chrome/browser/resources/net_internals/logviewpainter.js
new file mode 100644
index 0000000..8a068f7
--- /dev/null
+++ b/chrome/browser/resources/net_internals/logviewpainter.js
@@ -0,0 +1,117 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * TODO(eroman): This needs better presentation, and cleaner code. This
+ * implementation is more of a transitionary step as
+ * the old net-internals is replaced.
+ */
+
+var PaintLogView;
+
+// Start of anonymous namespace.
+(function() {
+
+PaintLogView = function(sourceEntries, node) {
+ for (var i = 0; i < sourceEntries.length; ++i) {
+ if (i != 0)
+ addNode(node, 'hr');
+ addSourceEntry_(node, sourceEntries[i]);
+ }
+}
+
+const INDENTATION_PX = 20;
+
+function addSourceEntry_(node, sourceEntry) {
+ var div = addNode(node, 'div');
+ div.className = 'logSourceEntry';
+
+ var p = addNode(div, 'p');
+ var nobr = addNode(p, 'nobr');
+
+ addTextNode(nobr, sourceEntry.getDescription());
+
+ var groupedEntries = LogGroupEntry.createArrayFrom(
+ sourceEntry.getLogEntries());
+
+ makeLoadLogTable_(div, groupedEntries);
+}
+
+function makeLoadLogTable_(node, entries) {
+ var table = addNode(node, 'table');
+ var tbody = addNode(node, 'tbody');
+
+ for (var i = 0; i < entries.length; ++i) {
+ var entry = entries[i];
+
+ // Avoid printing the END for a BEGIN that was immediately before.
+ if (entry.isEnd() && entry.begin && entry.begin.index == i - 1) {
+ continue;
+ }
+
+ var tr = addNode(node, 'tr');
+
+ var timeLabelCell = addNode(tr, 'td');
+ addTextNode(timeLabelCell, 't=');
+
+ var timeCell = addNode(tr, 'td');
+ timeCell.style.textAlign = 'right';
+ timeCell.style.paddingRight = '5px';
+ addTextNode(timeCell, entry.orig.time);
+
+ var mainCell = addNode(tr, 'td');
+ mainCell.style.paddingRight = '5px';
+ var dtLabelCell = addNode(tr, 'td');
+ var dtCell = addNode(tr, 'td');
+ dtCell.style.textAlign = 'right';
+
+ mainCell.style.paddingLeft = (INDENTATION_PX * entry.getDepth()) + "px";
+
+ if (entry.orig.type == LogEntryType.TYPE_EVENT) {
+ addTextNode(mainCell, getTextForEvent(entry));
+
+ // Get the elapsed time.
+ if (entry.isBegin()) {
+ addTextNode(dtLabelCell, '[dt=');
+
+ // Definite time.
+ if (entry.end) {
+ var dt = entry.end.orig.time - entry.orig.time;
+ addTextNode(dtCell, dt + ']');
+ } else {
+ addTextNode(dtCell, '?]');
+ }
+ }
+ } else {
+ mainCell.colSpan = '3';
+ if (entry.orig.type == LogEntryType.TYPE_STRING) {
+ addTextNode(mainCell, entry.orig.string);
+ } else if (entry.orig.type == LogEntryType.TYPE_ERROR_CODE) {
+ // TODO(eroman): print symbolic name of error code.
+ addTextNode(mainCell, "Network error: " + entry.orig.error_code);
+ } else {
+ addTextNode(mainCell, "Unrecognized entry type: " + entry.orig.type);
+ }
+ }
+ }
+}
+
+function getTextForEvent(entry) {
+ var text = '';
+
+ if (entry.isBegin()) {
+ text = '+' + text;
+ } else if (entry.isEnd()) {
+ text = '-' + text;
+ } else {
+ text = '.';
+ }
+
+ text += getKeyWithValue(LogEventType, entry.orig.event.type);
+ return text;
+}
+
+// End of anonymous namespace.
+})();
+
diff --git a/chrome/browser/resources/net_internals/main.css b/chrome/browser/resources/net_internals/main.css
new file mode 100644
index 0000000..e6d6437
--- /dev/null
+++ b/chrome/browser/resources/net_internals/main.css
@@ -0,0 +1,134 @@
+/*
+Copyright (c) 2010 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.
+*/
+
+* {
+ -webkit-box-sizing: border-box;
+}
+body {
+ font-family: sans-serif;
+}
+
+#filterBox {
+ background: #efefef;
+ padding: 5px;
+ border-bottom: 1px solid gray;
+ overflow: hidden;
+}
+
+#filterBox * {
+ white-space: nowrap;
+ font-family: sans-serif;
+ font-size: 12px;
+}
+
+#filterBox input {
+ width: 100%;
+}
+
+#actionBox {
+ background: #efefef;
+ white-space: nowrap;
+ border-top: 1px solid gray;
+ overflow: hidden;
+}
+
+#requestsBox {
+ overflow-x: hidden;
+ overflow-y: auto
+}
+
+#detailsBox {
+ overflow: auto;
+}
+
+#splitterBox {
+ background: #bfbfbf;
+ border-left: 1px inset black;
+ border-right: 1px solid black;
+ position:absolute;
+ width: 8px;
+ cursor: col-resize;
+ user-select: none;
+}
+
+#requestsListTable {
+ cursor: pointer;
+}
+
+#requestsListTable thead td {
+ text-align: left;
+ font-weight: bold;
+ background: rgb(229, 236, 249);
+}
+
+#requestsListTable td {
+ padding: 3px;
+ border-left: 1px solid #afafaf;
+ border-bottom: 1px solid #afafaf;
+ text-overflow: ellipsis;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+#requestsListTableBody .mouseover {
+ background: rgb(244,244,255);
+}
+
+#requestsListTableBody .selected,
+#requestsListTableBody .mouseover .selected {
+ background: #C3D9FF;
+}
+
+#requestsListTableBody .source_CONNECT_JOB {
+ color: blue;
+}
+
+#requestsListTableBody .source_INIT_PROXY_RESOLVER {
+ color: orange;
+}
+
+.tabSwitcher {
+ margin-top: 10px;
+ margin-left: 10px;
+}
+
+.tabSwitcher 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;
+}
+
+.tabSwitcher th.selected, .tabSwitcherLine {
+ background: rgb(195,217,255);
+}
+
+.tabSwitcherLine {
+ height: 10px;
+}
+
+#detailsTabArea {
+ margin-top: 10px;
+}
+
+.logSourceEntry {
+ margin: 5px;
+}
+
+.logSourceEntry * p {
+ font-weight: bold;
+ font-size: 12px;
+}
+
+.logSourceEntry * td {
+ font-size: 10px;
+}
diff --git a/chrome/browser/resources/net_internals/main.js b/chrome/browser/resources/net_internals/main.js
new file mode 100644
index 0000000..b23d71f
--- /dev/null
+++ b/chrome/browser/resources/net_internals/main.js
@@ -0,0 +1,90 @@
+// Copyright (c) 2010 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.
+
+/**
+ * Dictionary of constants (initialized by browser).
+ */
+var LogEntryType = null;
+var LogEventType = null;
+var LogEventPhase = null;
+var LogSourceType = null;
+
+/**
+ * Main entry point. called once the page has loaded.
+ */
+function onLoaded() {
+ // Layout the various DIVs in a vertically split fashion.
+ new LayoutManager("filterBox",
+ "requestsBox",
+ "actionBox",
+ "splitterBox",
+ "detailsBox");
+
+ // Create the view which displays information on the current selection.
+ var detailsView = new DetailsView("detailsLogTabHandle",
+ "detailsTimelineTabHandle",
+ "detailsTabArea");
+
+ // Create the view which displays requests lists, and lets you select, filter
+ // and delete them.
+ new RequestsView('requestsListTableBody',
+ 'filterInput',
+ 'filterCount',
+ 'deleteSelected',
+ 'selectAll',
+ detailsView);
+
+ // Tell the browser that we are ready to start receiving log events.
+ notifyApplicationReady();
+}
+
+//------------------------------------------------------------------------------
+// Messages sent to the browser
+//------------------------------------------------------------------------------
+
+function notifyApplicationReady() {
+ chrome.send('notifyReady');
+}
+
+//------------------------------------------------------------------------------
+// Messages received from the browser
+//------------------------------------------------------------------------------
+
+function onLogEntryAdded(logEntry) {
+ LogDataProvider.broadcast(logEntry);
+}
+
+function setLogEventTypeConstants(constantsMap) {
+ LogEventType = constantsMap;
+}
+
+function setLogEventPhaseConstants(constantsMap) {
+ LogEventPhase = constantsMap;
+}
+
+function setLogSourceTypeConstants(constantsMap) {
+ LogSourceType = constantsMap;
+}
+
+function setLogEntryTypeConstants(constantsMap) {
+ LogEntryType = constantsMap;
+}
+
+//------------------------------------------------------------------------------
+// LogDataProvider
+//------------------------------------------------------------------------------
+
+var LogDataProvider = {}
+
+LogDataProvider.observers_ = [];
+
+LogDataProvider.broadcast = function(logEntry) {
+ for (var i = 0; i < this.observers_.length; ++i) {
+ this.observers_[i].onLogEntryAdded(logEntry);
+ }
+};
+
+LogDataProvider.addObserver = function(observer) {
+ this.observers_.push(observer);
+};
diff --git a/chrome/browser/resources/net_internals/requestsview.js b/chrome/browser/resources/net_internals/requestsview.js
new file mode 100644
index 0000000..40707a4
--- /dev/null
+++ b/chrome/browser/resources/net_internals/requestsview.js
@@ -0,0 +1,165 @@
+// Copyright (c) 2010 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.
+
+/**
+ * RequestsView is the class which glues together the different components to
+ * form the primary UI:
+ *
+ * - The search filter
+ * - The requests table
+ * - The details panel
+ * - The action bar
+ *
+ * @constructor
+ */
+function RequestsView(tableBodyId, filterInputId, filterCountId,
+ deleteSelectedId, selectAllId, detailsView) {
+ this.sourceIdToEntryMap_ = {};
+ this.currentSelectedSources_ = [];
+
+ LogDataProvider.addObserver(this);
+
+ this.tableBody_ = document.getElementById(tableBodyId);
+ this.detailsView_ = detailsView;
+
+ this.filterInput_ = document.getElementById(filterInputId);
+ this.filterCount_ = document.getElementById(filterCountId);
+
+ this.filterInput_.addEventListener("search",
+ this.onFilterTextChanged_.bind(this), true);
+
+ document.getElementById(deleteSelectedId).onclick =
+ this.deleteSelected_.bind(this);
+
+ document.getElementById(selectAllId).addEventListener(
+ 'click', this.selectAll_.bind(this), true);
+
+ this.currentFilter_ = '';
+ this.numPrefilter_ = 0;
+ this.numPostfilter_ = 0;
+
+ this.invalidateFilterCounter_();
+ this.invalidateDetailsView_();
+}
+
+// How soon after updating the filter list the counter should be updated.
+RequestsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
+
+RequestsView.prototype.onFilterTextChanged_ = function() {
+ this.setFilter_(this.filterInput_.value);
+};
+
+RequestsView.prototype.setFilter_ = function(filterText) {
+ this.currentFilter_ = filterText;
+
+ // Iterate through all of the rows and see if they match the filter.
+ for (var id in this.sourceIdToEntryMap_) {
+ var entry = this.sourceIdToEntryMap_[id];
+ entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_));
+ }
+};
+
+RequestsView.prototype.onLogEntryAdded = function(logEntry) {
+ // Lookup the source.
+ var sourceEntry = this.sourceIdToEntryMap_[logEntry.source.id];
+
+ if (!sourceEntry) {
+ sourceEntry = new SourceEntry(this);
+ this.sourceIdToEntryMap_[logEntry.source.id] = sourceEntry;
+ this.incrementPrefilterCount(1);
+ }
+
+ sourceEntry.update(logEntry);
+
+ if (sourceEntry.isSelected())
+ this.invalidateDetailsView_();
+};
+
+RequestsView.prototype.incrementPrefilterCount = function(offset) {
+ this.numPrefilter_ += offset;
+ this.invalidateFilterCounter_();
+};
+
+RequestsView.prototype.incrementPostfilterCount = function(offset) {
+ this.numPostfilter_ += offset;
+ this.invalidateFilterCounter_();
+};
+
+RequestsView.prototype.onSelectionChanged = function() {
+ this.invalidateDetailsView_();
+};
+
+RequestsView.prototype.clearSelection = function() {
+ var prevSelection = this.currentSelectedSources_;
+ this.currentSelectedSources_ = [];
+
+ // Unselect everything that is currently selected.
+ for (var i = 0; i < prevSelection.length; ++i) {
+ prevSelection[i].setSelected(false);
+ }
+
+ this.onSelectionChanged();
+};
+
+RequestsView.prototype.deleteSelected_ = function() {
+ var prevSelection = this.currentSelectedSources_;
+ this.currentSelectedSources_ = [];
+
+ for (var i = 0; i < prevSelection.length; ++i) {
+ var entry = prevSelection[i];
+ entry.remove();
+ delete this.sourceIdToEntryMap_[entry.getSourceId()];
+ this.incrementPrefilterCount(-1);
+ }
+};
+
+RequestsView.prototype.selectAll_ = function(event) {
+ for (var id in this.sourceIdToEntryMap_) {
+ var entry = this.sourceIdToEntryMap_[id];
+ if (entry.isMatchedByFilter()) {
+ entry.setSelected(true);
+ }
+ }
+ event.preventDefault();
+};
+
+RequestsView.prototype.modifySelectionArray = function(
+ sourceEntry, addToSelection) {
+ // Find the index for |sourceEntry| in the current selection list.
+ var index = -1;
+ for (var i = 0; i < this.currentSelectedSources_.length; ++i) {
+ if (this.currentSelectedSources_[i] == sourceEntry) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index != -1 && !addToSelection) {
+ // Remove from the selection.
+ this.currentSelectedSources_.splice(index, 1);
+ }
+
+ if (index == -1 && addToSelection) {
+ this.currentSelectedSources_.push(sourceEntry);
+ }
+}
+
+RequestsView.prototype.invalidateDetailsView_ = function() {
+ this.detailsView_.setData(this.currentSelectedSources_);
+};
+
+RequestsView.prototype.invalidateFilterCounter_ = function() {
+ if (!this.outstandingRepaintFilterCounter_) {
+ this.outstandingRepaintFilterCounter_ = true;
+ window.setTimeout(this.repaintFilterCounter_.bind(this),
+ RequestsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS);
+ }
+};
+
+RequestsView.prototype.repaintFilterCounter_ = function() {
+ this.outstandingRepaintFilterCounter_ = false;
+ this.filterCount_.innerHTML = '';
+ addTextNode(this.filterCount_,
+ this.numPostfilter_ + " of " + this.numPrefilter_);
+};
diff --git a/chrome/browser/resources/net_internals/sourceentry.js b/chrome/browser/resources/net_internals/sourceentry.js
new file mode 100644
index 0000000..72d3a44
--- /dev/null
+++ b/chrome/browser/resources/net_internals/sourceentry.js
@@ -0,0 +1,182 @@
+// Copyright (c) 2010 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.
+
+/**
+ * Each row in the filtered items list is backed by a SourceEntry. This
+ * instance contains all of the data pertaining to that row, and notifies
+ * its parent view (the RequestsView) whenever its data changes.
+ *
+ * @constructor
+ */
+function SourceEntry(parentView) {
+ this.entries_ = [];
+ this.parentView_ = parentView;
+ this.isSelected_ = false;
+ this.isMatchedByFilter_ = false;
+}
+
+SourceEntry.prototype.isSelected = function() {
+ return this.isSelected_;
+};
+
+SourceEntry.prototype.setSelectedStyles = function(isSelected) {
+ changeClassName(this.row_, 'selected', isSelected);
+ this.getSelectionCheckbox().checked = isSelected;
+};
+
+SourceEntry.prototype.setMouseoverStyle = function(isMouseOver) {
+ changeClassName(this.row_, 'mouseover', isMouseOver);
+};
+
+SourceEntry.prototype.setIsMatchedByFilter = function(isMatchedByFilter) {
+ if (this.isMatchedByFilter() == isMatchedByFilter)
+ return; // No change.
+
+ this.isMatchedByFilter_ = isMatchedByFilter;
+
+ this.setFilterStyles(isMatchedByFilter);
+
+ if (isMatchedByFilter) {
+ this.parentView_.incrementPostfilterCount(1);
+ } else {
+ this.parentView_.incrementPostfilterCount(-1);
+ // If we are filtering an entry away, make sure it is no longer
+ // part of the selection.
+ this.setSelected(false);
+ }
+};
+
+SourceEntry.prototype.isMatchedByFilter = function() {
+ return this.isMatchedByFilter_;
+};
+
+SourceEntry.prototype.setFilterStyles = function(isMatchedByFilter) {
+ // Hide rows which have been filtered away.
+ if (isMatchedByFilter) {
+ this.row_.style.display = '';
+ } else {
+ this.row_.style.display = 'none';
+ }
+};
+
+SourceEntry.prototype.update = function(logEntry) {
+ this.entries_.push(logEntry);
+
+ if (this.entries_.length == 1) {
+ this.createRow_();
+
+ // Only apply the filter during the first update.
+ // TODO(eroman): once filters use other data, apply it on each update.
+ var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_);
+ this.setIsMatchedByFilter(matchesFilter);
+ }
+};
+
+SourceEntry.prototype.onCheckboxToggled_ = function() {
+ this.setSelected(this.getSelectionCheckbox().checked);
+};
+
+SourceEntry.prototype.matchesFilter = function(filterText) {
+ // TODO(eroman): Support more advanced filter syntax.
+ if (filterText == '')
+ return true;
+
+ var filterText = filterText.toLowerCase();
+
+ return this.getDescription().toLowerCase().indexOf(filterText) != -1 ||
+ this.getSourceTypeString().toLowerCase().indexOf(filterText) != -1;
+};
+
+SourceEntry.prototype.setSelected = function(isSelected) {
+ if (isSelected == this.isSelected())
+ return;
+
+ this.isSelected_ = isSelected;
+
+ this.setSelectedStyles(isSelected);
+ this.parentView_.modifySelectionArray(this, isSelected);
+ this.parentView_.onSelectionChanged();
+};
+
+SourceEntry.prototype.onClicked_ = function() {
+ this.parentView_.clearSelection();
+ this.setSelected(true);
+};
+
+SourceEntry.prototype.onMouseover_ = function() {
+ this.setMouseoverStyle(true);
+};
+
+SourceEntry.prototype.onMouseout_ = function() {
+ this.setMouseoverStyle(false);
+};
+
+SourceEntry.prototype.createRow_ = function() {
+ // Create a row.
+ var tr = addNode(this.parentView_.tableBody_, 'tr');
+ tr.style.display = 'none';
+ this.row_ = tr;
+
+ var selectionCol = addNode(tr, 'td');
+ var checkbox = addNode(selectionCol, 'input');
+ checkbox.type = 'checkbox';
+
+ var idCell = addNode(tr, 'td');
+ var typeCell = addNode(tr, 'td');
+ var descriptionCell = addNode(tr, 'td');
+
+ // Connect listeners.
+ checkbox.onchange = this.onCheckboxToggled_.bind(this);
+
+ var onclick = this.onClicked_.bind(this);
+ idCell.onclick = onclick;
+ typeCell.onclick = onclick;
+ descriptionCell.onclick = onclick;
+
+ tr.onmouseover = this.onMouseover_.bind(this);
+ tr.onmouseout = this.onMouseout_.bind(this);
+
+ // Set the cell values to match this source's data.
+ addTextNode(idCell, this.getSourceId());
+ var sourceTypeString = this.getSourceTypeString();
+ addTextNode(typeCell, sourceTypeString);
+ addTextNode(descriptionCell, this.getDescription());
+
+ // Add a CSS classname specific to this source type (so CSS can specify
+ // different stylings for different types).
+ changeClassName(this.row_, "source_" + sourceTypeString, true);
+};
+
+SourceEntry.prototype.getDescription = function() {
+ var e = this.entries_[0];
+ if (e.type == LogEntryType.TYPE_EVENT &&
+ e.event.phase == LogEventPhase.PHASE_BEGIN &&
+ e.string != undefined) {
+ return e.string; // The URL / hostname / whatever.
+ }
+ return '';
+};
+
+SourceEntry.prototype.getLogEntries = function() {
+ return this.entries_;
+};
+
+SourceEntry.prototype.getSourceTypeString = function() {
+ return getKeyWithValue(LogSourceType, this.entries_[0].source.type);
+};
+
+SourceEntry.prototype.getSelectionCheckbox = function() {
+ return this.row_.childNodes[0].firstChild;
+};
+
+SourceEntry.prototype.getSourceId = function() {
+ return this.entries_[0].source.id;
+};
+
+SourceEntry.prototype.remove = function() {
+ this.setSelected(false);
+ this.setIsMatchedByFilter(false);
+ this.row_.parentNode.removeChild(this.row_);
+};
+
diff --git a/chrome/browser/resources/net_internals/timelineviewpainter.js b/chrome/browser/resources/net_internals/timelineviewpainter.js
new file mode 100644
index 0000000..2c7fb046
--- /dev/null
+++ b/chrome/browser/resources/net_internals/timelineviewpainter.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2010 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() {
+
+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');
+
+}
+
+})();
diff --git a/chrome/browser/resources/net_internals/util.js b/chrome/browser/resources/net_internals/util.js
new file mode 100644
index 0000000..780a030
--- /dev/null
+++ b/chrome/browser/resources/net_internals/util.js
@@ -0,0 +1,88 @@
+// Copyright (c) 2010 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.
+
+/**
+ * Helper that binds the |this| object to a method to create a callback.
+ */
+Function.prototype.bind = function(thisObj) {
+ var func = this;
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ return func.apply(thisObj,
+ args.concat(Array.prototype.slice.call(arguments, 0)))
+ };
+};
+
+/**
+ * Sets the width (in pixels) on a DOM node.
+ */
+function setNodeWidth(node, widthPx) {
+ node.style.width = widthPx.toFixed(0) + "px";
+}
+
+/**
+ * Sets the height (in pixels) on a DOM node.
+ */
+function setNodeHeight(node, heightPx) {
+ node.style.height = heightPx.toFixed(0) + "px";
+}
+
+/**
+ * Sets the position and size of a DOM node (in pixels).
+ */
+function setNodePosition(node, leftPx, topPx, widthPx, heightPx) {
+ node.style.left = leftPx.toFixed(0) + "px";
+ node.style.top = topPx.toFixed(0) + "px";
+ setNodeWidth(node, widthPx);
+ setNodeHeight(node, heightPx);
+}
+
+/**
+ * Adds a node to |parentNode|, of type |tagName|.
+ */
+function addNode(parentNode, tagName) {
+ var elem = parentNode.ownerDocument.createElement(tagName);
+ parentNode.appendChild(elem);
+ return elem;
+}
+
+/**
+ * Adds text to node |parentNode|.
+ */
+function addTextNode(parentNode, text) {
+ var textNode = parentNode.ownerDocument.createTextNode(text);
+ parentNode.appendChild(textNode);
+ return textNode;
+}
+
+/**
+ * Adds or removes a CSS class to |node|.
+ */
+function changeClassName(node, classNameToAddOrRemove, isAdd) {
+ // Multiple classes can be separated by spaces.
+ var currentNames = node.className.split(" ");
+
+ if (isAdd) {
+ if (!(classNameToAddOrRemove in currentNames)) {
+ currentNames.push(classNameToAddOrRemove);
+ }
+ } else {
+ for (var i = 0; i < currentNames.length; ++i) {
+ if (currentNames[i] == classNameToAddOrRemove) {
+ currentNames.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ node.className = currentNames.join(" ");
+}
+
+function getKeyWithValue(map, value) {
+ for (key in map) {
+ if (map[key] == value)
+ return key;
+ }
+ return '?';
+}