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 16 var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading'; 17 18 // Specifies which stats should be drawn on the 'bweCompound' graph and how. 19 var bweCompoundGraphConfig = { 20 googAvailableSendBandwidth: {color: 'red'}, 21 googTargetEncBitrateCorrected: {color: 'purple'}, 22 googActualEncBitrate: {color: 'orange'}, 23 googRetransmitBitrate: {color: 'blue'}, 24 googTransmitBitrate: {color: 'green'}, 25 }; 26 27 // Converts the last entry of |srcDataSeries| from the total amount to the 28 // amount per second. 29 var totalToPerSecond = function(srcDataSeries) { 30 var length = srcDataSeries.dataPoints_.length; 31 if (length >= 2) { 32 var lastDataPoint = srcDataSeries.dataPoints_[length - 1]; 33 var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2]; 34 return (lastDataPoint.value - secondLastDataPoint.value) * 1000 / 35 (lastDataPoint.time - secondLastDataPoint.time); 36 } 37 38 return 0; 39 }; 40 41 // Converts the value of total bytes to bits per second. 42 var totalBytesToBitsPerSecond = function(srcDataSeries) { 43 return totalToPerSecond(srcDataSeries) * 8; 44 }; 45 46 // Specifies which stats should be converted before drawn and how. 47 // |convertedName| is the name of the converted value, |convertFunction| 48 // is the function used to calculate the new converted value based on the 49 // original dataSeries. 50 var dataConversionConfig = { 51 packetsSent: { 52 convertedName: 'packetsSentPerSecond', 53 convertFunction: totalToPerSecond, 54 }, 55 bytesSent: { 56 convertedName: 'bitsSentPerSecond', 57 convertFunction: totalBytesToBitsPerSecond, 58 }, 59 packetsReceived: { 60 convertedName: 'packetsReceivedPerSecond', 61 convertFunction: totalToPerSecond, 62 }, 63 bytesReceived: { 64 convertedName: 'bitsReceivedPerSecond', 65 convertFunction: totalBytesToBitsPerSecond, 66 }, 67 // This is due to a bug of wrong units reported for googTargetEncBitrate. 68 // TODO (jiayl): remove this when the unit bug is fixed. 69 googTargetEncBitrate: { 70 convertedName: 'googTargetEncBitrateCorrected', 71 convertFunction: function (srcDataSeries) { 72 var length = srcDataSeries.dataPoints_.length; 73 var lastDataPoint = srcDataSeries.dataPoints_[length - 1]; 74 if (lastDataPoint.value < 5000) 75 return lastDataPoint.value * 1000; 76 return lastDataPoint.value; 77 } 78 } 79 }; 80 81 82 // The object contains the stats names that should not be added to the graph, 83 // even if they are numbers. 84 var statsNameBlackList = { 85 'ssrc': true, 86 'googTrackId': true, 87 'googComponent': true, 88 'googLocalAddress': true, 89 'googRemoteAddress': true, 90 }; 91 92 var graphViews = {}; 93 94 // Returns number parsed from |value|, or NaN if the stats name is black-listed. 95 function getNumberFromValue(name, value) { 96 if (statsNameBlackList[name]) 97 return NaN; 98 return parseFloat(value); 99 } 100 101 // Adds the stats report |report| to the timeline graph for the given 102 // |peerConnectionElement|. 103 function drawSingleReport(peerConnectionElement, report) { 104 var reportType = report.type; 105 var reportId = report.id; 106 var stats = report.stats; 107 if (!stats || !stats.values) 108 return; 109 110 for (var i = 0; i < stats.values.length - 1; i = i + 2) { 111 var rawLabel = stats.values[i]; 112 var rawDataSeriesId = reportId + '-' + rawLabel; 113 var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]); 114 if (isNaN(rawValue)) { 115 // We do not draw non-numerical values, but still want to record it in the 116 // data series. 117 addDataSeriesPoint(peerConnectionElement, 118 rawDataSeriesId, stats.timestamp, 119 rawLabel, stats.values[i + 1]); 120 continue; 121 } 122 123 var finalDataSeriesId = rawDataSeriesId; 124 var finalLabel = rawLabel; 125 var finalValue = rawValue; 126 // We need to convert the value if dataConversionConfig[rawLabel] exists. 127 if (dataConversionConfig[rawLabel]) { 128 // Updates the original dataSeries before the conversion. 129 addDataSeriesPoint(peerConnectionElement, 130 rawDataSeriesId, stats.timestamp, 131 rawLabel, rawValue); 132 133 // Convert to another value to draw on graph, using the original 134 // dataSeries as input. 135 finalValue = dataConversionConfig[rawLabel].convertFunction( 136 peerConnectionDataStore[peerConnectionElement.id].getDataSeries( 137 rawDataSeriesId)); 138 finalLabel = dataConversionConfig[rawLabel].convertedName; 139 finalDataSeriesId = reportId + '-' + finalLabel; 140 } 141 142 // Updates the final dataSeries to draw. 143 addDataSeriesPoint(peerConnectionElement, 144 finalDataSeriesId, 145 stats.timestamp, 146 finalLabel, 147 finalValue); 148 149 // Updates the graph. 150 var graphType = bweCompoundGraphConfig[finalLabel] ? 151 'bweCompound' : finalLabel; 152 var graphViewId = 153 peerConnectionElement.id + '-' + reportId + '-' + graphType; 154 155 if (!graphViews[graphViewId]) { 156 graphViews[graphViewId] = createStatsGraphView(peerConnectionElement, 157 report, 158 graphType); 159 var date = new Date(stats.timestamp); 160 graphViews[graphViewId].setDateRange(date, date); 161 } 162 // Adds the new dataSeries to the graphView. We have to do it here to cover 163 // both the simple and compound graph cases. 164 var dataSeries = 165 peerConnectionDataStore[peerConnectionElement.id].getDataSeries( 166 finalDataSeriesId); 167 if (!graphViews[graphViewId].hasDataSeries(dataSeries)) 168 graphViews[graphViewId].addDataSeries(dataSeries); 169 graphViews[graphViewId].updateEndDate(); 170 } 171 } 172 173 // Makes sure the TimelineDataSeries with id |dataSeriesId| is created, 174 // and adds the new data point to it. 175 function addDataSeriesPoint( 176 peerConnectionElement, dataSeriesId, time, label, value) { 177 var dataSeries = 178 peerConnectionDataStore[peerConnectionElement.id].getDataSeries( 179 dataSeriesId); 180 if (!dataSeries) { 181 dataSeries = new TimelineDataSeries(); 182 peerConnectionDataStore[peerConnectionElement.id].setDataSeries( 183 dataSeriesId, dataSeries); 184 if (bweCompoundGraphConfig[label]) { 185 dataSeries.setColor(bweCompoundGraphConfig[label].color); 186 } 187 } 188 dataSeries.addPoint(time, value); 189 } 190 191 // Ensures a div container to hold all stats graphs for one track is created as 192 // a child of |peerConnectionElement|. 193 function ensureStatsGraphTopContainer(peerConnectionElement, report) { 194 var containerId = peerConnectionElement.id + '-' + 195 report.type + '-' + report.id + '-graph-container'; 196 var container = $(containerId); 197 if (!container) { 198 container = document.createElement('details'); 199 container.id = containerId; 200 container.className = 'stats-graph-container'; 201 202 peerConnectionElement.appendChild(container); 203 container.innerHTML ='<summary><span></span></summary>'; 204 container.firstChild.firstChild.className = 205 STATS_GRAPH_CONTAINER_HEADING_CLASS; 206 container.firstChild.firstChild.textContent = 207 'Stats graphs for ' + report.id; 208 209 if (report.type == 'ssrc') { 210 var ssrcInfoElement = document.createElement('div'); 211 container.firstChild.appendChild(ssrcInfoElement); 212 ssrcInfoManager.populateSsrcInfo(ssrcInfoElement, 213 GetSsrcFromReport(report)); 214 } 215 } 216 return container; 217 } 218 219 // Creates the container elements holding a timeline graph 220 // and the TimelineGraphView object. 221 function createStatsGraphView( 222 peerConnectionElement, report, statsName) { 223 var topContainer = ensureStatsGraphTopContainer(peerConnectionElement, 224 report); 225 226 var graphViewId = 227 peerConnectionElement.id + '-' + report.id + '-' + statsName; 228 var divId = graphViewId + '-div'; 229 var canvasId = graphViewId + '-canvas'; 230 var container = document.createElement("div"); 231 container.className = 'stats-graph-sub-container'; 232 233 topContainer.appendChild(container); 234 container.innerHTML = '<div>' + statsName + '</div>' + 235 '<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>'; 236 if (statsName == 'bweCompound') { 237 container.insertBefore( 238 createBweCompoundLegend(peerConnectionElement, report.id), 239 $(divId)); 240 } 241 return new TimelineGraphView(divId, canvasId); 242 } 243 244 // Creates the legend section for the bweCompound graph. 245 // Returns the legend element. 246 function createBweCompoundLegend(peerConnectionElement, reportId) { 247 var legend = document.createElement('div'); 248 for (var prop in bweCompoundGraphConfig) { 249 var div = document.createElement('div'); 250 legend.appendChild(div); 251 div.innerHTML = '<input type=checkbox checked></input>' + prop; 252 div.style.color = bweCompoundGraphConfig[prop].color; 253 div.dataSeriesId = reportId + '-' + prop; 254 div.graphViewId = 255 peerConnectionElement.id + '-' + reportId + '-bweCompound'; 256 div.firstChild.addEventListener('click', function(event) { 257 var target = 258 peerConnectionDataStore[peerConnectionElement.id].getDataSeries( 259 event.target.parentNode.dataSeriesId); 260 target.show(event.target.checked); 261 graphViews[event.target.parentNode.graphViewId].repaint(); 262 }); 263 } 264 return legend; 265 } 266