1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 // This file contains helper methods to draw the stats timeline graphs.
7 // Each graph represents a series of stats report for a PeerConnection,
8 // e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent
9 // for ssrc-abcd123 of PeerConnection 0 in process 1234.
10 // The graphs are drawn as CANVAS, grouped per report type per PeerConnection.
11 // Each group has an expand/collapse button and is collapsed initially.
14 <include src="timeline_graph_view.js"/>
16 var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
18 var RECEIVED_PROPAGATION_DELTA_LABEL =
19 'googReceivedPacketGroupPropagationDeltaDebug';
20 var RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL =
21 'googReceivedPacketGroupArrivalTimeDebug';
23 // Specifies which stats should be drawn on the 'bweCompound' graph and how.
24 var bweCompoundGraphConfig = {
25 googAvailableSendBandwidth: {color: 'red'},
26 googTargetEncBitrateCorrected: {color: 'purple'},
27 googActualEncBitrate: {color: 'orange'},
28 googRetransmitBitrate: {color: 'blue'},
29 googTransmitBitrate: {color: 'green'},
32 // Converts the last entry of |srcDataSeries| from the total amount to the
34 var totalToPerSecond = function(srcDataSeries) {
35 var length = srcDataSeries.dataPoints_.length;
37 var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
38 var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2];
39 return (lastDataPoint.value - secondLastDataPoint.value) * 1000 /
40 (lastDataPoint.time - secondLastDataPoint.time);
46 // Converts the value of total bytes to bits per second.
47 var totalBytesToBitsPerSecond = function(srcDataSeries) {
48 return totalToPerSecond(srcDataSeries) * 8;
51 // Specifies which stats should be converted before drawn and how.
52 // |convertedName| is the name of the converted value, |convertFunction|
53 // is the function used to calculate the new converted value based on the
54 // original dataSeries.
55 var dataConversionConfig = {
57 convertedName: 'packetsSentPerSecond',
58 convertFunction: totalToPerSecond,
61 convertedName: 'bitsSentPerSecond',
62 convertFunction: totalBytesToBitsPerSecond,
65 convertedName: 'packetsReceivedPerSecond',
66 convertFunction: totalToPerSecond,
69 convertedName: 'bitsReceivedPerSecond',
70 convertFunction: totalBytesToBitsPerSecond,
72 // This is due to a bug of wrong units reported for googTargetEncBitrate.
73 // TODO (jiayl): remove this when the unit bug is fixed.
74 googTargetEncBitrate: {
75 convertedName: 'googTargetEncBitrateCorrected',
76 convertFunction: function (srcDataSeries) {
77 var length = srcDataSeries.dataPoints_.length;
78 var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
79 if (lastDataPoint.value < 5000)
80 return lastDataPoint.value * 1000;
81 return lastDataPoint.value;
87 // The object contains the stats names that should not be added to the graph,
88 // even if they are numbers.
89 var statsNameBlackList = {
92 'googComponent': true,
93 'googLocalAddress': true,
94 'googRemoteAddress': true,
95 'googFingerprint': true,
100 // Returns number parsed from |value|, or NaN if the stats name is black-listed.
101 function getNumberFromValue(name, value) {
102 if (statsNameBlackList[name])
104 return parseFloat(value);
107 // Adds the stats report |report| to the timeline graph for the given
108 // |peerConnectionElement|.
109 function drawSingleReport(peerConnectionElement, report) {
110 var reportType = report.type;
111 var reportId = report.id;
112 var stats = report.stats;
113 if (!stats || !stats.values)
116 for (var i = 0; i < stats.values.length - 1; i = i + 2) {
117 var rawLabel = stats.values[i];
118 // Propagation deltas are handled separately.
119 if (rawLabel == RECEIVED_PROPAGATION_DELTA_LABEL) {
120 drawReceivedPropagationDelta(
121 peerConnectionElement, report, stats.values[i + 1]);
124 var rawDataSeriesId = reportId + '-' + rawLabel;
125 var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]);
126 if (isNaN(rawValue)) {
127 // We do not draw non-numerical values, but still want to record it in the
129 addDataSeriesPoints(peerConnectionElement,
133 [stats.values[i + 1]]);
137 var finalDataSeriesId = rawDataSeriesId;
138 var finalLabel = rawLabel;
139 var finalValue = rawValue;
140 // We need to convert the value if dataConversionConfig[rawLabel] exists.
141 if (dataConversionConfig[rawLabel]) {
142 // Updates the original dataSeries before the conversion.
143 addDataSeriesPoints(peerConnectionElement,
149 // Convert to another value to draw on graph, using the original
150 // dataSeries as input.
151 finalValue = dataConversionConfig[rawLabel].convertFunction(
152 peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
154 finalLabel = dataConversionConfig[rawLabel].convertedName;
155 finalDataSeriesId = reportId + '-' + finalLabel;
158 // Updates the final dataSeries to draw.
159 addDataSeriesPoints(peerConnectionElement,
165 // Updates the graph.
166 var graphType = bweCompoundGraphConfig[finalLabel] ?
167 'bweCompound' : finalLabel;
169 peerConnectionElement.id + '-' + reportId + '-' + graphType;
171 if (!graphViews[graphViewId]) {
172 graphViews[graphViewId] = createStatsGraphView(peerConnectionElement,
175 var date = new Date(stats.timestamp);
176 graphViews[graphViewId].setDateRange(date, date);
178 // Adds the new dataSeries to the graphView. We have to do it here to cover
179 // both the simple and compound graph cases.
181 peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
183 if (!graphViews[graphViewId].hasDataSeries(dataSeries))
184 graphViews[graphViewId].addDataSeries(dataSeries);
185 graphViews[graphViewId].updateEndDate();
189 // Makes sure the TimelineDataSeries with id |dataSeriesId| is created,
190 // and adds the new data points to it. |times| is the list of timestamps for
191 // each data point, and |values| is the list of the data point values.
192 function addDataSeriesPoints(
193 peerConnectionElement, dataSeriesId, label, times, values) {
195 peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
198 dataSeries = new TimelineDataSeries();
199 peerConnectionDataStore[peerConnectionElement.id].setDataSeries(
200 dataSeriesId, dataSeries);
201 if (bweCompoundGraphConfig[label]) {
202 dataSeries.setColor(bweCompoundGraphConfig[label].color);
205 for (var i = 0; i < times.length; ++i)
206 dataSeries.addPoint(times[i], values[i]);
209 // Draws the received propagation deltas using the packet group arrival time as
210 // the x-axis. For example, |report.stats.values| should be like
211 // ['googReceivedPacketGroupArrivalTimeDebug', '[123456, 234455, 344566]',
212 // 'googReceivedPacketGroupPropagationDeltaDebug', '[23, 45, 56]', ...].
213 function drawReceivedPropagationDelta(peerConnectionElement, report, deltas) {
214 var reportId = report.id;
215 var stats = report.stats;
217 // Find the packet group arrival times.
218 for (var i = 0; i < stats.values.length - 1; i = i + 2) {
219 if (stats.values[i] == RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL) {
220 times = stats.values[i + 1];
228 // Convert |deltas| and |times| from strings to arrays of numbers.
230 deltas = JSON.parse(deltas);
231 times = JSON.parse(times);
237 // Update the data series.
238 var dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL;
240 peerConnectionElement,
242 RECEIVED_PROPAGATION_DELTA_LABEL,
246 var graphViewId = peerConnectionElement.id + '-' + reportId + '-' +
247 RECEIVED_PROPAGATION_DELTA_LABEL;
248 var date = new Date(times[times.length - 1]);
249 if (!graphViews[graphViewId]) {
250 graphViews[graphViewId] = createStatsGraphView(
251 peerConnectionElement,
253 RECEIVED_PROPAGATION_DELTA_LABEL);
254 graphViews[graphViewId].setScale(10);
255 graphViews[graphViewId].setDateRange(date, date);
256 var dataSeries = peerConnectionDataStore[peerConnectionElement.id]
257 .getDataSeries(dataSeriesId);
258 graphViews[graphViewId].addDataSeries(dataSeries);
260 graphViews[graphViewId].updateEndDate(date);
263 // Ensures a div container to hold all stats graphs for one track is created as
264 // a child of |peerConnectionElement|.
265 function ensureStatsGraphTopContainer(peerConnectionElement, report) {
266 var containerId = peerConnectionElement.id + '-' +
267 report.type + '-' + report.id + '-graph-container';
268 var container = $(containerId);
270 container = document.createElement('details');
271 container.id = containerId;
272 container.className = 'stats-graph-container';
274 peerConnectionElement.appendChild(container);
275 container.innerHTML ='<summary><span></span></summary>';
276 container.firstChild.firstChild.className =
277 STATS_GRAPH_CONTAINER_HEADING_CLASS;
278 container.firstChild.firstChild.textContent =
279 'Stats graphs for ' + report.id;
281 if (report.type == 'ssrc') {
282 var ssrcInfoElement = document.createElement('div');
283 container.firstChild.appendChild(ssrcInfoElement);
284 ssrcInfoManager.populateSsrcInfo(ssrcInfoElement,
285 GetSsrcFromReport(report));
291 // Creates the container elements holding a timeline graph
292 // and the TimelineGraphView object.
293 function createStatsGraphView(
294 peerConnectionElement, report, statsName) {
295 var topContainer = ensureStatsGraphTopContainer(peerConnectionElement,
299 peerConnectionElement.id + '-' + report.id + '-' + statsName;
300 var divId = graphViewId + '-div';
301 var canvasId = graphViewId + '-canvas';
302 var container = document.createElement("div");
303 container.className = 'stats-graph-sub-container';
305 topContainer.appendChild(container);
306 container.innerHTML = '<div>' + statsName + '</div>' +
307 '<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>';
308 if (statsName == 'bweCompound') {
309 container.insertBefore(
310 createBweCompoundLegend(peerConnectionElement, report.id),
313 return new TimelineGraphView(divId, canvasId);
316 // Creates the legend section for the bweCompound graph.
317 // Returns the legend element.
318 function createBweCompoundLegend(peerConnectionElement, reportId) {
319 var legend = document.createElement('div');
320 for (var prop in bweCompoundGraphConfig) {
321 var div = document.createElement('div');
322 legend.appendChild(div);
323 div.innerHTML = '<input type=checkbox checked></input>' + prop;
324 div.style.color = bweCompoundGraphConfig[prop].color;
325 div.dataSeriesId = reportId + '-' + prop;
327 peerConnectionElement.id + '-' + reportId + '-bweCompound';
328 div.firstChild.addEventListener('click', function(event) {
330 peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
331 event.target.parentNode.dataSeriesId);
332 target.show(event.target.checked);
333 graphViews[event.target.parentNode.graphViewId].repaint();