blob: 36cff04e931412392d0dd62bae3df39a8da4937d [file] [log] [blame]
// Copyright (c) 2019 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.
class CalculatedMetric {
constructor(insertAtOriginalMetricName, name, value) {
this.insertAtOriginalMetricName = insertAtOriginalMetricName;
this.name = name;
this.value = value;
}
}
class StatsReport {
constructor() {
// Represents an RTCStatsReport. It is a Map RTCStats.id -> RTCStats.
// https://w3c.github.io/webrtc-pc/#dom-rtcstatsreport
this.statsById = new Map();
this.calculatedStatsById = new Map();
}
// |internalReports| is an array, each element represents an RTCStats object,
// but the format is a little different from the spec. This is the format:
// {
// id: "string",
// type: "string",
// stats: {
// timestamp: <milliseconds>,
// values: ["member1", value1, "member2", value2...]
// }
// }
static fromInternalsReportList(internalReports) {
const result = new StatsReport();
internalReports.forEach(internalReport => {
if (!internalReport.stats || !internalReport.stats.values) {
return; // continue;
}
const stats = {
id: internalReport.id,
type: internalReport.type,
timestamp: internalReport.stats.timestamp / 1000.0 // ms -> s
};
const values = internalReport.stats.values;
for (let i = 0; i < values.length; i += 2) {
// Metric "name: value".
stats[values[i]] = values[i + 1];
}
result.statsById.set(stats.id, stats);
});
return result;
}
toInternalsReportList() {
const result = [];
for (let stats of this.statsById.values()) {
const internalReport = {
id: stats.id,
type: stats.type,
stats: {
timestamp: stats.timestamp * 1000.0, // s -> ms
values: []
}
};
Object.keys(stats).forEach(metricName => {
if (metricName == 'id' || metricName == 'type' ||
metricName == 'timestamp') {
return; // continue;
}
internalReport.stats.values.push(metricName);
internalReport.stats.values.push(stats[metricName]);
const calculatedMetric =
this.getCalculatedMetricByOriginalName(stats.id, metricName);
if (calculatedMetric) {
internalReport.stats.values.push(calculatedMetric.name);
// Treat calculated metrics that are undefined as 0 to ensure graphs
// can be created anyway.
internalReport.stats.values.push(
calculatedMetric.value ? calculatedMetric.value : 0);
}
});
result.push(internalReport);
}
return result;
}
toString() {
let str = '';
for (let stats of this.statsById.values()) {
if (str != '') {
str += ',';
}
str += JSON.stringify(stats);
}
let str2 = '';
for (let stats of this.calculatedStatsById.values()) {
if (str2 != '') {
str2 += ',';
}
str2 += JSON.stringify(stats);
}
return '[original:' + str + '],calculated:[' + str2 + ']';
}
get(id) {
return this.statsById.get(id);
}
getByType(type) {
const result = [];
for (let stats of this.statsById.values()) {
if (stats.type == type) {
result.push(stats);
}
}
return result;
}
setCalculatedMetric(id, insertAtOriginalMetricName, name, value) {
let calculatedStats = this.calculatedStatsById.get(id);
if (!calculatedStats) {
calculatedStats = {};
this.calculatedStatsById.set(id, calculatedStats);
}
calculatedStats[insertAtOriginalMetricName] =
new CalculatedMetric(insertAtOriginalMetricName, name, value);
}
getCalculatedMetricByOriginalName(id, originalMetricName) {
const calculatedStats = this.calculatedStatsById.get(id);
return calculatedStats ? calculatedStats[originalMetricName] : undefined;
}
}
class StatsRatesCalculator {
constructor() {
this.previousReport = null;
this.currentReport = null;
}
addStatsReport(report) {
this.previousReport = this.currentReport;
this.currentReport = report;
this.updateCalculatedMetrics_();
}
// Updates all "calculated metrics", which are metrics derived from standard
// values, such as converting total counters (e.g. bytesSent) to rates (e.g.
// bytesSent/s).
updateCalculatedMetrics_() {
const calculatedMetrics = [
{
type: 'data-channel',
condition: null,
names: [
['messagesSent', 'timestamp'],
['messagesReceived', 'timestamp'],
['bytesSent', 'timestamp'],
['bytesReceived', 'timestamp'],
],
},
{
type: 'track',
condition: null,
names: [
['framesSent', 'timestamp'],
['framesReceived', 'timestamp'],
[
'totalAudioEnergy',
'totalSamplesDuration',
'[Audio_Level_in_RMS]',
(value) => {
// Calculated according to:
// https://w3c.github.io/webrtc-stats/#dom-rtcaudiohandlerstats-totalaudioenergy
return Math.sqrt(value);
},
],
[
'jitterBufferDelay',
'jitterBufferEmittedCount',
'[jitterBufferDelay/jitterBufferEmittedCount_in_ms]',
(value) => {
return value * 1000; // s -> ms
},
],
],
},
{
type: 'outbound-rtp',
condition: null,
names: [
['bytesSent', 'timestamp'],
['packetsSent', 'timestamp'],
[
'totalPacketSendDelay',
'packetsSent',
'[totalPacketSendDelay/packetsSent_in_ms]',
(value) => {
return value * 1000; // s -> ms
},
],
['framesEncoded', 'timestamp'],
[
'totalEncodedBytesTarget', 'framesEncoded',
'[targetEncodedBytes/s]',
(value, currentStats, previousStats) => {
if (!previousStats) {
return 0;
}
const deltaTime =
currentStats.timestamp - previousStats.timestamp;
const deltaFrames =
currentStats.framesEncoded - previousStats.framesEncoded;
const encodedFrameRate = deltaFrames / deltaTime;
return value * encodedFrameRate;
}
],
[
'totalEncodeTime', 'framesEncoded',
'[totalEncodeTime/framesEncoded_in_ms]',
(value) => {
return value * 1000; // s -> ms
}
],
['qpSum', 'framesEncoded'],
],
},
{
type: 'inbound-rtp',
condition: null,
names: [
['bytesReceived', 'timestamp'],
['packetsReceived', 'timestamp'],
['framesDecoded', 'timestamp'],
['qpSum', 'framesDecoded'],
],
},
{
type: 'transport',
condition: null,
names: [
['bytesSent', 'timestamp'], ['bytesReceived', 'timestamp'],
// TODO(https://crbug.com/webrtc/10568): Add packetsSent and
// packetsReceived once implemented.
],
},
{
type: 'candidate-pair',
condition: null,
names: [
['bytesSent', 'timestamp'],
['bytesReceived', 'timestamp'],
// TODO(https://crbug.com/webrtc/10569): Add packetsSent and
// packetsReceived once implemented.
['requestsSent', 'timestamp'],
['requestsReceived', 'timestamp'],
['responsesSent', 'timestamp'],
['responsesReceived', 'timestamp'],
['consentRequestsSent', 'timestamp'],
['consentRequestsReceived', 'timestamp'],
[
'totalRoundTripTime',
'responsesReceived',
'[totalRoundTripTime/responsesReceived_in_ms]',
(value) => {
return value * 1000; // s -> ms
},
],
],
},
];
calculatedMetrics.forEach(calculatedMetric => {
calculatedMetric.names.forEach(
([accumulativeMetric, samplesMetric, resultName, transformation]) => {
this.currentReport.getByType(calculatedMetric.type)
.forEach(stats => {
if (!calculatedMetric.condition ||
calculatedMetric.condition(stats)) {
if (!resultName) {
resultName = (samplesMetric == 'timestamp') ?
'[' + accumulativeMetric + '/s]' :
'[' + accumulativeMetric + '/' + samplesMetric + ']';
}
let result = this.calculateAccumulativeMetricOverSamples_(
stats.id, accumulativeMetric, samplesMetric);
if (result && transformation) {
const previousStats = this.previousReport.get(stats.id);
result = transformation(result, stats, previousStats);
}
this.currentReport.setCalculatedMetric(
stats.id, accumulativeMetric, resultName, result);
}
});
});
});
}
// Calculates the rate "delta accumulative / delta samples" and returns it. If
// a rate cannot be calculated, such as the metric is missing in the current
// or previous report, undefined is returned.
calculateAccumulativeMetricOverSamples_(
id, accumulativeMetric, samplesMetric) {
if (!this.previousReport || !this.currentReport) {
return undefined;
}
const previousStats = this.previousReport.get(id);
const currentStats = this.currentReport.get(id);
if (!previousStats || !currentStats) {
return undefined;
}
const deltaTime = currentStats.timestamp - previousStats.timestamp;
if (deltaTime <= 0) {
return undefined;
}
// Try to convert whatever the values are to numbers. This gets around the
// fact that some types that are not supported by base::Value (e.g. uint32,
// int64, uint64 and double) are passed as strings.
const previousValue = Number(previousStats[accumulativeMetric]);
const currentValue = Number(currentStats[accumulativeMetric]);
if (typeof previousValue != 'number' || typeof currentValue != 'number') {
return undefined;
}
const previousSamples = Number(previousStats[samplesMetric]);
const currentSamples = Number(currentStats[samplesMetric]);
if (typeof previousSamples != 'number' ||
typeof currentSamples != 'number') {
return undefined;
}
const deltaValue = currentValue - previousValue;
const deltaSamples = currentSamples - previousSamples;
return deltaValue / deltaSamples;
}
}