• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
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.
12//
13
14<include src="timeline_graph_view.js"/>
15
16var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
17
18var RECEIVED_PROPAGATION_DELTA_LABEL =
19    'googReceivedPacketGroupPropagationDeltaDebug';
20var RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL =
21    'googReceivedPacketGroupArrivalTimeDebug';
22
23// Specifies which stats should be drawn on the 'bweCompound' graph and how.
24var bweCompoundGraphConfig = {
25  googAvailableSendBandwidth: {color: 'red'},
26  googTargetEncBitrateCorrected: {color: 'purple'},
27  googActualEncBitrate: {color: 'orange'},
28  googRetransmitBitrate: {color: 'blue'},
29  googTransmitBitrate: {color: 'green'},
30};
31
32// Converts the last entry of |srcDataSeries| from the total amount to the
33// amount per second.
34var 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  }
42
43  return 0;
44};
45
46// Converts the value of total bytes to bits per second.
47var totalBytesToBitsPerSecond = function(srcDataSeries) {
48  return totalToPerSecond(srcDataSeries) * 8;
49};
50
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.
55var 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  }
84};
85
86
87// The object contains the stats names that should not be added to the graph,
88// even if they are numbers.
89var statsNameBlackList = {
90  'ssrc': true,
91  'googTrackId': true,
92  'googComponent': true,
93  'googLocalAddress': true,
94  'googRemoteAddress': true,
95  'googFingerprint': true,
96};
97
98var graphViews = {};
99
100// Returns number parsed from |value|, or NaN if the stats name is black-listed.
101function getNumberFromValue(name, value) {
102  if (statsNameBlackList[name])
103    return NaN;
104  return parseFloat(value);
105}
106
107// Adds the stats report |report| to the timeline graph for the given
108// |peerConnectionElement|.
109function 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;
115
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    }
136
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]);
148
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    }
157
158    // Updates the final dataSeries to draw.
159    addDataSeriesPoints(peerConnectionElement,
160                        finalDataSeriesId,
161                        finalLabel,
162                        [stats.timestamp],
163                        [finalValue]);
164
165    // Updates the graph.
166    var graphType = bweCompoundGraphConfig[finalLabel] ?
167                    'bweCompound' : finalLabel;
168    var graphViewId =
169        peerConnectionElement.id + '-' + reportId + '-' + graphType;
170
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  }
187}
188
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.
192function 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]);
207}
208
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]', ...].
213function 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;
227
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  }
236
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);
261}
262
263// Ensures a div container to hold all stats graphs for one track is created as
264// a child of |peerConnectionElement|.
265function ensureStatsGraphTopContainer(peerConnectionElement, report) {
266  var containerId = peerConnectionElement.id + '-' +
267      report.type + '-' + report.id + '-graph-container';
268  var container = $(containerId);
269  if (!container) {
270    container = document.createElement('details');
271    container.id = containerId;
272    container.className = 'stats-graph-container';
273
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;
280
281    if (report.type == 'ssrc') {
282      var ssrcInfoElement = document.createElement('div');
283      container.firstChild.appendChild(ssrcInfoElement);
284      ssrcInfoManager.populateSsrcInfo(ssrcInfoElement,
285                                       GetSsrcFromReport(report));
286    }
287  }
288  return container;
289}
290
291// Creates the container elements holding a timeline graph
292// and the TimelineGraphView object.
293function createStatsGraphView(
294    peerConnectionElement, report, statsName) {
295  var topContainer = ensureStatsGraphTopContainer(peerConnectionElement,
296                                                  report);
297
298  var graphViewId =
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';
304
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),
311          $(divId));
312  }
313  return new TimelineGraphView(divId, canvasId);
314}
315
316// Creates the legend section for the bweCompound graph.
317// Returns the legend element.
318function 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;
326    div.graphViewId =
327        peerConnectionElement.id + '-' + reportId + '-bweCompound';
328    div.firstChild.addEventListener('click', function(event) {
329        var target =
330            peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
331                event.target.parentNode.dataSeriesId);
332        target.show(event.target.checked);
333        graphViews[event.target.parentNode.graphViewId].repaint();
334    });
335  }
336  return legend;
337}
338