Home | History | Annotate | Download | only in media
      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