summaryrefslogtreecommitdiffstats
path: root/toolkit/content/aboutwebrtc/graphdb.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/aboutwebrtc/graphdb.mjs')
-rw-r--r--toolkit/content/aboutwebrtc/graphdb.mjs208
1 files changed, 208 insertions, 0 deletions
diff --git a/toolkit/content/aboutwebrtc/graphdb.mjs b/toolkit/content/aboutwebrtc/graphdb.mjs
new file mode 100644
index 0000000000..c559921113
--- /dev/null
+++ b/toolkit/content/aboutwebrtc/graphdb.mjs
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const CHECK_RTC_STATS_COLLECTION = [
+ "inboundRtpStreamStats",
+ "outboundRtpStreamStats",
+ "remoteInboundRtpStreamStats",
+ "remoteOutboundRtpStreamStats",
+];
+
+const DEFAULT_PROPS = {
+ avgPoints: 10,
+ histSecs: 15,
+ toRate: false,
+ noAvg: false,
+ fixedPointDecimals: 2,
+ toHuman: false,
+};
+
+const REMOTE_RTP_PROPS = "avgPoints=2;histSecs=90";
+const GRAPH_KEYS = [
+ "inbound-rtp.framesPerSecond;noAvg",
+ "inbound-rtp.packetsReceived;toRate",
+ "inbound-rtp.packetsLost;toRate",
+ "inbound-rtp.jitter;fixedPointDecimals=4",
+ `remote-inbound-rtp.roundTripTime;${REMOTE_RTP_PROPS}`,
+ `remote-inbound-rtp.packetsReceived;toRate;${REMOTE_RTP_PROPS}`,
+ "outbound-rtp.packetsSent;toRate",
+ "outbound-rtp.nackCount",
+ "outbound-rtp.pliCount",
+ "outbound-rtp.firCount",
+ `remote-outbound-rtp.bytesSent;toHuman;toRate;${REMOTE_RTP_PROPS}`,
+ `remote-outbound-rtp.packetsSent;toRate;${REMOTE_RTP_PROPS}`,
+]
+ .map(k => k.split(".", 2))
+ .reduce((mapOfArr, [k, rest]) => {
+ mapOfArr[k] ??= [];
+ const [subKey, ...conf] = rest.split(";");
+ let config = conf.reduce((c, v) => {
+ let [configName, ...configVal] = v.split("=", 2);
+ c[configName] = !configVal.length ? true : configVal[0];
+ return c;
+ }, {});
+ mapOfArr[k].push({ subKey, config });
+ return mapOfArr;
+ }, {});
+
+// Sliding window iterator of size n (where: n >= 1) over the array.
+// Only returns full windows.
+// Returns [] if n > array.length.
+// eachN(['a','b','c','d','e'], 3) will yield the following values:
+// ['a','b','c'], ['b','c','d'], and ['c','d','e']
+const eachN = (array, n) => {
+ return {
+ // Index state
+ index: 0,
+ // Iteration function
+ next() {
+ let slice = array.slice(this.index, this.index + n);
+ this.index++;
+ // Done is true _AFTER_ the last value has returned.
+ // When done is true, value is ignored.
+ return { value: slice, done: slice.length < n };
+ },
+ [Symbol.iterator]() {
+ return this;
+ },
+ };
+};
+
+const msToSec = ms => 1000 * ms;
+
+//
+// A subset of the graph data
+//
+class GraphDataSet {
+ constructor(dataPoints) {
+ this.dataPoints = dataPoints;
+ }
+
+ // The latest
+ latest = () => (this.dataPoints ? this.dataPoints.slice(-1)[0] : undefined);
+
+ earliest = () => (this.dataPoints ? this.dataPoints[0] : undefined);
+
+ // The returns the earliest time to graph
+ startTime = () => (this.earliest() || { time: 0 }).time;
+
+ // Returns the latest time to graph
+ stopTime = () => (this.latest() || { time: 0 }).time;
+
+ // Elapsed time within the display window
+ elapsed = () =>
+ this.dataPoints ? this.latest().time - this.earliest().time : 0;
+
+ // Return a new data set that has been filtered
+ filter = fn => new GraphDataSet([...this.dataPoints].filter(fn));
+
+ // The range of values in the set or or undefined if the set is empty
+ dataRange = () =>
+ this.dataPoints.reduce(
+ ({ min, max }, { value }) => ({
+ min: Math.min(min, value),
+ max: Math.max(max, value),
+ }),
+ this.dataPoints.length
+ ? { min: this.dataPoints[0].value, max: this.dataPoints[0].value }
+ : undefined
+ );
+
+ // Get the rates between points. By definition the rates will have
+ // one fewer data points.
+ toRateDataSet = () =>
+ new GraphDataSet(
+ [...eachN(this.dataPoints, 2)].map(([a, b]) => ({
+ // Time mid point
+ time: (b.time + a.time) / 2,
+ value: msToSec(b.value - a.value) / (b.time - a.time),
+ }))
+ );
+
+ average = samples =>
+ samples.reduce(
+ ({ time, value }, { time: t, value: v }) => ({
+ time: time + t / samples.length,
+ value: value + v / samples.length,
+ }),
+ { time: 0, value: 0 }
+ );
+
+ toRollingAverageDataSet = sampleSize =>
+ new GraphDataSet([...eachN(this.dataPoints, sampleSize)].map(this.average));
+}
+
+class GraphData {
+ constructor(id, key, subKey, config) {
+ this.id = id;
+ this.key = key;
+ this.subKey = subKey;
+ this.data = [];
+ this.config = Object.assign({}, DEFAULT_PROPS, config);
+ }
+
+ setValueForTime(dataPoint) {
+ this.data = this.data.filter(({ time: t }) => t != dataPoint.time);
+ this.data.push(dataPoint);
+ }
+
+ getValuesSince = time => this.data.filter(dp => dp.time > time);
+
+ getDataSetSince = time =>
+ new GraphDataSet(this.data.filter(dp => dp.time > time));
+
+ getConfig = () => this.config;
+
+ // Cull old data, but keep twice the window size for average computation
+ cullData = timeNow =>
+ (this.data = this.data.filter(
+ ({ time }) => time + msToSec(this.config.histSecs * 2) > timeNow
+ ));
+}
+
+class GraphDb {
+ constructor(report) {
+ this.graphDatas = new Map();
+ this.insertReportData(report);
+ }
+
+ mkStoreKey = ({ id, key, subKey }) => `${key}.${id}.${subKey}`;
+
+ insertDataPoint(id, key, subKey, config, time, value) {
+ let storeKey = this.mkStoreKey({ id, key, subKey });
+ let data =
+ this.graphDatas.get(storeKey) || new GraphData(id, key, subKey, config);
+ data.setValueForTime({ time, value });
+ data.cullData(time);
+ this.graphDatas.set(storeKey, data);
+ }
+
+ insertReportData(report) {
+ if (report.timestamp == this.lastReportTimestamp) {
+ return;
+ }
+ this.lastReportTimestamp = report.timestamp;
+ CHECK_RTC_STATS_COLLECTION.forEach(listName => {
+ (report[listName] || []).forEach(stats => {
+ (GRAPH_KEYS[stats.type] || []).forEach(({ subKey, config }) => {
+ if (stats[subKey] !== undefined) {
+ this.insertDataPoint(
+ stats.id,
+ stats.type,
+ subKey,
+ config,
+ stats.timestamp,
+ stats[subKey]
+ );
+ }
+ });
+ });
+ });
+ }
+
+ getGraphDataById = id =>
+ [...this.graphDatas.values()].filter(gd => gd.id == id);
+}
+
+export { GraphDb };