609 lines
22 KiB
JavaScript
609 lines
22 KiB
JavaScript
// Derived from Chromium WebRTC Internals Dashboard - see Acknowledgements for full license details
|
||
|
||
const CalculatorModifier = Object.freeze({
|
||
kNone: Object.freeze({postfix: '', multiplier: 1}),
|
||
kMillisecondsFromSeconds:
|
||
Object.freeze({postfix: '_in_ms', multiplier: 1000}),
|
||
kBytesToBits: Object.freeze({bitrate: true, multiplier: 8}),
|
||
});
|
||
|
||
class Metric {
|
||
constructor(name, value) {
|
||
this.name = name;
|
||
this.value = value;
|
||
}
|
||
|
||
toString() {
|
||
return '{"' + this.name + '":"' + this.value + '"}';
|
||
}
|
||
}
|
||
|
||
// Represents a companion dictionary to an RTCStats object of an RTCStatsReport.
|
||
// The CalculatedStats object contains additional metrics associated with the
|
||
// original RTCStats object. Typically, the RTCStats object contains
|
||
// accumulative counters, but in chrome://webrc-internals/ we also want graphs
|
||
// for the average rate over the last second, so we have CalculatedStats
|
||
// containing calculated Metrics.
|
||
class CalculatedStats {
|
||
constructor(id) {
|
||
this.id = id;
|
||
// A map Original Name -> Array of Metrics, where Original Name refers to
|
||
// the name of the metric in the original RTCStats object, and the Metrics
|
||
// are calculated metrics. For example, if the original RTCStats report
|
||
// contains framesReceived, and from that we've calculated
|
||
// [framesReceived/s] and [framesReceived-framesDecoded], then there will be
|
||
// a mapping from "framesReceived" to an array of two Metric objects,
|
||
// "[framesReceived/s]" and "[framesReceived-framesDecoded]".
|
||
this.calculatedMetricsByOriginalName = new Map();
|
||
}
|
||
|
||
addCalculatedMetric(originalName, metric) {
|
||
let calculatedMetrics =
|
||
this.calculatedMetricsByOriginalName.get(originalName);
|
||
if (!calculatedMetrics) {
|
||
calculatedMetrics = [];
|
||
this.calculatedMetricsByOriginalName.set(originalName, calculatedMetrics);
|
||
}
|
||
calculatedMetrics.push(metric);
|
||
}
|
||
|
||
// Gets the calculated metrics associated with |originalName| in the order
|
||
// that they were added, or an empty list if there are no associated metrics.
|
||
getCalculatedMetrics(originalName) {
|
||
const calculatedMetrics =
|
||
this.calculatedMetricsByOriginalName.get(originalName);
|
||
if (!calculatedMetrics) {
|
||
return [];
|
||
}
|
||
return calculatedMetrics;
|
||
}
|
||
|
||
toString() {
|
||
let str = '{id:"' + this.id + '"';
|
||
for (const originalName of this.calculatedMetricsByOriginalName.keys()) {
|
||
const calculatedMetrics =
|
||
this.calculatedMetricsByOriginalName.get(originalName);
|
||
str += ',' + originalName + ':[';
|
||
for (let i = 0; i < calculatedMetrics.length; i++) {
|
||
str += calculatedMetrics[i].toString();
|
||
if (i + 1 < calculatedMetrics.length) {
|
||
str += ',';
|
||
}
|
||
str += ']';
|
||
}
|
||
}
|
||
str += '}';
|
||
return str;
|
||
}
|
||
}
|
||
|
||
// Contains the metrics of an RTCStatsReport, as well as calculated metrics
|
||
// associated with metrics from the original report. Convertible to and from the
|
||
// "internal reports" format used by webrtc_internals.js to pass stats from C++
|
||
// to JavaScript.
|
||
export 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();
|
||
// RTCStats.id -> CalculatedStats
|
||
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 (const 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 calculatedMetrics =
|
||
this.getCalculatedMetrics(stats.id, metricName);
|
||
calculatedMetrics.forEach(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 (const stats of this.statsById.values()) {
|
||
if (str !== '') {
|
||
str += ',';
|
||
}
|
||
str += JSON.stringify(stats);
|
||
}
|
||
let str2 = '';
|
||
for (const stats of this.calculatedStatsById.values()) {
|
||
if (str2 !== '') {
|
||
str2 += ',';
|
||
}
|
||
str2 += stats.toString();
|
||
}
|
||
return '[original:' + str + '],calculated:[' + str2 + ']';
|
||
}
|
||
|
||
get(id) {
|
||
return this.statsById.get(id);
|
||
}
|
||
|
||
getByType(type) {
|
||
const result = [];
|
||
for (const stats of this.statsById.values()) {
|
||
if (stats.type === type) {
|
||
result.push(stats);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
addCalculatedMetric(id, insertAtOriginalMetricName, name, value) {
|
||
let calculatedStats = this.calculatedStatsById.get(id);
|
||
if (!calculatedStats) {
|
||
calculatedStats = new CalculatedStats(id);
|
||
this.calculatedStatsById.set(id, calculatedStats);
|
||
}
|
||
calculatedStats.addCalculatedMetric(
|
||
insertAtOriginalMetricName, new Metric(name, value));
|
||
}
|
||
|
||
getCalculatedMetrics(id, originalMetricName) {
|
||
const calculatedStats = this.calculatedStatsById.get(id);
|
||
return calculatedStats ?
|
||
calculatedStats.getCalculatedMetrics(originalMetricName) :
|
||
[];
|
||
}
|
||
}
|
||
|
||
// Shows a `DOMHighResTimeStamp` as a human readable date time.
|
||
// The metric must be a time value in milliseconds with Unix epoch as time
|
||
// origin.
|
||
class DateCalculator {
|
||
constructor(metric) {
|
||
this.metric = metric;
|
||
}
|
||
getCalculatedMetricName() {
|
||
return '[' + this.metric + ']';
|
||
}
|
||
calculate(id, previousReport, currentReport) {
|
||
const timestamp = currentReport.get(id)[this.metric];
|
||
const date = new Date(timestamp);
|
||
return date.toLocaleString();
|
||
}
|
||
}
|
||
|
||
// 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.
|
||
class RateCalculator {
|
||
constructor(
|
||
accumulativeMetric, samplesMetric, modifier = CalculatorModifier.kNone) {
|
||
this.accumulativeMetric = accumulativeMetric;
|
||
this.samplesMetric = samplesMetric;
|
||
this.modifier = modifier;
|
||
}
|
||
|
||
getCalculatedMetricName() {
|
||
const accumulativeMetric = this.modifier.bitrate ?
|
||
this.accumulativeMetric + '_in_bits' :
|
||
this.accumulativeMetric;
|
||
if (this.samplesMetric === 'timestamp') {
|
||
return '[' + accumulativeMetric + '/s]';
|
||
}
|
||
return '[' + accumulativeMetric + '/' + this.samplesMetric +
|
||
this.modifier.postfix + ']';
|
||
}
|
||
|
||
calculate(id, previousReport, currentReport) {
|
||
return RateCalculator.calculateRate(
|
||
id, previousReport, currentReport, this.accumulativeMetric,
|
||
this.samplesMetric) *
|
||
this.modifier.multiplier;
|
||
}
|
||
|
||
static calculateRate(
|
||
id, previousReport, currentReport, accumulativeMetric, samplesMetric) {
|
||
if (!previousReport || !currentReport) {
|
||
return undefined;
|
||
}
|
||
const previousStats = previousReport.get(id);
|
||
const currentStats = 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;
|
||
}
|
||
}
|
||
|
||
// Looks up codec and payload type from a codecId reference, constructing an
|
||
// informative string about which codec is used.
|
||
class CodecCalculator {
|
||
getCalculatedMetricName() {
|
||
return '[codec]';
|
||
}
|
||
|
||
calculate(id, previousReport, currentReport) {
|
||
const targetStats = currentReport.get(id);
|
||
const codecStats = currentReport.get(targetStats.codecId);
|
||
if (!codecStats) {
|
||
return undefined;
|
||
}
|
||
// If mimeType is 'video/VP8' then codec is 'VP8'.
|
||
const codec =
|
||
codecStats.mimeType.substr(codecStats.mimeType.indexOf('/') + 1);
|
||
|
||
let fmtpLine = '';
|
||
if (codecStats.sdpFmtpLine) {
|
||
fmtpLine = ', ' + codecStats.sdpFmtpLine;
|
||
}
|
||
return codec + ' (' + codecStats.payloadType + fmtpLine + ')';
|
||
}
|
||
}
|
||
|
||
// Calculates "RMS" audio level, which is the average audio level between the
|
||
// previous and current report, in the interval [0,1]. Calculated per:
|
||
// https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-totalaudioenergy
|
||
class AudioLevelRmsCalculator {
|
||
getCalculatedMetricName() {
|
||
return '[Audio_Level_in_RMS]';
|
||
}
|
||
|
||
calculate(id, previousReport, currentReport) {
|
||
const averageAudioLevelSquared = RateCalculator.calculateRate(
|
||
id, previousReport, currentReport, 'totalAudioEnergy',
|
||
'totalSamplesDuration');
|
||
return Math.sqrt(averageAudioLevelSquared);
|
||
}
|
||
}
|
||
|
||
// Calculates "metricA - SUM(otherMetrics)", only looking at the current report.
|
||
class DifferenceCalculator {
|
||
constructor(metricA, ...otherMetrics) {
|
||
this.metricA = metricA;
|
||
this.otherMetrics = otherMetrics;
|
||
}
|
||
|
||
getCalculatedMetricName() {
|
||
return '[' + this.metricA + '-' + this.otherMetrics.join('-') + ']';
|
||
}
|
||
|
||
calculate(id, previousReport, currentReport) {
|
||
const currentStats = currentReport.get(id);
|
||
return parseInt(currentStats[this.metricA], 10)
|
||
- this.otherMetrics.map(metric => parseInt(currentStats[metric], 10))
|
||
.reduce((a, b) => a + b, 0);
|
||
}
|
||
}
|
||
|
||
// Calculates the standard deviation from a totalSquaredSum, totalSum, and
|
||
// totalCount. If the standard deviation cannot be calculated, such as the
|
||
// metric is missing in the current or previous report, undefined is returned.
|
||
class StandardDeviationCalculator {
|
||
constructor(totalSquaredSumMetric, totalSumMetric, totalCount, label) {
|
||
this.totalSquaredSumMetric = totalSquaredSumMetric;
|
||
this.totalSumMetric = totalSumMetric;
|
||
this.totalCount = totalCount;
|
||
this.label = label;
|
||
}
|
||
|
||
getCalculatedMetricName() {
|
||
return '[' + this.label + 'StDev_in_ms]';
|
||
}
|
||
|
||
calculate(id, previousReport, currentReport) {
|
||
return StandardDeviationCalculator.calculateStandardDeviation(
|
||
id, previousReport, currentReport, this.totalSquaredSumMetric,
|
||
this.totalSumMetric, this.totalCount);
|
||
}
|
||
|
||
static calculateStandardDeviation(
|
||
id, previousReport, currentReport, totalSquaredSumMetric, totalSumMetric,
|
||
totalCount) {
|
||
if (!previousReport || !currentReport) {
|
||
return undefined;
|
||
}
|
||
const previousStats = previousReport.get(id);
|
||
const currentStats = currentReport.get(id);
|
||
if (!previousStats || !currentStats) {
|
||
return undefined;
|
||
}
|
||
const deltaCount =
|
||
Number(currentStats[totalCount]) - Number(previousStats[totalCount]);
|
||
if (deltaCount <= 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 previousSquaredSumValue =
|
||
Number(previousStats[totalSquaredSumMetric]);
|
||
const currentSquaredSumValue = Number(currentStats[totalSquaredSumMetric]);
|
||
if (typeof previousSquaredSumValue !== 'number' ||
|
||
typeof currentSquaredSumValue !== 'number') {
|
||
return undefined;
|
||
}
|
||
const previousSumValue = Number(previousStats[totalSumMetric]);
|
||
const currentSumValue = Number(currentStats[totalSumMetric]);
|
||
if (typeof previousSumValue !== 'number' ||
|
||
typeof currentSumValue !== 'number') {
|
||
return undefined;
|
||
}
|
||
|
||
const deltaSquaredSum = currentSquaredSumValue - previousSquaredSumValue;
|
||
const deltaSum = currentSumValue - previousSumValue;
|
||
const variance =
|
||
(deltaSquaredSum - Math.pow(deltaSum, 2) / deltaCount) / deltaCount;
|
||
if (variance < 0) {
|
||
return undefined;
|
||
}
|
||
return 1000 * Math.sqrt(variance);
|
||
}
|
||
}
|
||
|
||
// Keeps track of previous and current stats report and calculates all
|
||
// calculated metrics.
|
||
export class StatsRatesCalculator {
|
||
constructor() {
|
||
this.previousReport = null;
|
||
this.currentReport = null;
|
||
this.statsCalculators = [
|
||
{
|
||
type: 'data-channel',
|
||
metricCalculators: {
|
||
messagesSent: new RateCalculator('messagesSent', 'timestamp'),
|
||
messagesReceived: new RateCalculator('messagesReceived', 'timestamp'),
|
||
bytesSent: new RateCalculator(
|
||
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
bytesReceived: new RateCalculator(
|
||
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
},
|
||
},
|
||
{
|
||
type: 'media-source',
|
||
metricCalculators: {
|
||
totalAudioEnergy: new AudioLevelRmsCalculator(),
|
||
},
|
||
},
|
||
{
|
||
type: 'outbound-rtp',
|
||
metricCalculators: {
|
||
bytesSent: new RateCalculator(
|
||
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
headerBytesSent: new RateCalculator(
|
||
'headerBytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
retransmittedBytesSent: new RateCalculator(
|
||
'retransmittedBytesSent', 'timestamp',
|
||
CalculatorModifier.kBytesToBits),
|
||
packetsSent: new RateCalculator('packetsSent', 'timestamp'),
|
||
retransmittedPacketsSent:
|
||
new RateCalculator('retransmittedPacketsSent', 'timestamp'),
|
||
totalPacketSendDelay: new RateCalculator(
|
||
'totalPacketSendDelay', 'packetsSent',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
framesEncoded: new RateCalculator('framesEncoded', 'timestamp'),
|
||
framesSent: new RateCalculator('framesSent', 'timestamp'),
|
||
totalEncodedBytesTarget: new RateCalculator(
|
||
'totalEncodedBytesTarget', 'timestamp',
|
||
CalculatorModifier.kBytesToBits),
|
||
totalEncodeTime: new RateCalculator(
|
||
'totalEncodeTime', 'framesEncoded',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
qpSum: new RateCalculator('qpSum', 'framesEncoded'),
|
||
codecId: new CodecCalculator(),
|
||
},
|
||
},
|
||
{
|
||
type: 'inbound-rtp',
|
||
metricCalculators: {
|
||
bytesReceived: new RateCalculator(
|
||
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
headerBytesReceived: new RateCalculator(
|
||
'headerBytesReceived', 'timestamp',
|
||
CalculatorModifier.kBytesToBits),
|
||
retransmittedBytesReceived: new RateCalculator(
|
||
'retransmittedBytesReceived', 'timestamp',
|
||
CalculatorModifier.kBytesToBits),
|
||
fecBytesReceived: new RateCalculator(
|
||
'fecBytesReceived', 'timestamp',
|
||
CalculatorModifier.kBytesToBits),
|
||
packetsReceived: new RateCalculator('packetsReceived', 'timestamp'),
|
||
packetsDiscarded: new RateCalculator('packetsDiscarded', 'timestamp'),
|
||
retransmittedPacketsReceived:
|
||
new RateCalculator('retransmittedPacketsReceived', 'timestamp'),
|
||
fecPacketsReceived:
|
||
new RateCalculator('fecPacketsReceived', 'timestamp'),
|
||
fecPacketsDiscarded:
|
||
new RateCalculator('fecPacketsDiscarded', 'timestamp'),
|
||
framesReceived: [
|
||
new RateCalculator('framesReceived', 'timestamp'),
|
||
new DifferenceCalculator('framesReceived',
|
||
'framesDecoded', 'framesDropped'),
|
||
],
|
||
framesDecoded: new RateCalculator('framesDecoded', 'timestamp'),
|
||
keyFramesDecoded: new RateCalculator('keyFramesDecoded', 'timestamp'),
|
||
totalDecodeTime: new RateCalculator(
|
||
'totalDecodeTime', 'framesDecoded',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
totalInterFrameDelay: new RateCalculator(
|
||
'totalInterFrameDelay', 'framesDecoded',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
totalSquaredInterFrameDelay: new StandardDeviationCalculator(
|
||
'totalSquaredInterFrameDelay', 'totalInterFrameDelay',
|
||
'framesDecoded', 'interFrameDelay'),
|
||
totalSamplesReceived:
|
||
new RateCalculator('totalSamplesReceived', 'timestamp'),
|
||
concealedSamples: [
|
||
new RateCalculator('concealedSamples', 'timestamp'),
|
||
new RateCalculator('concealedSamples', 'totalSamplesReceived'),
|
||
],
|
||
silentConcealedSamples:
|
||
new RateCalculator('silentConcealedSamples', 'timestamp'),
|
||
insertedSamplesForDeceleration:
|
||
new RateCalculator('insertedSamplesForDeceleration', 'timestamp'),
|
||
removedSamplesForAcceleration:
|
||
new RateCalculator('removedSamplesForAcceleration', 'timestamp'),
|
||
qpSum: new RateCalculator('qpSum', 'framesDecoded'),
|
||
codecId: new CodecCalculator(),
|
||
totalAudioEnergy: new AudioLevelRmsCalculator(),
|
||
jitterBufferDelay: new RateCalculator(
|
||
'jitterBufferDelay', 'jitterBufferEmittedCount',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
jitterBufferTargetDelay: new RateCalculator(
|
||
'jitterBufferTargetDelay', 'jitterBufferEmittedCount',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
jitterBufferMinimumDelay: new RateCalculator(
|
||
'jitterBufferMinimumDelay', 'jitterBufferEmittedCount',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
lastPacketReceivedTimestamp: new DateCalculator(
|
||
'lastPacketReceivedTimestamp'),
|
||
estimatedPlayoutTimestamp: new DateCalculator(
|
||
'estimatedPlayoutTimestamp'),
|
||
totalProcessingDelay: new RateCalculator(
|
||
'totalProcessingDelay', 'framesDecoded',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
totalAssemblyTime: new RateCalculator(
|
||
'totalAssemblyTime', 'framesAssembledFromMultiplePackets',
|
||
CalculatorModifier.kMillisecondsFromSeconds),
|
||
},
|
||
},
|
||
{
|
||
type: 'remote-outbound-rtp',
|
||
metricCalculators: {
|
||
remoteTimestamp: new DateCalculator('remoteTimestamp'),
|
||
},
|
||
},
|
||
{
|
||
type: 'transport',
|
||
metricCalculators: {
|
||
bytesSent: new RateCalculator(
|
||
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
bytesReceived: new RateCalculator(
|
||
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
packetsSent: new RateCalculator(
|
||
'packetsSent', 'timestamp'),
|
||
packetsReceived: new RateCalculator(
|
||
'packetsReceived', 'timestamp'),
|
||
},
|
||
},
|
||
{
|
||
type: 'candidate-pair',
|
||
metricCalculators: {
|
||
bytesSent: new RateCalculator(
|
||
'bytesSent', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
bytesReceived: new RateCalculator(
|
||
'bytesReceived', 'timestamp', CalculatorModifier.kBytesToBits),
|
||
packetsSent: new RateCalculator(
|
||
'packetsSent', 'timestamp'),
|
||
packetsReceived: new RateCalculator(
|
||
'packetsReceived', 'timestamp'),
|
||
totalRoundTripTime:
|
||
new RateCalculator('totalRoundTripTime', 'responsesReceived'),
|
||
lastPacketReceivedTimestamp: new DateCalculator(
|
||
'lastPacketReceivedTimestamp'),
|
||
lastPacketSentTimestamp: new DateCalculator(
|
||
'lastPacketSentTimestamp'),
|
||
},
|
||
},
|
||
];
|
||
}
|
||
|
||
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_() {
|
||
this.statsCalculators.forEach(statsCalculator => {
|
||
this.currentReport.getByType(statsCalculator.type).forEach(stats => {
|
||
Object.keys(statsCalculator.metricCalculators)
|
||
.forEach(originalMetric => {
|
||
let metricCalculators =
|
||
statsCalculator.metricCalculators[originalMetric];
|
||
if (!Array.isArray(metricCalculators)) {
|
||
metricCalculators = [metricCalculators];
|
||
}
|
||
metricCalculators.forEach(metricCalculator => {
|
||
this.currentReport.addCalculatedMetric(
|
||
stats.id, originalMetric,
|
||
metricCalculator.getCalculatedMetricName(),
|
||
metricCalculator.calculate(
|
||
stats.id, this.previousReport, this.currentReport));
|
||
});
|
||
});
|
||
});
|
||
});
|
||
}
|
||
}
|