blob: ad479dcdc559b18950b6135380237e06dbd6b2d5 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file contains helper methods to draw the stats timeline graphs.
// Each graph represents a series of stats report for a PeerConnection,
// e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent
// for ssrc-abcd123 of PeerConnection 0 in process 1234.
// The graphs are drawn as CANVAS, grouped per report type per PeerConnection.
// Each group has an expand/collapse button and is collapsed initially.
//
<include src="timeline_graph_view.js"/>
var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
var RECEIVED_PROPAGATION_DELTA_LABEL =
'googReceivedPacketGroupPropagationDeltaDebug';
var RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL =
'googReceivedPacketGroupArrivalTimeDebug';
// Specifies which stats should be drawn on the 'bweCompound' graph and how.
var bweCompoundGraphConfig = {
googAvailableSendBandwidth: {color: 'red'},
googTargetEncBitrateCorrected: {color: 'purple'},
googActualEncBitrate: {color: 'orange'},
googRetransmitBitrate: {color: 'blue'},
googTransmitBitrate: {color: 'green'},
};
// Converts the last entry of |srcDataSeries| from the total amount to the
// amount per second.
var totalToPerSecond = function(srcDataSeries) {
var length = srcDataSeries.dataPoints_.length;
if (length >= 2) {
var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2];
return (lastDataPoint.value - secondLastDataPoint.value) * 1000 /
(lastDataPoint.time - secondLastDataPoint.time);
}
return 0;
};
// Converts the value of total bytes to bits per second.
var totalBytesToBitsPerSecond = function(srcDataSeries) {
return totalToPerSecond(srcDataSeries) * 8;
};
// Specifies which stats should be converted before drawn and how.
// |convertedName| is the name of the converted value, |convertFunction|
// is the function used to calculate the new converted value based on the
// original dataSeries.
var dataConversionConfig = {
packetsSent: {
convertedName: 'packetsSentPerSecond',
convertFunction: totalToPerSecond,
},
bytesSent: {
convertedName: 'bitsSentPerSecond',
convertFunction: totalBytesToBitsPerSecond,
},
packetsReceived: {
convertedName: 'packetsReceivedPerSecond',
convertFunction: totalToPerSecond,
},
bytesReceived: {
convertedName: 'bitsReceivedPerSecond',
convertFunction: totalBytesToBitsPerSecond,
},
// This is due to a bug of wrong units reported for googTargetEncBitrate.
// TODO (jiayl): remove this when the unit bug is fixed.
googTargetEncBitrate: {
convertedName: 'googTargetEncBitrateCorrected',
convertFunction: function (srcDataSeries) {
var length = srcDataSeries.dataPoints_.length;
var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
if (lastDataPoint.value < 5000)
return lastDataPoint.value * 1000;
return lastDataPoint.value;
}
}
};
// The object contains the stats names that should not be added to the graph,
// even if they are numbers.
var statsNameBlackList = {
'ssrc': true,
'googTrackId': true,
'googComponent': true,
'googLocalAddress': true,
'googRemoteAddress': true,
'googFingerprint': true,
};
var graphViews = {};
// Returns number parsed from |value|, or NaN if the stats name is black-listed.
function getNumberFromValue(name, value) {
if (statsNameBlackList[name])
return NaN;
return parseFloat(value);
}
// Adds the stats report |report| to the timeline graph for the given
// |peerConnectionElement|.
function drawSingleReport(peerConnectionElement, report) {
var reportType = report.type;
var reportId = report.id;
var stats = report.stats;
if (!stats || !stats.values)
return;
for (var i = 0; i < stats.values.length - 1; i = i + 2) {
var rawLabel = stats.values[i];
// Propagation deltas are handled separately.
if (rawLabel == RECEIVED_PROPAGATION_DELTA_LABEL) {
drawReceivedPropagationDelta(
peerConnectionElement, report, stats.values[i + 1]);
continue;
}
var rawDataSeriesId = reportId + '-' + rawLabel;
var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]);
if (isNaN(rawValue)) {
// We do not draw non-numerical values, but still want to record it in the
// data series.
addDataSeriesPoints(peerConnectionElement,
rawDataSeriesId,
rawLabel,
[stats.timestamp],
[stats.values[i + 1]]);
continue;
}
var finalDataSeriesId = rawDataSeriesId;
var finalLabel = rawLabel;
var finalValue = rawValue;
// We need to convert the value if dataConversionConfig[rawLabel] exists.
if (dataConversionConfig[rawLabel]) {
// Updates the original dataSeries before the conversion.
addDataSeriesPoints(peerConnectionElement,
rawDataSeriesId,
rawLabel,
[stats.timestamp],
[rawValue]);
// Convert to another value to draw on graph, using the original
// dataSeries as input.
finalValue = dataConversionConfig[rawLabel].convertFunction(
peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
rawDataSeriesId));
finalLabel = dataConversionConfig[rawLabel].convertedName;
finalDataSeriesId = reportId + '-' + finalLabel;
}
// Updates the final dataSeries to draw.
addDataSeriesPoints(peerConnectionElement,
finalDataSeriesId,
finalLabel,
[stats.timestamp],
[finalValue]);
// Updates the graph.
var graphType = bweCompoundGraphConfig[finalLabel] ?
'bweCompound' : finalLabel;
var graphViewId =
peerConnectionElement.id + '-' + reportId + '-' + graphType;
if (!graphViews[graphViewId]) {
graphViews[graphViewId] = createStatsGraphView(peerConnectionElement,
report,
graphType);
var date = new Date(stats.timestamp);
graphViews[graphViewId].setDateRange(date, date);
}
// Adds the new dataSeries to the graphView. We have to do it here to cover
// both the simple and compound graph cases.
var dataSeries =
peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
finalDataSeriesId);
if (!graphViews[graphViewId].hasDataSeries(dataSeries))
graphViews[graphViewId].addDataSeries(dataSeries);
graphViews[graphViewId].updateEndDate();
}
}
// Makes sure the TimelineDataSeries with id |dataSeriesId| is created,
// and adds the new data points to it. |times| is the list of timestamps for
// each data point, and |values| is the list of the data point values.
function addDataSeriesPoints(
peerConnectionElement, dataSeriesId, label, times, values) {
var dataSeries =
peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
dataSeriesId);
if (!dataSeries) {
dataSeries = new TimelineDataSeries();
peerConnectionDataStore[peerConnectionElement.id].setDataSeries(
dataSeriesId, dataSeries);
if (bweCompoundGraphConfig[label]) {
dataSeries.setColor(bweCompoundGraphConfig[label].color);
}
}
for (var i = 0; i < times.length; ++i)
dataSeries.addPoint(times[i], values[i]);
}
// Draws the received propagation deltas using the packet group arrival time as
// the x-axis. For example, |report.stats.values| should be like
// ['googReceivedPacketGroupArrivalTimeDebug', '[123456, 234455, 344566]',
// 'googReceivedPacketGroupPropagationDeltaDebug', '[23, 45, 56]', ...].
function drawReceivedPropagationDelta(peerConnectionElement, report, deltas) {
var reportId = report.id;
var stats = report.stats;
var times = null;
// Find the packet group arrival times.
for (var i = 0; i < stats.values.length - 1; i = i + 2) {
if (stats.values[i] == RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL) {
times = stats.values[i + 1];
break;
}
}
// Unexpected.
if (times == null)
return;
// Convert |deltas| and |times| from strings to arrays of numbers.
try {
deltas = JSON.parse(deltas);
times = JSON.parse(times);
} catch (e) {
console.log(e);
return;
}
// Update the data series.
var dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL;
addDataSeriesPoints(
peerConnectionElement,
dataSeriesId,
RECEIVED_PROPAGATION_DELTA_LABEL,
times,
deltas);
// Update the graph.
var graphViewId = peerConnectionElement.id + '-' + reportId + '-' +
RECEIVED_PROPAGATION_DELTA_LABEL;
var date = new Date(times[times.length - 1]);
if (!graphViews[graphViewId]) {
graphViews[graphViewId] = createStatsGraphView(
peerConnectionElement,
report,
RECEIVED_PROPAGATION_DELTA_LABEL);
graphViews[graphViewId].setScale(10);
graphViews[graphViewId].setDateRange(date, date);
var dataSeries = peerConnectionDataStore[peerConnectionElement.id]
.getDataSeries(dataSeriesId);
graphViews[graphViewId].addDataSeries(dataSeries);
}
graphViews[graphViewId].updateEndDate(date);
}
// Ensures a div container to hold all stats graphs for one track is created as
// a child of |peerConnectionElement|.
function ensureStatsGraphTopContainer(peerConnectionElement, report) {
var containerId = peerConnectionElement.id + '-' +
report.type + '-' + report.id + '-graph-container';
var container = $(containerId);
if (!container) {
container = document.createElement('details');
container.id = containerId;
container.className = 'stats-graph-container';
peerConnectionElement.appendChild(container);
container.innerHTML ='<summary><span></span></summary>';
container.firstChild.firstChild.className =
STATS_GRAPH_CONTAINER_HEADING_CLASS;
container.firstChild.firstChild.textContent =
'Stats graphs for ' + report.id;
if (report.type == 'ssrc') {
var ssrcInfoElement = document.createElement('div');
container.firstChild.appendChild(ssrcInfoElement);
ssrcInfoManager.populateSsrcInfo(ssrcInfoElement,
GetSsrcFromReport(report));
}
}
return container;
}
// Creates the container elements holding a timeline graph
// and the TimelineGraphView object.
function createStatsGraphView(
peerConnectionElement, report, statsName) {
var topContainer = ensureStatsGraphTopContainer(peerConnectionElement,
report);
var graphViewId =
peerConnectionElement.id + '-' + report.id + '-' + statsName;
var divId = graphViewId + '-div';
var canvasId = graphViewId + '-canvas';
var container = document.createElement("div");
container.className = 'stats-graph-sub-container';
topContainer.appendChild(container);
container.innerHTML = '<div>' + statsName + '</div>' +
'<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>';
if (statsName == 'bweCompound') {
container.insertBefore(
createBweCompoundLegend(peerConnectionElement, report.id),
$(divId));
}
return new TimelineGraphView(divId, canvasId);
}
// Creates the legend section for the bweCompound graph.
// Returns the legend element.
function createBweCompoundLegend(peerConnectionElement, reportId) {
var legend = document.createElement('div');
for (var prop in bweCompoundGraphConfig) {
var div = document.createElement('div');
legend.appendChild(div);
div.innerHTML = '<input type=checkbox checked></input>' + prop;
div.style.color = bweCompoundGraphConfig[prop].color;
div.dataSeriesId = reportId + '-' + prop;
div.graphViewId =
peerConnectionElement.id + '-' + reportId + '-bweCompound';
div.firstChild.addEventListener('click', function(event) {
var target =
peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
event.target.parentNode.dataSeriesId);
target.show(event.target.checked);
graphViews[event.target.parentNode.graphViewId].repaint();
});
}
return legend;
}