Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / content / browser / resources / media / stats_graph_helper.js
blob39b6c59c72a801a76c7edabd552ddd99b2165d39
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.
5 //
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
33 // amount per second.
34 var totalToPerSecond = function(srcDataSeries) {
35   var length = srcDataSeries.dataPoints_.length;
36   if (length >= 2) {
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);
41   }
43   return 0;
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 = {
56   packetsSent: {
57     convertedName: 'packetsSentPerSecond',
58     convertFunction: totalToPerSecond,
59   },
60   bytesSent: {
61     convertedName: 'bitsSentPerSecond',
62     convertFunction: totalBytesToBitsPerSecond,
63   },
64   packetsReceived: {
65     convertedName: 'packetsReceivedPerSecond',
66     convertFunction: totalToPerSecond,
67   },
68   bytesReceived: {
69     convertedName: 'bitsReceivedPerSecond',
70     convertFunction: totalBytesToBitsPerSecond,
71   },
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;
82     }
83   }
87 // The object contains the stats names that should not be added to the graph,
88 // even if they are numbers.
89 var statsNameBlackList = {
90   'ssrc': true,
91   'googTrackId': true,
92   'googComponent': true,
93   'googLocalAddress': true,
94   'googRemoteAddress': true,
95   'googFingerprint': true,
98 var graphViews = {};
100 // Returns number parsed from |value|, or NaN if the stats name is black-listed.
101 function getNumberFromValue(name, value) {
102   if (statsNameBlackList[name])
103     return NaN;
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)
114     return;
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]);
122       continue;
123     }
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
128       // data series.
129       addDataSeriesPoints(peerConnectionElement,
130                           rawDataSeriesId,
131                           rawLabel,
132                           [stats.timestamp],
133                           [stats.values[i + 1]]);
134       continue;
135     }
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,
144                           rawDataSeriesId,
145                           rawLabel,
146                           [stats.timestamp],
147                           [rawValue]);
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(
153               rawDataSeriesId));
154       finalLabel = dataConversionConfig[rawLabel].convertedName;
155       finalDataSeriesId = reportId + '-' + finalLabel;
156     }
158     // Updates the final dataSeries to draw.
159     addDataSeriesPoints(peerConnectionElement,
160                         finalDataSeriesId,
161                         finalLabel,
162                         [stats.timestamp],
163                         [finalValue]);
165     // Updates the graph.
166     var graphType = bweCompoundGraphConfig[finalLabel] ?
167                     'bweCompound' : finalLabel;
168     var graphViewId =
169         peerConnectionElement.id + '-' + reportId + '-' + graphType;
171     if (!graphViews[graphViewId]) {
172       graphViews[graphViewId] = createStatsGraphView(peerConnectionElement,
173                                                      report,
174                                                      graphType);
175       var date = new Date(stats.timestamp);
176       graphViews[graphViewId].setDateRange(date, date);
177     }
178     // Adds the new dataSeries to the graphView. We have to do it here to cover
179     // both the simple and compound graph cases.
180     var dataSeries =
181         peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
182             finalDataSeriesId);
183     if (!graphViews[graphViewId].hasDataSeries(dataSeries))
184       graphViews[graphViewId].addDataSeries(dataSeries);
185     graphViews[graphViewId].updateEndDate();
186   }
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) {
194   var dataSeries =
195     peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
196         dataSeriesId);
197   if (!dataSeries) {
198     dataSeries = new TimelineDataSeries();
199     peerConnectionDataStore[peerConnectionElement.id].setDataSeries(
200         dataSeriesId, dataSeries);
201     if (bweCompoundGraphConfig[label]) {
202       dataSeries.setColor(bweCompoundGraphConfig[label].color);
203     }
204   }
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;
216   var times = null;
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];
221       break;
222     }
223   }
224   // Unexpected.
225   if (times == null)
226     return;
228   // Convert |deltas| and |times| from strings to arrays of numbers.
229   try {
230     deltas = JSON.parse(deltas);
231     times = JSON.parse(times);
232   } catch (e) {
233     console.log(e);
234     return;
235   }
237   // Update the data series.
238   var dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL;
239   addDataSeriesPoints(
240       peerConnectionElement,
241       dataSeriesId,
242       RECEIVED_PROPAGATION_DELTA_LABEL,
243       times,
244       deltas);
245   // Update the graph.
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,
252         report,
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);
259   }
260   graphViews[graphViewId].updateEndDate(date);
263 // Get report types for SSRC reports. Returns 'audio' or 'video' where this type
264 // can be deduced from existing stats labels. Otherwise empty string for
265 // non-SSRC reports or where type (audio/video) can't be deduced.
266 function getSsrcReportType(report) {
267   if (report.type != 'ssrc')
268     return '';
269   if (report.stats && report.stats.values) {
270     // Known stats keys for audio send/receive streams.
271     if (report.stats.values.indexOf('audioOutputLevel') != -1 ||
272         report.stats.values.indexOf('audioInputLevel') != -1) {
273       return 'audio';
274     }
275     // Known stats keys for video send/receive streams.
276     // TODO(pbos): Change to use some non-goog-prefixed stats when available for
277     // video.
278     if (report.stats.values.indexOf('googFrameRateReceived') != -1 ||
279         report.stats.values.indexOf('googFrameRateSent') != -1) {
280       return 'video';
281     }
282   }
283   return '';
286 // Ensures a div container to hold all stats graphs for one track is created as
287 // a child of |peerConnectionElement|.
288 function ensureStatsGraphTopContainer(peerConnectionElement, report) {
289   var containerId = peerConnectionElement.id + '-' +
290       report.type + '-' + report.id + '-graph-container';
291   var container = $(containerId);
292   if (!container) {
293     container = document.createElement('details');
294     container.id = containerId;
295     container.className = 'stats-graph-container';
297     peerConnectionElement.appendChild(container);
298     container.innerHTML ='<summary><span></span></summary>';
299     container.firstChild.firstChild.className =
300         STATS_GRAPH_CONTAINER_HEADING_CLASS;
301     container.firstChild.firstChild.textContent =
302         'Stats graphs for ' + report.id;
303     var statsType = getSsrcReportType(report);
304     if (statsType != '')
305       container.firstChild.firstChild.textContent += ' (' + statsType + ')';
307     if (report.type == 'ssrc') {
308       var ssrcInfoElement = document.createElement('div');
309       container.firstChild.appendChild(ssrcInfoElement);
310       ssrcInfoManager.populateSsrcInfo(ssrcInfoElement,
311                                        GetSsrcFromReport(report));
312     }
313   }
314   return container;
317 // Creates the container elements holding a timeline graph
318 // and the TimelineGraphView object.
319 function createStatsGraphView(
320     peerConnectionElement, report, statsName) {
321   var topContainer = ensureStatsGraphTopContainer(peerConnectionElement,
322                                                   report);
324   var graphViewId =
325       peerConnectionElement.id + '-' + report.id + '-' + statsName;
326   var divId = graphViewId + '-div';
327   var canvasId = graphViewId + '-canvas';
328   var container = document.createElement("div");
329   container.className = 'stats-graph-sub-container';
331   topContainer.appendChild(container);
332   container.innerHTML = '<div>' + statsName + '</div>' +
333       '<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>';
334   if (statsName == 'bweCompound') {
335       container.insertBefore(
336           createBweCompoundLegend(peerConnectionElement, report.id),
337           $(divId));
338   }
339   return new TimelineGraphView(divId, canvasId);
342 // Creates the legend section for the bweCompound graph.
343 // Returns the legend element.
344 function createBweCompoundLegend(peerConnectionElement, reportId) {
345   var legend = document.createElement('div');
346   for (var prop in bweCompoundGraphConfig) {
347     var div = document.createElement('div');
348     legend.appendChild(div);
349     div.innerHTML = '<input type=checkbox checked></input>' + prop;
350     div.style.color = bweCompoundGraphConfig[prop].color;
351     div.dataSeriesId = reportId + '-' + prop;
352     div.graphViewId =
353         peerConnectionElement.id + '-' + reportId + '-bweCompound';
354     div.firstChild.addEventListener('click', function(event) {
355         var target =
356             peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
357                 event.target.parentNode.dataSeriesId);
358         target.show(event.target.checked);
359         graphViews[event.target.parentNode.graphViewId].repaint();
360     });
361   }
362   return legend;