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