summaryrefslogtreecommitdiffstats
path: root/content
diff options
context:
space:
mode:
authorjiayl@chromium.org <jiayl@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-23 22:11:48 +0000
committerjiayl@chromium.org <jiayl@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-23 22:11:48 +0000
commit03bf84adf8081592e527fdd781391c1ad9765a98 (patch)
treeebd76832dc1e58adbb92ee3d1212f4e5a25543fa /content
parentd82b38b7f6f7c98aea83c3b194bd6fa5f58f522e (diff)
downloadchromium_src-03bf84adf8081592e527fdd781391c1ad9765a98.zip
chromium_src-03bf84adf8081592e527fdd781391c1ad9765a98.tar.gz
chromium_src-03bf84adf8081592e527fdd781391c1ad9765a98.tar.bz2
Add graphs for PeerConnection stats
BUG=188643 Review URL: https://chromiumcodereview.appspot.com/12619006 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@190095 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content')
-rw-r--r--content/browser/media/webrtc_internals_browsertest.cc438
-rw-r--r--content/browser/resources/media/data_series.js109
-rw-r--r--content/browser/resources/media/stats_graph_helper.js220
-rw-r--r--content/browser/resources/media/timeline_graph_view.js556
-rw-r--r--content/browser/resources/media/webrtc_internals.css32
-rw-r--r--content/browser/resources/media/webrtc_internals.js17
-rw-r--r--content/content_tests.gypi1
7 files changed, 1366 insertions, 7 deletions
diff --git a/content/browser/media/webrtc_internals_browsertest.cc b/content/browser/media/webrtc_internals_browsertest.cc
new file mode 100644
index 0000000..a497c3d
--- /dev/null
+++ b/content/browser/media/webrtc_internals_browsertest.cc
@@ -0,0 +1,438 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/time.h"
+#include "base/utf_string_conversions.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/shell/shell.h"
+#include "content/test/content_browser_test.h"
+#include "content/test/content_browser_test_utils.h"
+
+using std::string;
+namespace content {
+
+struct EventEntry {
+ string type;
+ string value;
+};
+
+struct StatsUnit {
+ string GetString() const {
+ std::stringstream ss;
+ ss << "{timestamp:" << timestamp << ", values:[";
+ std::map<string, string>::const_iterator iter;
+ for (iter = values.begin(); iter != values.end(); iter++) {
+ ss << "'" << iter->first << "','" << iter->second << "',";
+ }
+ ss << "]}";
+ return ss.str();
+ }
+
+ int64 timestamp;
+ std::map<string, string> values;
+};
+
+struct StatsEntry {
+ string type;
+ string id;
+ StatsUnit local;
+ StatsUnit remote;
+};
+
+typedef std::map<string, std::vector<string> > StatsMap;
+
+class PeerConnectionEntry {
+ public:
+ PeerConnectionEntry(int pid, int lid) : pid_(pid), lid_(lid) {}
+
+ void AddEvent(const string& type, const string& value) {
+ EventEntry entry = {type, value};
+ events_.push_back(entry);
+ }
+
+ string getIdString() const {
+ std::stringstream ss;
+ ss << pid_ << "-" << lid_;
+ return ss.str();
+ }
+
+ string getLogIdString() const {
+ std::stringstream ss;
+ ss << pid_ << "-" << lid_ << "-log";
+ return ss.str();
+ }
+
+ string getAllUpdateString() const {
+ std::stringstream ss;
+ ss << "{pid:" << pid_ << ", lid:" << lid_ << ", log:[";
+ for (size_t i = 0; i < events_.size(); ++i) {
+ ss << "{type:'" << events_[i].type <<
+ "', value:'" << events_[i].value << "'},";
+ }
+ ss << "]}";
+ return ss.str();
+ }
+
+ int pid_;
+ int lid_;
+ std::vector<EventEntry> events_;
+ // This is a record of the history of stats value reported for each stats
+ // report id (e.g. ssrc-1234) for each stats name (e.g. framerate).
+ // It a 2-D map with each map entry is a vector of reported values.
+ // It is used to verify the graph data series.
+ std::map<string, StatsMap> stats_;
+};
+
+static const int64 FAKE_TIME_STAMP = 0;
+
+class WebRTCInternalsBrowserTest: public ContentBrowserTest {
+ public:
+ WebRTCInternalsBrowserTest() {}
+ virtual ~WebRTCInternalsBrowserTest() {}
+
+ protected:
+ bool ExecuteJavascript(const string& javascript) {
+ return ExecuteScript(shell()->web_contents(), javascript);
+ }
+
+ // Execute the javascript of addPeerConnection.
+ void ExecuteAddPeerConnectionJs(const PeerConnectionEntry& pc) {
+ std::stringstream ss;
+ ss << "{pid:" << pc.pid_ <<", lid:" << pc.lid_ << ", " <<
+ "url:'u', servers:'s', constraints:'c'}";
+ ASSERT_TRUE(ExecuteJavascript("addPeerConnection(" + ss.str() + ");"));
+ }
+
+ // Execute the javascript of removePeerConnection.
+ void ExecuteRemovePeerConnectionJs(const PeerConnectionEntry& pc) {
+ std::stringstream ss;
+ ss << "{pid:" << pc.pid_ <<", lid:" << pc.lid_ << "}";
+
+ ASSERT_TRUE(ExecuteJavascript("removePeerConnection(" + ss.str() + ");"));
+ }
+
+ // Verifies that the DOM element with id |id| exists.
+ void VerifyElementWithId(const string& id) {
+ bool result = false;
+ ASSERT_TRUE(ExecuteScriptAndExtractBool(
+ shell()->web_contents(),
+ "window.domAutomationController.send($('" + id + "') != null);",
+ &result));
+ EXPECT_TRUE(result);
+ }
+
+ // Verifies that the DOM element with id |id| does not exist.
+ void VerifyNoElementWithId(const string& id) {
+ bool result = false;
+ ASSERT_TRUE(ExecuteScriptAndExtractBool(
+ shell()->web_contents(),
+ "window.domAutomationController.send($('" + id + "') == null);",
+ &result));
+ EXPECT_TRUE(result);
+ }
+
+ // Verifies that DOM for |pc| is correctly created with the right content.
+ void VerifyPeerConnectionEntry(const PeerConnectionEntry& pc) {
+ VerifyElementWithId(pc.getIdString());
+ if (pc.events_.size() == 0)
+ return;
+
+ string log_id = pc.getLogIdString();
+ VerifyElementWithId(log_id);
+ string result;
+ for (size_t i = 0; i < pc.events_.size(); ++i) {
+ std::stringstream ss;
+ ss << "var row = $('" << log_id << "').rows[" << (i + 1) << "];"
+ "var cell = row.lastChild;"
+ "var type = cell.firstChild.textContent;"
+ "var value = cell.lastChild.lastChild.value;"
+ "window.domAutomationController.send(type + ':' + value);";
+ ASSERT_TRUE(ExecuteScriptAndExtractString(
+ shell()->web_contents(), ss.str(), &result));
+ EXPECT_EQ(pc.events_[i].type + ":" + pc.events_[i].value, result);
+ }
+ }
+
+ // Executes the javascript of updatePeerConnection and verifies the result.
+ void ExecuteAndVerifyUpdatePeerConnection(
+ PeerConnectionEntry& pc, const string& type, const string& value) {
+ pc.AddEvent(type, value);
+
+ std::stringstream ss;
+ ss << "{pid:" << pc.pid_ <<", lid:" << pc.lid_ <<
+ ", type:'" << type << "', value:'" << value << "'}";
+ ASSERT_TRUE(ExecuteJavascript("updatePeerConnection(" + ss.str() + ")"));
+
+ VerifyPeerConnectionEntry(pc);
+ }
+
+ // Execute addStats and verifies that the stats table has the right content.
+ void ExecuteAndVerifyAddStats(
+ PeerConnectionEntry& pc, const string& type, const string& id,
+ StatsUnit& local, StatsUnit& remote) {
+ StatsEntry entry = {type, id, local, remote};
+
+ // Adds each new value to the map of stats history.
+ std::map<string, string>::iterator iter;
+ for (iter = local.values.begin(); iter != local.values.end(); iter++) {
+ pc.stats_[type + "-" + id][iter->first].push_back(iter->second);
+ }
+ for (iter = remote.values.begin(); iter != remote.values.end(); iter++) {
+ pc.stats_[type + "-" + id][iter->first].push_back(iter->second);
+ }
+
+ std::stringstream ss;
+ ss << "{pid:" << pc.pid_ << ", lid:" << pc.lid_ << ","
+ "reports:[" << "{id:'" << id << "', type:'" << type << "', "
+ "local:" << local.GetString() << ", "
+ "remote:" << remote.GetString() << "}]}";
+
+ ASSERT_TRUE(ExecuteJavascript("addStats(" + ss.str() + ")"));
+ VerifyStatsTable(pc, entry);
+ }
+
+
+ // Verifies that the stats table has the right content.
+ void VerifyStatsTable(const PeerConnectionEntry& pc,
+ const StatsEntry& report) {
+ string table_id =
+ pc.getIdString() + "-table-" + report.type + "-" + report.id;
+ VerifyElementWithId(table_id);
+
+ std::map<string, string>::const_iterator iter;
+ for (iter = report.local.values.begin();
+ iter != report.local.values.end(); iter++) {
+ VerifyStatsTableRow(table_id, iter->first, iter->second);
+ }
+ for (iter = report.remote.values.begin();
+ iter != report.remote.values.end(); iter++) {
+ VerifyStatsTableRow(table_id, iter->first, iter->second);
+ }
+ }
+
+ // Verifies that the row named as |name| of the stats table |table_id| has
+ // the correct content as |name| : |value|.
+ void VerifyStatsTableRow(const string& table_id,
+ const string& name,
+ const string& value) {
+ VerifyElementWithId(table_id + "-" + name);
+
+ string result;
+ ASSERT_TRUE(ExecuteScriptAndExtractString(
+ shell()->web_contents(),
+ "var row = $('" + table_id + "-" + name + "');"
+ "var name = row.cells[0].textContent;"
+ "var value = row.cells[1].textContent;"
+ "window.domAutomationController.send(name + ':' + value)",
+ &result));
+ EXPECT_EQ(name + ":" + value, result);
+ }
+
+ // Verifies that the graph data series consistent with pc.stats_.
+ void VerifyStatsGraph(const PeerConnectionEntry& pc) {
+ std::map<string, StatsMap>::const_iterator stream_iter;
+ for (stream_iter = pc.stats_.begin();
+ stream_iter != pc.stats_.end(); stream_iter++) {
+ StatsMap::const_iterator stats_iter;
+ for (stats_iter = stream_iter->second.begin();
+ stats_iter != stream_iter->second.end();
+ stats_iter++) {
+ for (size_t i = 0; i < stats_iter->second.size(); ++i) {
+ string graph_id = pc.getIdString() + "-" +
+ stream_iter->first + "-" + stats_iter->first;
+ VerifyGraphDataPoint(graph_id, i, stats_iter->second[i]);
+ }
+ }
+ }
+ }
+
+ // Verifies that the graph data point at index |index| has value |value|.
+ void VerifyGraphDataPoint(
+ const string& graph_id, int index, const string& value) {
+ bool result = false;
+ ASSERT_TRUE(ExecuteScriptAndExtractBool(
+ shell()->web_contents(),
+ "window.domAutomationController.send("
+ " graphViews['" + graph_id + "'] != null)",
+ &result));
+ EXPECT_TRUE(result);
+
+ std::stringstream ss;
+ ss << "var dp = dataSeries['" << graph_id << "']"
+ ".dataPoints_[" << index << "];"
+ "window.domAutomationController.send(dp.value.toString())";
+ string actual_value;
+ ASSERT_TRUE(ExecuteScriptAndExtractString(
+ shell()->web_contents(), ss.str(), &actual_value));
+ EXPECT_EQ(value, actual_value);
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, AddAndRemovePeerConnection) {
+ GURL url("chrome://webrtc-internals");
+ NavigateToURL(shell(), url);
+
+ // Add two PeerConnections and then remove them.
+ PeerConnectionEntry pc_1(1, 0);
+ ExecuteAddPeerConnectionJs(pc_1);
+ VerifyPeerConnectionEntry(pc_1);
+
+ PeerConnectionEntry pc_2(2, 1);
+ ExecuteAddPeerConnectionJs(pc_2);
+ VerifyPeerConnectionEntry(pc_2);
+
+ ExecuteRemovePeerConnectionJs(pc_1);
+ VerifyNoElementWithId(pc_1.getIdString());
+ VerifyPeerConnectionEntry(pc_2);
+
+ ExecuteRemovePeerConnectionJs(pc_2);
+ VerifyNoElementWithId(pc_2.getIdString());
+}
+
+IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, UpdateAllPeerConnections) {
+ GURL url("chrome://webrtc-internals");
+ NavigateToURL(shell(), url);
+
+ PeerConnectionEntry pc_0(1, 0);
+ pc_0.AddEvent("e1", "v1");
+ pc_0.AddEvent("e2", "v2");
+ PeerConnectionEntry pc_1(1, 1);
+ pc_1.AddEvent("e3", "v3");
+ pc_1.AddEvent("e4", "v4");
+ string pc_array = "[" + pc_0.getAllUpdateString() + ", " +
+ pc_1.getAllUpdateString() + "]";
+ EXPECT_TRUE(ExecuteJavascript("updateAllPeerConnections(" + pc_array + ");"));
+ VerifyPeerConnectionEntry(pc_0);
+ VerifyPeerConnectionEntry(pc_1);
+}
+
+IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, UpdatePeerConnection) {
+ GURL url("chrome://webrtc-internals");
+ NavigateToURL(shell(), url);
+
+ // Add one PeerConnection and send one update.
+ PeerConnectionEntry pc_1(1, 0);
+ ExecuteAddPeerConnectionJs(pc_1);
+
+ ExecuteAndVerifyUpdatePeerConnection(pc_1, "e1", "v1");
+
+ // Add another PeerConnection and send two updates.
+ PeerConnectionEntry pc_2(1, 1);
+ ExecuteAddPeerConnectionJs(pc_2);
+
+ ExecuteAndVerifyUpdatePeerConnection(pc_2, "e2", "v2");
+ ExecuteAndVerifyUpdatePeerConnection(pc_2, "e3", "v3");
+}
+
+// Tests that adding random named stats updates the dataSeries and graphs.
+IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, AddStats) {
+ GURL url("chrome://webrtc-internals");
+ NavigateToURL(shell(), url);
+
+ PeerConnectionEntry pc(1, 0);
+ ExecuteAddPeerConnectionJs(pc);
+
+ const string type = "ssrc";
+ const string id = "1234";
+ StatsUnit local = {FAKE_TIME_STAMP};
+ local.values["bitrate"] = "2000";
+ local.values["framerate"] = "30";
+ StatsUnit remote = {FAKE_TIME_STAMP};
+ remote.values["jitter"] = "1";
+ remote.values["rtt"] = "20";
+
+ // Add new stats and verify the stats table and graphs.
+ ExecuteAndVerifyAddStats(pc, type, id, local, remote);
+ VerifyStatsGraph(pc);
+
+ // Update existing stats and verify the stats table and graphs.
+ local.values["bitrate"] = "2001";
+ local.values["framerate"] = "31";
+ ExecuteAndVerifyAddStats(pc, type, id, local, remote);
+ VerifyStatsGraph(pc);
+}
+
+// Tests that the bandwidth estimation values are drawn on a single graph.
+IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, BweCompoundGraph) {
+ GURL url("chrome://webrtc-internals");
+ NavigateToURL(shell(), url);
+
+ PeerConnectionEntry pc(1, 0);
+ ExecuteAddPeerConnectionJs(pc);
+
+ StatsUnit local = {FAKE_TIME_STAMP};
+ local.values["googAvailableSendBandwidth"] = "1000000";
+ local.values["googTargetEncBitrate"] = "1000";
+ local.values["googActualEncBitrate"] = "1000000";
+ local.values["googRetransmitBitrate"] = "10";
+ local.values["googTransmitBitrate"] = "1000000";
+ const string stats_type = "bwe";
+ const string stats_id = "videobwe";
+ ExecuteAndVerifyAddStats(pc, stats_type, stats_id, local, local);
+
+ string graph_id =
+ pc.getIdString() + "-" + stats_type + "-" + stats_id + "-bweCompound";
+ bool result = false;
+ // Verify that the bweCompound graph exists.
+ ASSERT_TRUE(ExecuteScriptAndExtractBool(
+ shell()->web_contents(),
+ "window.domAutomationController.send("
+ " graphViews['" + graph_id + "'] != null)",
+ &result));
+ EXPECT_TRUE(result);
+
+ // Verify that the bweCompound graph contains multiple dataSeries.
+ int count = 0;
+ ASSERT_TRUE(ExecuteScriptAndExtractInt(
+ shell()->web_contents(),
+ "window.domAutomationController.send("
+ " graphViews['" + graph_id + "'].getDataSeriesCount())",
+ &count));
+ EXPECT_EQ((int)local.values.size(), count);
+}
+
+// Tests that the total packet/byte count is converted to count per second,
+// and the converted data is drawn.
+IN_PROC_BROWSER_TEST_F(WebRTCInternalsBrowserTest, ConvertedGraphs) {
+ GURL url("chrome://webrtc-internals");
+ NavigateToURL(shell(), url);
+
+ PeerConnectionEntry pc(1, 0);
+ ExecuteAddPeerConnectionJs(pc);
+
+ const string stats_type = "s";
+ const string stats_id = "1";
+ const int num_converted_stats = 4;
+ const string stats_names[] =
+ {"packetsSent", "bytesSent", "packetsReceived", "bytesReceived"};
+ const string converted_names[] =
+ {"packetsSentPerSecond", "bitsSentPerSecond",
+ "packetsReceivedPerSecond", "bitsReceivedPerSecond"};
+ const string first_value = "1000";
+ const string second_value = "2000";
+ const string converted_values[] = {"1000", "8000", "1000", "8000"};
+
+ // Send the first data point.
+ StatsUnit remote = {FAKE_TIME_STAMP};
+ StatsUnit local = {FAKE_TIME_STAMP};
+ for (int i = 0; i < num_converted_stats; ++i)
+ local.values[stats_names[i]] = first_value;
+
+ ExecuteAndVerifyAddStats(pc, stats_type, stats_id, local, remote);
+
+ // Send the second data point at 1000ms after the first data point.
+ local.timestamp += 1000;
+ for (int i = 0; i < num_converted_stats; ++i)
+ local.values[stats_names[i]] = second_value;
+ ExecuteAndVerifyAddStats(pc, stats_type, stats_id, local, remote);
+
+ // Verifies the graph data matches converted_values.
+ string graph_id_prefix = pc.getIdString() + "-" + stats_type + "-" + stats_id;
+ for (int i = 0; i < num_converted_stats; ++i) {
+ VerifyGraphDataPoint(
+ graph_id_prefix + "-" + converted_names[i], 1, converted_values[i]);
+ }
+}
+
+} // namespace content
diff --git a/content/browser/resources/media/data_series.js b/content/browser/resources/media/data_series.js
new file mode 100644
index 0000000..f32c822
--- /dev/null
+++ b/content/browser/resources/media/data_series.js
@@ -0,0 +1,109 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * A TimelineDataSeries collects an ordered series of (time, value) pairs,
+ * and converts them to graph points. It also keeps track of its color and
+ * current visibility state.
+ */
+var TimelineDataSeries = (function() {
+ 'use strict';
+
+ /**
+ * @constructor
+ */
+ function TimelineDataSeries() {
+ // List of DataPoints in chronological order.
+ this.dataPoints_ = [];
+
+ // Default color. Should always be overridden prior to display.
+ this.color_ = 'red';
+ // Whether or not the data series should be drawn.
+ this.isVisible_ = true;
+
+ this.cacheStartTime_ = null;
+ this.cacheStepSize_ = 0;
+ this.cacheValues_ = [];
+ }
+
+ TimelineDataSeries.prototype = {
+ /**
+ * Adds a DataPoint to |this| with the specified time and value.
+ * DataPoints are assumed to be received in chronological order.
+ */
+ addPoint: function(timeTicks, value) {
+ var time = new Date(timeTicks);
+ this.dataPoints_.push(new DataPoint(time, value));
+ },
+
+ isVisible: function() {
+ return this.isVisible_;
+ },
+
+ show: function(isVisible) {
+ this.isVisible_ = isVisible;
+ },
+
+ getColor: function() {
+ return this.color_;
+ },
+
+ setColor: function(color) {
+ this.color_ = color;
+ },
+
+ /**
+ * Returns a list containing the values of the data series at |count|
+ * points, starting at |startTime|, and |stepSize| milliseconds apart.
+ * Caches values, so showing/hiding individual data series is fast.
+ */
+ getValues: function(startTime, stepSize, count) {
+ // Use cached values, if we can.
+ if (this.cacheStartTime_ == startTime &&
+ this.cacheStepSize_ == stepSize &&
+ this.cacheValues_.length == count) {
+ return this.cacheValues_;
+ }
+
+ // Do all the work.
+ this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count);
+ this.cacheStartTime_ = startTime;
+ this.cacheStepSize_ = stepSize;
+
+ return this.cacheValues_;
+ },
+
+ /**
+ * Returns the cached |values| in the specified time period.
+ */
+ getValuesInternal_: function(startTime, stepSize, count) {
+ var values = [];
+ var nextPoint = 0;
+ var currentValue = 0;
+ var time = startTime;
+ for (var i = 0; i < count; ++i) {
+ while (nextPoint < this.dataPoints_.length &&
+ this.dataPoints_[nextPoint].time < time) {
+ currentValue = this.dataPoints_[nextPoint].value;
+ ++nextPoint;
+ }
+ values[i] = currentValue;
+ time += stepSize;
+ }
+ return values;
+ }
+ };
+
+ /**
+ * A single point in a data series. Each point has a time, in the form of
+ * milliseconds since the Unix epoch, and a numeric value.
+ * @constructor
+ */
+ function DataPoint(time, value) {
+ this.time = time;
+ this.value = value;
+ }
+
+ return TimelineDataSeries;
+})();
diff --git a/content/browser/resources/media/stats_graph_helper.js b/content/browser/resources/media/stats_graph_helper.js
new file mode 100644
index 0000000..c56ab72
--- /dev/null
+++ b/content/browser/resources/media/stats_graph_helper.js
@@ -0,0 +1,220 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//
+// This file contains helper methods to draw the stats timeline graphs.
+// Each graph represents a series of stats report for a PeerConnection,
+// e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent
+// for ssrc-abcd123 of PeerConnection 0 in process 1234.
+// The graphs are drawn as CANVAS, grouped per report type per PeerConnection.
+// Each group has an expand/collapse button and is collapsed initially.
+//
+
+<include src="data_series.js"/>
+<include src="timeline_graph_view.js"/>
+
+// Specifies which stats should be drawn on the 'bweCompound' graph and how.
+var bweCompoundGraphConfig = {
+ googAvailableSendBandwidth: {color: 'red'},
+ googTargetEncBitrateCorrected: {color: 'purple'},
+ googActualEncBitrate: {color: 'orange'},
+ googRetransmitBitrate: {color: 'blue'},
+ googTransmitBitrate: {color: 'green'},
+};
+
+// Converts the last entry of |srcDataSeries| from the total amount to the
+// amount per second.
+var totalToPerSecond = function(srcDataSeries) {
+ var length = srcDataSeries.dataPoints_.length;
+ if (length >= 2) {
+ var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
+ var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2];
+ return (lastDataPoint.value - secondLastDataPoint.value) * 1000 /
+ (lastDataPoint.time - secondLastDataPoint.time);
+ }
+
+ return 0;
+};
+
+// Converts the value of total bytes to bits per second.
+var totalBytesToBitsPerSecond = function(srcDataSeries) {
+ return totalToPerSecond(srcDataSeries) * 8;
+};
+
+// Specifies which stats should be converted before drawn and how.
+// |convertedName| is the name of the converted value, |convertFunction|
+// is the function used to calculate the new converted value based on the
+// original dataSeries.
+var dataConversionConfig = {
+ packetsSent: {
+ convertedName: 'packetsSentPerSecond',
+ convertFunction: totalToPerSecond,
+ },
+ bytesSent: {
+ convertedName: 'bitsSentPerSecond',
+ convertFunction: totalBytesToBitsPerSecond,
+ },
+ packetsReceived: {
+ convertedName: 'packetsReceivedPerSecond',
+ convertFunction: totalToPerSecond,
+ },
+ bytesReceived: {
+ convertedName: 'bitsReceivedPerSecond',
+ convertFunction: totalBytesToBitsPerSecond,
+ },
+ // This is due to a bug of wrong units reported for googTargetEncBitrate.
+ // TODO (jiayl): remove this when the unit bug is fixed.
+ googTargetEncBitrate: {
+ convertedName: 'googTargetEncBitrateCorrected',
+ convertFunction: function (srcDataSeries) {
+ var length = srcDataSeries.dataPoints_.length;
+ var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
+ if (lastDataPoint.value < 5000)
+ return lastDataPoint.value * 1000;
+ return lastDataPoint.value;
+ }
+ }
+};
+
+var graphViews = {};
+var dataSeries = {};
+
+// Adds the stats report |singleReport| to the timeline graph for the given
+// |peerConnectionElement| and |reportName|.
+function drawSingleReport(peerConnectionElement, reportName, singleReport) {
+ if (!singleReport || !singleReport.values)
+ return;
+ for (var i = 0; i < singleReport.values.length - 1; i = i + 2) {
+ var rawLabel = singleReport.values[i];
+ var rawValue = parseInt(singleReport.values[i + 1]);
+ var rawDataSeriesId =
+ peerConnectionElement.id + '-' + reportName + '-' + rawLabel;
+
+ var finalDataSeriesId = rawDataSeriesId;
+ var finalLabel = rawLabel;
+ var finalValue = rawValue;
+ // We need to convert the value if dataConversionConfig[rawLabel] exists.
+ if (dataConversionConfig[rawLabel]) {
+ // Updates the original dataSeries before the conversion.
+ addDataSeriesPoint(rawDataSeriesId, singleReport.timestamp,
+ rawLabel, rawValue);
+
+ // Convert to another value to draw on graph, using the original
+ // dataSeries as input.
+ finalValue = dataConversionConfig[rawLabel].convertFunction(
+ dataSeries[rawDataSeriesId]);
+ finalLabel = dataConversionConfig[rawLabel].convertedName;
+ finalDataSeriesId =
+ peerConnectionElement.id + '-' + reportName + '-' + finalLabel;
+ }
+
+ // Updates the final dataSeries to draw.
+ addDataSeriesPoint(
+ finalDataSeriesId, singleReport.timestamp, finalLabel, finalValue);
+
+ // Updates the graph.
+ var graphType = bweCompoundGraphConfig[finalLabel] ?
+ 'bweCompound' : finalLabel;
+ var graphViewId =
+ peerConnectionElement.id + '-' + reportName + '-' + graphType;
+
+ if (!graphViews[graphViewId]) {
+ graphViews[graphViewId] = createStatsGraphView(peerConnectionElement,
+ reportName,
+ graphType);
+ var date = new Date(singleReport.timestamp);
+ graphViews[graphViewId].setDateRange(date, date);
+ }
+ // Adds the new dataSeries to the graphView. We have to do it here to cover
+ // both the simple and compound graph cases.
+ if (!graphViews[graphViewId].hasDataSeries(dataSeries[finalDataSeriesId]))
+ graphViews[graphViewId].addDataSeries(dataSeries[finalDataSeriesId]);
+ graphViews[graphViewId].updateEndDate();
+ }
+}
+
+// Makes sure the TimelineDataSeries with id |dataSeriesId| is created,
+// and adds the new data point to it.
+function addDataSeriesPoint(dataSeriesId, time, label, value) {
+ if (!dataSeries[dataSeriesId]) {
+ dataSeries[dataSeriesId] = new TimelineDataSeries();
+ if (bweCompoundGraphConfig[label]) {
+ dataSeries[dataSeriesId].setColor(
+ bweCompoundGraphConfig[label].color);
+ }
+ }
+ dataSeries[dataSeriesId].addPoint(time, value);
+}
+
+// Ensures a div container to hold all stats graphs for a report type
+// is created as a child of |peerConnectionElement|.
+function ensureStatsGraphTopContainer(peerConnectionElement, reportName) {
+ var containerId = peerConnectionElement.id + '-' +
+ reportName + '-graph-container';
+ var container = $(containerId);
+ if (!container) {
+ container = document.createElement('div');
+ container.id = containerId;
+ container.className = 'stats-graph-container-collapsed';
+
+ peerConnectionElement.appendChild(container);
+ container.innerHTML =
+ '<button>Expand/Collapse Graphs for ' + reportName + '</button>';
+
+ // Expands or collapses the graphs on click.
+ container.childNodes[0].addEventListener('click', function(event) {
+ var element = event.target.parentNode;
+ if (element.className == 'stats-graph-container-collapsed')
+ element.className = 'stats-graph-container';
+ else
+ element.className = 'stats-graph-container-collapsed';
+ });
+ }
+ return container;
+}
+
+// Creates the container elements holding a timeline graph
+// and the TimelineGraphView object.
+function createStatsGraphView(peerConnectionElement, reportName, statsName) {
+ var topContainer = ensureStatsGraphTopContainer(peerConnectionElement,
+ reportName);
+
+ var graphViewId =
+ peerConnectionElement.id + '-' + reportName + '-' + statsName;
+ var divId = graphViewId + '-div';
+ var canvasId = graphViewId + '-canvas';
+ var container = document.createElement("div");
+ container.className = 'stats-graph-sub-container';
+
+ topContainer.appendChild(container);
+ container.innerHTML = '<div>' + statsName + '</div>' +
+ '<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>';
+ if (statsName == 'bweCompound') {
+ container.insertBefore(createBweCompoundLegend(peerConnectionElement,
+ reportName),
+ $(divId));
+ }
+ return new TimelineGraphView(divId, canvasId);
+}
+
+// Creates the legend section for the bweCompound graph.
+// Returns the legend element.
+function createBweCompoundLegend(peerConnectionElement, reportName) {
+ var legend = document.createElement('div');
+ for (var prop in bweCompoundGraphConfig) {
+ var div = document.createElement('div');
+ legend.appendChild(div);
+ div.innerHTML = '<input type=checkbox checked></input>' + prop;
+ div.style.color = bweCompoundGraphConfig[prop].color;
+ div.dataSeriesId = peerConnectionElement.id + '-' + reportName + '-' + prop;
+ div.graphViewId =
+ peerConnectionElement.id + '-' + reportName + '-bweCompound';
+ div.firstChild.addEventListener('click', function(event) {
+ var target = dataSeries[event.target.parentNode.dataSeriesId];
+ target.show(event.target.checked);
+ graphViews[event.target.parentNode.graphViewId].repaint();
+ });
+ }
+ return legend;
+}
diff --git a/content/browser/resources/media/timeline_graph_view.js b/content/browser/resources/media/timeline_graph_view.js
new file mode 100644
index 0000000..bb3557d
--- /dev/null
+++ b/content/browser/resources/media/timeline_graph_view.js
@@ -0,0 +1,556 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * A TimelineGraphView displays a timeline graph on a canvas element.
+ */
+var TimelineGraphView = (function() {
+ 'use strict';
+
+ // Default starting scale factor, in terms of milliseconds per pixel.
+ var DEFAULT_SCALE = 1000;
+
+ // Maximum number of labels placed vertically along the sides of the graph.
+ var MAX_VERTICAL_LABELS = 6;
+
+ // Vertical spacing between labels and between the graph and labels.
+ var LABEL_VERTICAL_SPACING = 4;
+ // Horizontal spacing between vertically placed labels and the edges of the
+ // graph.
+ var LABEL_HORIZONTAL_SPACING = 3;
+ // Horizintal spacing between two horitonally placed labels along the bottom
+ // of the graph.
+ var LABEL_LABEL_HORIZONTAL_SPACING = 25;
+
+ // Length of ticks, in pixels, next to y-axis labels. The x-axis only has
+ // one set of labels, so it can use lines instead.
+ var Y_AXIS_TICK_LENGTH = 10;
+
+ var GRID_COLOR = '#CCC';
+ var TEXT_COLOR = '#000';
+ var BACKGROUND_COLOR = '#FFF';
+
+ /**
+ * @constructor
+ */
+ function TimelineGraphView(divId, canvasId) {
+ this.scrollbar_ = {position_: 0, range_: 0};
+
+ this.graphDiv_ = $(divId);
+ this.canvas_ = $(canvasId);
+
+ // Set the range and scale of the graph. Times are in milliseconds since
+ // the Unix epoch.
+
+ // All measurements we have must be after this time.
+ this.startTime_ = 0;
+ // The current rightmost position of the graph is always at most this.
+ this.endTime_ = 1;
+
+ this.graph_ = null;
+
+ // Initialize the scrollbar.
+ this.updateScrollbarRange_(true);
+ }
+
+ TimelineGraphView.prototype = {
+ // Returns the total length of the graph, in pixels.
+ getLength_: function() {
+ var timeRange = this.endTime_ - this.startTime_;
+ // Math.floor is used to ignore the last partial area, of length less
+ // than DEFAULT_SCALE.
+ return Math.floor(timeRange / DEFAULT_SCALE);
+ },
+
+ /**
+ * Returns true if the graph is scrolled all the way to the right.
+ */
+ graphScrolledToRightEdge_: function() {
+ return this.scrollbar_.position_ == this.scrollbar_.range_;
+ },
+
+ /**
+ * Update the range of the scrollbar. If |resetPosition| is true, also
+ * sets the slider to point at the rightmost position and triggers a
+ * repaint.
+ */
+ updateScrollbarRange_: function(resetPosition) {
+ var scrollbarRange = this.getLength_() - this.canvas_.width;
+ if (scrollbarRange < 0)
+ scrollbarRange = 0;
+
+ // If we've decreased the range to less than the current scroll position,
+ // we need to move the scroll position.
+ if (this.scrollbar_.position_ > scrollbarRange)
+ resetPosition = true;
+
+ this.scrollbar_.range_ = scrollbarRange;
+ if (resetPosition) {
+ this.scrollbar_.position_ = scrollbarRange;
+ this.repaint();
+ }
+ },
+
+ /**
+ * Sets the date range displayed on the graph, switches to the default
+ * scale factor, and moves the scrollbar all the way to the right.
+ */
+ setDateRange: function(startDate, endDate) {
+ this.startTime_ = startDate.getTime();
+ this.endTime_ = endDate.getTime();
+
+ // Safety check.
+ if (this.endTime_ <= this.startTime_)
+ this.startTime_ = this.endTime_ - 1;
+
+ this.updateScrollbarRange_(true);
+ },
+
+ /**
+ * Updates the end time at the right of the graph to be the current time.
+ * Specifically, updates the scrollbar's range, and if the scrollbar is
+ * all the way to the right, keeps it all the way to the right. Otherwise,
+ * leaves the view as-is and doesn't redraw anything.
+ */
+ updateEndDate: function() {
+ this.endTime_ = (new Date()).getTime();
+ this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
+ },
+
+ getStartDate: function() {
+ return new Date(this.startTime_);
+ },
+
+ /**
+ * Replaces the current TimelineDataSeries with |dataSeries|.
+ */
+ setDataSeries: function(dataSeries) {
+ // Simply recreates the Graph.
+ this.graph_ = new Graph();
+ for (var i = 0; i < dataSeries.length; ++i)
+ this.graph_.addDataSeries(dataSeries[i]);
+ this.repaint();
+ },
+
+ /**
+ * Adds |dataSeries| to the current graph.
+ */
+ addDataSeries: function(dataSeries) {
+ if (!this.graph_)
+ this.graph_ = new Graph();
+ this.graph_.addDataSeries(dataSeries);
+ this.repaint();
+ },
+
+ /**
+ * Draws the graph on |canvas_|.
+ */
+ repaint: function() {
+ this.repaintTimerRunning_ = false;
+
+ var width = this.canvas_.width;
+ var height = this.canvas_.height;
+ var context = this.canvas_.getContext('2d');
+
+ // Clear the canvas.
+ context.fillStyle = BACKGROUND_COLOR;
+ context.fillRect(0, 0, width, height);
+
+ // Try to get font height in pixels. Needed for layout.
+ var fontHeightString = context.font.match(/([0-9]+)px/)[1];
+ var fontHeight = parseInt(fontHeightString);
+
+ // Safety check, to avoid drawing anything too ugly.
+ if (fontHeightString.length == 0 || fontHeight <= 0 ||
+ fontHeight * 4 > height || width < 50) {
+ return;
+ }
+
+ // Save current transformation matrix so we can restore it later.
+ context.save();
+
+ // The center of an HTML canvas pixel is technically at (0.5, 0.5). This
+ // makes near straight lines look bad, due to anti-aliasing. This
+ // translation reduces the problem a little.
+ context.translate(0.5, 0.5);
+
+ // Figure out what time values to display.
+ var position = this.scrollbar_.position_;
+ // If the entire time range is being displayed, align the right edge of
+ // the graph to the end of the time range.
+ if (this.scrollbar_.range_ == 0)
+ position = this.getLength_() - this.canvas_.width;
+ var visibleStartTime = this.startTime_ + position * DEFAULT_SCALE;
+
+ // Make space at the bottom of the graph for the time labels, and then
+ // draw the labels.
+ var textHeight = height;
+ height -= fontHeight + LABEL_VERTICAL_SPACING;
+ this.drawTimeLabels(context, width, height, textHeight, visibleStartTime);
+
+ // Draw outline of the main graph area.
+ context.strokeStyle = GRID_COLOR;
+ context.strokeRect(0, 0, width - 1, height - 1);
+
+ if (this.graph_) {
+ // Layout graph and have them draw their tick marks.
+ this.graph_.layout(
+ width, height, fontHeight, visibleStartTime, DEFAULT_SCALE);
+ this.graph_.drawTicks(context);
+
+ // Draw the lines of all graphs, and then draw their labels.
+ this.graph_.drawLines(context);
+ this.graph_.drawLabels(context);
+ }
+
+ // Restore original transformation matrix.
+ context.restore();
+ },
+
+ /**
+ * Draw time labels below the graph. Takes in start time as an argument
+ * since it may not be |startTime_|, when we're displaying the entire
+ * time range.
+ */
+ drawTimeLabels: function(context, width, height, textHeight, startTime) {
+ // Text for a time string to use in determining how far apart
+ // to place text labels.
+ var sampleText = (new Date(startTime)).toLocaleTimeString();
+
+ // The desired spacing for text labels.
+ var targetSpacing = context.measureText(sampleText).width +
+ LABEL_LABEL_HORIZONTAL_SPACING;
+
+ // The allowed time step values between adjacent labels. Anything much
+ // over a couple minutes isn't terribly realistic, given how much memory
+ // we use, and how slow a lot of the net-internals code is.
+ var timeStepValues = [
+ 1000, // 1 second
+ 1000 * 5,
+ 1000 * 30,
+ 1000 * 60, // 1 minute
+ 1000 * 60 * 5,
+ 1000 * 60 * 30,
+ 1000 * 60 * 60, // 1 hour
+ 1000 * 60 * 60 * 5
+ ];
+
+ // Find smallest time step value that gives us at least |targetSpacing|,
+ // if any.
+ var timeStep = null;
+ for (var i = 0; i < timeStepValues.length; ++i) {
+ if (timeStepValues[i] / DEFAULT_SCALE >= targetSpacing) {
+ timeStep = timeStepValues[i];
+ break;
+ }
+ }
+
+ // If no such value, give up.
+ if (!timeStep)
+ return;
+
+ // Find the time for the first label. This time is a perfect multiple of
+ // timeStep because of how UTC times work.
+ var time = Math.ceil(startTime / timeStep) * timeStep;
+
+ context.textBaseline = 'bottom';
+ context.textAlign = 'center';
+ context.fillStyle = TEXT_COLOR;
+ context.strokeStyle = GRID_COLOR;
+
+ // Draw labels and vertical grid lines.
+ while (true) {
+ var x = Math.round((time - startTime) / DEFAULT_SCALE);
+ if (x >= width)
+ break;
+ var text = (new Date(time)).toLocaleTimeString();
+ context.fillText(text, x, textHeight);
+ context.beginPath();
+ context.lineTo(x, 0);
+ context.lineTo(x, height);
+ context.stroke();
+ time += timeStep;
+ }
+ },
+
+ getDataSeriesCount: function() {
+ if (this.graph_)
+ return this.graph_.dataSeries_.length;
+ return 0;
+ },
+
+ hasDataSeries: function(dataSeries) {
+ if (this.graph_)
+ return this.graph_.hasDataSeries(dataSeries);
+ return false;
+ },
+
+ };
+
+ /**
+ * A Graph is responsible for drawing all the TimelineDataSeries that have
+ * the same data type. Graphs are responsible for scaling the values, laying
+ * out labels, and drawing both labels and lines for its data series.
+ */
+ var Graph = (function() {
+ /**
+ * @constructor
+ */
+ function Graph() {
+ this.dataSeries_ = [];
+
+ // Cached properties of the graph, set in layout.
+ this.width_ = 0;
+ this.height_ = 0;
+ this.fontHeight_ = 0;
+ this.startTime_ = 0;
+ this.scale_ = 0;
+
+ // At least the highest value in the displayed range of the graph.
+ // Used for scaling and setting labels. Set in layoutLabels.
+ this.max_ = 0;
+
+ // Cached text of equally spaced labels. Set in layoutLabels.
+ this.labels_ = [];
+ }
+
+ /**
+ * A Label is the label at a particular position along the y-axis.
+ * @constructor
+ */
+ function Label(height, text) {
+ this.height = height;
+ this.text = text;
+ }
+
+ Graph.prototype = {
+ addDataSeries: function(dataSeries) {
+ this.dataSeries_.push(dataSeries);
+ },
+
+ hasDataSeries: function(dataSeries) {
+ for (var i = 0; i < this.dataSeries_.length; ++i) {
+ if (this.dataSeries_[i] == dataSeries)
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Returns a list of all the values that should be displayed for a given
+ * data series, using the current graph layout.
+ */
+ getValues: function(dataSeries) {
+ if (!dataSeries.isVisible())
+ return null;
+ return dataSeries.getValues(this.startTime_, this.scale_, this.width_);
+ },
+
+ /**
+ * Updates the graph's layout. In particular, both the max value and
+ * label positions are updated. Must be called before calling any of the
+ * drawing functions.
+ */
+ layout: function(width, height, fontHeight, startTime, scale) {
+ this.width_ = width;
+ this.height_ = height;
+ this.fontHeight_ = fontHeight;
+ this.startTime_ = startTime;
+ this.scale_ = scale;
+
+ // Find largest value.
+ var max = 0;
+ for (var i = 0; i < this.dataSeries_.length; ++i) {
+ var values = this.getValues(this.dataSeries_[i]);
+ if (!values)
+ continue;
+ for (var j = 0; j < values.length; ++j) {
+ if (values[j] > max)
+ max = values[j];
+ }
+ }
+
+ this.layoutLabels_(max);
+ },
+
+ /**
+ * Lays out labels and sets |max_|, taking the time units into
+ * consideration. |maxValue| is the actual maximum value, and
+ * |max_| will be set to the value of the largest label, which
+ * will be at least |maxValue|.
+ */
+ layoutLabels_: function(maxValue) {
+ if (maxValue < 1024) {
+ this.layoutLabelsBasic_(maxValue, 0);
+ return;
+ }
+
+ // Find appropriate units to use.
+ var units = ['', 'k', 'M', 'G', 'T', 'P'];
+ // Units to use for labels. 0 is '1', 1 is K, etc.
+ // We start with 1, and work our way up.
+ var unit = 1;
+ maxValue /= 1024;
+ while (units[unit + 1] && maxValue >= 1024) {
+ maxValue /= 1024;
+ ++unit;
+ }
+
+ // Calculate labels.
+ this.layoutLabelsBasic_(maxValue, 1);
+
+ // Append units to labels.
+ for (var i = 0; i < this.labels_.length; ++i)
+ this.labels_[i] += ' ' + units[unit];
+
+ // Convert |max_| back to unit '1'.
+ this.max_ *= Math.pow(1024, unit);
+ },
+
+ /**
+ * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the
+ * maximum number of decimal digits allowed. The minimum allowed
+ * difference between two adjacent labels is 10^-|maxDecimalDigits|.
+ */
+ layoutLabelsBasic_: function(maxValue, maxDecimalDigits) {
+ this.labels_ = [];
+ // No labels if |maxValue| is 0.
+ if (maxValue == 0) {
+ this.max_ = maxValue;
+ return;
+ }
+
+ // The maximum number of equally spaced labels allowed. |fontHeight_|
+ // is doubled because the top two labels are both drawn in the same
+ // gap.
+ var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;
+
+ // The + 1 is for the top label.
+ var maxLabels = 1 + this.height_ / minLabelSpacing;
+ if (maxLabels < 2) {
+ maxLabels = 2;
+ } else if (maxLabels > MAX_VERTICAL_LABELS) {
+ maxLabels = MAX_VERTICAL_LABELS;
+ }
+
+ // Initial try for step size between conecutive labels.
+ var stepSize = Math.pow(10, -maxDecimalDigits);
+ // Number of digits to the right of the decimal of |stepSize|.
+ // Used for formating label strings.
+ var stepSizeDecimalDigits = maxDecimalDigits;
+
+ // Pick a reasonable step size.
+ while (true) {
+ // If we use a step size of |stepSize| between labels, we'll need:
+ //
+ // Math.ceil(maxValue / stepSize) + 1
+ //
+ // labels. The + 1 is because we need labels at both at 0 and at
+ // the top of the graph.
+
+ // Check if we can use steps of size |stepSize|.
+ if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels)
+ break;
+ // Check |stepSize| * 2.
+ if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) {
+ stepSize *= 2;
+ break;
+ }
+ // Check |stepSize| * 5.
+ if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) {
+ stepSize *= 5;
+ break;
+ }
+ stepSize *= 10;
+ if (stepSizeDecimalDigits > 0)
+ --stepSizeDecimalDigits;
+ }
+
+ // Set the max so it's an exact multiple of the chosen step size.
+ this.max_ = Math.ceil(maxValue / stepSize) * stepSize;
+
+ // Create labels.
+ for (var label = this.max_; label >= 0; label -= stepSize)
+ this.labels_.push(label.toFixed(stepSizeDecimalDigits));
+ },
+
+ /**
+ * Draws tick marks for each of the labels in |labels_|.
+ */
+ drawTicks: function(context) {
+ var x1;
+ var x2;
+ x1 = this.width_ - 1;
+ x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;
+
+ context.fillStyle = GRID_COLOR;
+ context.beginPath();
+ for (var i = 1; i < this.labels_.length - 1; ++i) {
+ // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
+ // lines.
+ var y = Math.round(this.height_ * i / (this.labels_.length - 1));
+ context.moveTo(x1, y);
+ context.lineTo(x2, y);
+ }
+ context.stroke();
+ },
+
+ /**
+ * Draws a graph line for each of the data series.
+ */
+ drawLines: function(context) {
+ // Factor by which to scale all values to convert them to a number from
+ // 0 to height - 1.
+ var scale = 0;
+ var bottom = this.height_ - 1;
+ if (this.max_)
+ scale = bottom / this.max_;
+
+ // Draw in reverse order, so earlier data series are drawn on top of
+ // subsequent ones.
+ for (var i = this.dataSeries_.length - 1; i >= 0; --i) {
+ var values = this.getValues(this.dataSeries_[i]);
+ if (!values)
+ continue;
+ context.strokeStyle = this.dataSeries_[i].getColor();
+ context.beginPath();
+ for (var x = 0; x < values.length; ++x) {
+ // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
+ // horizontal lines.
+ context.lineTo(x, bottom - Math.round(values[x] * scale));
+ }
+ context.stroke();
+ }
+ },
+
+ /**
+ * Draw labels in |labels_|.
+ */
+ drawLabels: function(context) {
+ if (this.labels_.length == 0)
+ return;
+ var x = this.width_ - LABEL_HORIZONTAL_SPACING;
+
+ // Set up the context.
+ context.fillStyle = TEXT_COLOR;
+ context.textAlign = 'right';
+
+ // Draw top label, which is the only one that appears below its tick
+ // mark.
+ context.textBaseline = 'top';
+ context.fillText(this.labels_[0], x, 0);
+
+ // Draw all the other labels.
+ context.textBaseline = 'bottom';
+ var step = (this.height_ - 1) / (this.labels_.length - 1);
+ for (var i = 1; i < this.labels_.length; ++i)
+ context.fillText(this.labels_[i], x, step * i);
+ }
+ };
+
+ return Graph;
+ })();
+
+ return TimelineGraphView;
+})();
diff --git a/content/browser/resources/media/webrtc_internals.css b/content/browser/resources/media/webrtc_internals.css
index f48a722..83d1f39 100644
--- a/content/browser/resources/media/webrtc_internals.css
+++ b/content/browser/resources/media/webrtc_internals.css
@@ -6,6 +6,36 @@
float: left;
}
+.stats-graph-container,
+.stats-graph-container-collapsed {
+ clear: both;
+ margin: 0.5em 0 0.5em 0;
+}
+
+.stats-graph-container button,
+.stats-graph-container-collapsed button {
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+.stats-graph-container-collapsed .stats-graph-sub-container {
+ display: none;
+}
+
+.stats-graph-sub-container {
+ float: left;
+ margin: 0.5em;
+}
+
+.stats-graph-sub-container > div {
+ float: left;
+}
+
+.stats-graph-sub-container > div:first-child {
+ float: none;
+}
+
.stats-table-container {
float: left;
padding: 0 0 0 0;
@@ -36,7 +66,7 @@ h2 {
h3 {
color: #666;
font-size: 0.9em;
- margin: 0 0 0.5em 0;
+ margin: 1em 0 0.5em 0;
}
li {
diff --git a/content/browser/resources/media/webrtc_internals.js b/content/browser/resources/media/webrtc_internals.js
index af29ac9..77faaf8 100644
--- a/content/browser/resources/media/webrtc_internals.js
+++ b/content/browser/resources/media/webrtc_internals.js
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+<include src="stats_graph_helper.js"/>
+
var peerConnectionsListElem = null;
function initialize() {
@@ -169,13 +171,14 @@ function updatePeerConnection(data) {
addToPeerConnectionLog(logElement, data);
}
-// data is an array and each entry is in the same format as the input of
-// updatePeerConnection.
+// data is an array and each entry is
+// {pid:|integer|, lid:|integer|,
+// url:|string|, servers:|string|, constraints:|string|, log:|array|},
+// each entry of log is {type:|string|, value:|string|}.
function updateAllPeerConnections(data) {
for (var i = 0; i < data.length; ++i) {
var peerConnection = addPeerConnection(data[i]);
var logElement = ensurePeerConnectionLog(peerConnection);
- logElement.value = '';
var log = data[i].log;
for (var j = 0; j < log.length; ++j) {
@@ -186,7 +189,7 @@ function updateAllPeerConnections(data) {
// data = {pid:|integer|, lid:|integer|, reports:|array|}.
// Each entry of reports =
-// {id:|string|, type:|string|, local:|array|, remote:|array|}.
+// {id:|string|, type:|string|, local:|object|, remote:|object|}.
// reports.local or reports.remote =
// {timestamp: |double|, values: |array|},
// where values is an array of strings, whose even index entry represents
@@ -197,11 +200,13 @@ function addStats(data) {
getPeerConnectionId(data));
for (var i = 0; i < data.reports.length; ++i) {
var report = data.reports[i];
- var statsTable = ensureStatsTable(peerConnectionElement,
- report.type + '-' + report.id);
+ var reportName = report.type + '-' + report.id;
+ var statsTable = ensureStatsTable(peerConnectionElement, reportName);
addSingleReportToTable(statsTable, report.local);
+ drawSingleReport(peerConnectionElement, reportName, report.local);
addSingleReportToTable(statsTable, report.remote);
+ drawSingleReport(peerConnectionElement, reportName, report.remote);
}
}
diff --git a/content/content_tests.gypi b/content/content_tests.gypi
index 8336e0f..6f6bf82 100644
--- a/content/content_tests.gypi
+++ b/content/content_tests.gypi
@@ -734,6 +734,7 @@
'browser/media/audio_browsertest.cc',
'browser/media/encrypted_media_browsertest.cc',
'browser/media/media_browsertest.cc',
+ 'browser/media/webrtc_internals_browsertest.cc',
'browser/plugin_data_remover_impl_browsertest.cc',
'browser/plugin_browsertest.cc',
'browser/plugin_service_impl_browsertest.cc',