Skip to content

Commit 0ea0bb5

Browse files
feat: Optimisation of prev stats storing (#5)
* feat: add emitting of stats parsing time * feat: fix typos * feat: correct ts typechecking * feat: add cleanup of prev stats * feat: reuse task scheduler to clean up parser stats * feat: add logging * feat: add prerelease sm config * chore(release): 1.5.0-optimization-part-2.1 [skip ci] * feat: edit test logs * feat: move common code to base class * feat: update dev deps * feat: remove unnecessary task scheduling from detector * feat: move last stats methods to base class Co-authored-by: vlprojects-bot <[email protected]>
1 parent dc270df commit 0ea0bb5

25 files changed

+338
-143
lines changed

.releaserc

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"branches": [
33
{ "name": "master" },
4+
{ "name": "!(master)", "prerelease": true, "channel": "rc" }
45
],
56
"plugins": [
67
["@semantic-release/commit-analyzer", {

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ const issue = {
136136
```
137137

138138
### NetworkMediaSyncIssueDetector
139-
Detects issues with audio syncronization.
139+
Detects issues with audio synchronization.
140140
```js
141141
const issue = {
142142
type: 'network',
@@ -158,7 +158,7 @@ const issue = {
158158
```
159159

160160
### QualityLimitationsIssueDetector
161-
Detects issues with encoder and outbound network. Based on native qualitiLimitationReason.
161+
Detects issues with encoder and outbound network. Based on native qualityLimitationReason.
162162
```js
163163
const issue = {
164164
type: 'cpu',
@@ -191,7 +191,7 @@ const issue = {
191191

192192
## Roadmap
193193

194-
- [ ] Adaptive getStats() call interval based on last getStats() exectution time
194+
- [ ] Adaptive getStats() call interval based on last getStats() execution time
195195
- [ ] Structured issue debug
196196
- [ ] Issues detector for user devices permissions
197197

package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "webrtc-issue-detector",
3-
"version": "1.4.0",
3+
"version": "1.5.0-optimization-part-2.1",
44
"description": "WebRTC diagnostic tool that detects issues with network or user devices",
55
"main": "dist/bundle-cjs.js",
66
"module": "dist/bundle-esm.js",
@@ -45,17 +45,17 @@
4545
"@semantic-release/github": "^8.0.5",
4646
"@semantic-release/npm": "^9.0.1",
4747
"@semantic-release/release-notes-generator": "^10.0.3",
48-
"@types/chai": "^4.3.3",
48+
"@types/chai": "^4.3.4",
4949
"@types/chai-as-promised": "^7.1.5",
5050
"@types/chai-subset": "^1.3.3",
5151
"@types/faker": "^5.5.9",
52-
"@types/mocha": "^9.1.1",
53-
"@types/node": "12",
52+
"@types/mocha": "^10.0.1",
53+
"@types/node": "14",
5454
"@types/sinon": "^10.0.13",
55-
"@types/sinon-chai": "^3.2.8",
55+
"@types/sinon-chai": "^3.2.9",
5656
"@typescript-eslint/eslint-plugin": "^5.33.0",
5757
"@typescript-eslint/parser": "^5.33.0",
58-
"chai": "^4.3.6",
58+
"chai": "^4.3.7",
5959
"chai-as-promised": "^7.1.1",
6060
"chai-subset": "^1.6.0",
6161
"eslint": "7.32.0",
@@ -69,7 +69,7 @@
6969
"rollup-plugin-bundle-size": "^1.0.3",
7070
"rollup-plugin-polyfill-node": "^0.10.2",
7171
"rollup-plugin-terser": "^7.0.2",
72-
"sinon": "^14.0.0",
72+
"sinon": "^14.0.2",
7373
"sinon-chai": "^3.7.0",
7474
"ts-node": "^10.9.1",
7575
"typescript": "^4.7.4"

src/NetworkScoresCalculator.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@ import {
55
INetworkScoresCalculator,
66
WebRTCStatsParsed,
77
} from './types';
8+
import { scheduleTask } from './utils/tasks';
9+
import { CLEANUP_PREV_STATS_TTL_MS } from './utils/constants';
810

911
class NetworkScoresCalculator implements INetworkScoresCalculator {
1012
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
1113

1214
calculate(data: WebRTCStatsParsed): NetworkScores {
15+
const { connection: { id: connectionId } } = data;
1316
const outbound = this.calculateOutboundScore(data);
1417
const inbound = this.calculateInboundScore(data);
15-
this.#lastProcessedStats[data.connection.id] = data;
18+
this.#lastProcessedStats[connectionId] = data;
19+
20+
scheduleTask({
21+
taskId: connectionId,
22+
delayMs: CLEANUP_PREV_STATS_TTL_MS,
23+
callback: () => (delete this.#lastProcessedStats[connectionId]),
24+
});
25+
1626
return { outbound, inbound };
1727
}
1828

src/WebRTCIssueDetector.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import {
33
CompositeStatsParser,
44
DetectIssuesPayload,
55
EventType,
6+
INetworkScoresCalculator,
67
IssueDetector,
78
IssuePayload,
8-
INetworkScoresCalculator,
9+
Logger,
910
StatsReportItem,
1011
WebRTCIssueDetectorConstructorParams,
1112
WebRTCStatsParsed,
1213
WIDWindow,
13-
Logger,
1414
} from './types';
1515
import PeriodicWebRTCStatsReporter from './parser/PeriodicWebRTCStatsReporter';
1616
import DefaultNetworkScoresCalculator from './NetworkScoresCalculator';
@@ -21,7 +21,8 @@ import {
2121
InboundNetworkIssueDetector,
2222
NetworkMediaSyncIssueDetector,
2323
OutboundNetworkIssueDetector,
24-
QualityLimitationsIssueDetector, VideoCodecMismatchDetector,
24+
QualityLimitationsIssueDetector,
25+
VideoCodecMismatchDetector,
2526
} from './detectors';
2627
import { CompositeRTCStatsParser, RTCStatsParser } from './parser';
2728
import createLogger from './utils/logger';
@@ -84,13 +85,24 @@ class WebRTCIssueDetector {
8485

8586
this.calculateNetworkScores(report.stats);
8687
});
88+
89+
this.statsReporter.on(PeriodicWebRTCStatsReporter.STATS_REPORTS_PARSED, (data: { timeTaken: number }) => {
90+
const payload = {
91+
timeTaken: data.timeTaken,
92+
ts: Date.now(),
93+
};
94+
95+
this.eventEmitter.emit(EventType.StatsParsingFinished, payload);
96+
});
8797
}
8898

8999
public watchNewPeerConnections(): void {
90100
if (this.#running) {
91101
throw new Error('WebRTCIssueDetector is already started');
92102
}
93103

104+
this.logger.info('Start watching peer connections');
105+
94106
this.#running = true;
95107
this.statsReporter.startReporting();
96108
}
@@ -100,6 +112,8 @@ class WebRTCIssueDetector {
100112
throw new Error('WebRTCIssueDetector is already stopped');
101113
}
102114

115+
this.logger.info('Stop watching peer connections');
116+
103117
this.#running = false;
104118
this.statsReporter.stopReporting();
105119
}
@@ -143,6 +157,7 @@ class WebRTCIssueDetector {
143157

144158
private wrapRTCPeerConnection(): void {
145159
if (!window.RTCPeerConnection) {
160+
this.logger.warn('No RTCPeerConnection found in browser window. Skipping');
146161
return;
147162
}
148163

src/WebRTCIssueEmitter.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import {
44
EventPayload,
55
IssueDetectorResult,
66
NetworkScores,
7+
StatsParsingFinishedPayload,
78
} from './types';
89

910
export declare interface WebRTCIssueEmitter {
1011
on(event: EventType.Issue, listener: (payload: IssueDetectorResult) => void): this;
1112
on(event: EventType.NetworkScoresUpdated, listener: (payload: NetworkScores) => void): this;
13+
on(event: EventType.StatsParsingFinished, listener: (payload: StatsParsingFinishedPayload) => void): this;
1214
emit(event: EventType.Issue, payload: EventPayload): boolean;
1315
emit(event: EventType.NetworkScoresUpdated, payload: NetworkScores): boolean;
16+
emit(event: EventType.StatsParsingFinished, payload: StatsParsingFinishedPayload): boolean;
1417
}
1518

1619
export class WebRTCIssueEmitter extends EventEmitter {}

src/detectors/AvailableOutgoingBitrateIssueDetector.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import {
2-
IssueDetector,
32
IssueDetectorResult,
43
IssueReason,
54
IssueType,
65
WebRTCStatsParsed,
76
} from '../types';
7+
import BaseIssueDetector, { BaseIssueDetectorParams } from './BaseIssueDetector';
88

9-
class AvailableOutgoingBitrateIssueDetector implements IssueDetector {
10-
#availableOutgoingBitrateTreshold = 100000; // 100 kbit/s
9+
interface AvailableOutgoingBitrateIssueDetectorParams extends BaseIssueDetectorParams {
10+
availableOutgoingBitrateThreshold?: number;
11+
}
12+
13+
class AvailableOutgoingBitrateIssueDetector extends BaseIssueDetector {
14+
readonly #availableOutgoingBitrateThreshold: number;
15+
16+
constructor(params: AvailableOutgoingBitrateIssueDetectorParams = {}) {
17+
super(params);
18+
this.#availableOutgoingBitrateThreshold = params.availableOutgoingBitrateThreshold ?? 100_000; // 100 KBit/s
19+
}
1120

12-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
21+
performDetection(data: WebRTCStatsParsed): IssueDetectorResult {
1322
const issues: IssueDetectorResult = [];
1423
const { availableOutgoingBitrate } = data.connection;
1524
if (availableOutgoingBitrate === undefined) {
@@ -39,7 +48,7 @@ class AvailableOutgoingBitrateIssueDetector implements IssueDetector {
3948
return issues;
4049
}
4150

42-
if (videoStreamsTotalBitrate > 0 && availableOutgoingBitrate < this.#availableOutgoingBitrateTreshold) {
51+
if (videoStreamsTotalBitrate > 0 && availableOutgoingBitrate < this.#availableOutgoingBitrateThreshold) {
4352
issues.push({
4453
type: IssueType.Network,
4554
reason: IssueReason.OutboundNetworkThroughput,

src/detectors/BaseIssueDetector.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { IssueDetector, IssueDetectorResult, WebRTCStatsParsed } from '../types';
2+
import { scheduleTask } from '../utils/tasks';
3+
import { CLEANUP_PREV_STATS_TTL_MS } from '../utils/constants';
4+
5+
export interface PrevStatsCleanupPayload {
6+
connectionId: string;
7+
cleanupCallback?: () => void;
8+
}
9+
10+
export interface BaseIssueDetectorParams {
11+
statsCleanupTtlMs?: number;
12+
}
13+
14+
abstract class BaseIssueDetector implements IssueDetector {
15+
readonly #lastProcessedStats: Map<string, WebRTCStatsParsed | undefined>;
16+
17+
readonly #statsCleanupDelayMs: number;
18+
19+
constructor(params: BaseIssueDetectorParams = {}) {
20+
this.#lastProcessedStats = new Map();
21+
this.#statsCleanupDelayMs = params.statsCleanupTtlMs ?? CLEANUP_PREV_STATS_TTL_MS;
22+
}
23+
24+
abstract performDetection(data: WebRTCStatsParsed): IssueDetectorResult;
25+
26+
detect(data: WebRTCStatsParsed): IssueDetectorResult {
27+
const result = this.performDetection(data);
28+
29+
this.performPrevStatsCleanup({
30+
connectionId: data.connection.id,
31+
});
32+
33+
return result;
34+
}
35+
36+
protected performPrevStatsCleanup(payload: PrevStatsCleanupPayload): void {
37+
const { connectionId, cleanupCallback } = payload;
38+
39+
if (!this.#lastProcessedStats.has(connectionId)) {
40+
return;
41+
}
42+
43+
scheduleTask({
44+
taskId: connectionId,
45+
delayMs: this.#statsCleanupDelayMs,
46+
callback: () => {
47+
this.deleteLastProcessedStats(connectionId);
48+
49+
if (typeof cleanupCallback === 'function') {
50+
cleanupCallback();
51+
}
52+
},
53+
});
54+
}
55+
56+
protected setLastProcessedStats(connectionId: string, parsedStats: WebRTCStatsParsed): void {
57+
this.#lastProcessedStats.set(connectionId, parsedStats);
58+
}
59+
60+
protected getLastProcessedStats(connectionId: string): WebRTCStatsParsed | undefined {
61+
return this.#lastProcessedStats.get(connectionId);
62+
}
63+
64+
private deleteLastProcessedStats(connectionId: string): void {
65+
this.#lastProcessedStats.delete(connectionId);
66+
}
67+
}
68+
69+
export default BaseIssueDetector;

src/detectors/FramesDroppedIssueDetector.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import {
2-
IssueDetector,
32
IssueDetectorResult,
43
IssueReason,
54
IssueType,
65
WebRTCStatsParsed,
76
} from '../types';
7+
import BaseIssueDetector, { BaseIssueDetectorParams } from './BaseIssueDetector';
88

9-
class FramesDroppedIssueDetector implements IssueDetector {
10-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
9+
interface FramesDroppedIssueDetectorParams extends BaseIssueDetectorParams {
10+
framesDroppedThreshold?: number;
11+
}
12+
13+
class FramesDroppedIssueDetector extends BaseIssueDetector {
14+
readonly #framesDroppedThreshold: number;
1115

12-
#framesDroppedTreshold = 0.5;
16+
constructor(params: FramesDroppedIssueDetectorParams = {}) {
17+
super(params);
18+
this.#framesDroppedThreshold = params.framesDroppedThreshold ?? 0.5;
19+
}
1320

14-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
21+
performDetection(data: WebRTCStatsParsed): IssueDetectorResult {
22+
const { connection: { id: connectionId } } = data;
1523
const issues = this.processData(data);
16-
this.#lastProcessedStats[data.connection.id] = data;
24+
this.setLastProcessedStats(connectionId, data);
1725
return issues;
1826
}
1927

2028
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
2129
const streamsWithDroppedFrames = data.video.inbound.filter((stats) => stats.framesDropped > 0);
2230
const issues: IssueDetectorResult = [];
23-
const previousInboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.inbound;
31+
const previousInboundRTPVideoStreamsStats = this.getLastProcessedStats(data.connection.id)?.video.inbound;
2432

2533
if (!previousInboundRTPVideoStreamsStats) {
2634
return issues;
@@ -46,7 +54,7 @@ class FramesDroppedIssueDetector implements IssueDetector {
4654
}
4755

4856
const framesDropped = deltaFramesDropped / deltaFramesReceived;
49-
if (framesDropped >= this.#framesDroppedTreshold) {
57+
if (framesDropped >= this.#framesDroppedThreshold) {
5058
// more than half of the received frames were dropped
5159
issues.push({
5260
type: IssueType.CPU,

src/detectors/FramesEncodedSentIssueDetector.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import {
2-
IssueDetector,
32
IssueDetectorResult,
43
IssueReason,
54
IssueType,
65
WebRTCStatsParsed,
76
} from '../types';
7+
import BaseIssueDetector, { BaseIssueDetectorParams } from './BaseIssueDetector';
88

9-
class FramesEncodedSentIssueDetector implements IssueDetector {
10-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
9+
interface FramesEncodedSentIssueDetectorParams extends BaseIssueDetectorParams {
10+
missedFramesThreshold?: number;
11+
}
12+
13+
class FramesEncodedSentIssueDetector extends BaseIssueDetector {
14+
readonly #missedFramesThreshold: number;
1115

12-
#missedFramesTreshold = 0.15;
16+
constructor(params: FramesEncodedSentIssueDetectorParams = {}) {
17+
super(params);
18+
this.#missedFramesThreshold = params.missedFramesThreshold ?? 0.15;
19+
}
1320

14-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
21+
performDetection(data: WebRTCStatsParsed): IssueDetectorResult {
22+
const { connection: { id: connectionId } } = data;
1523
const issues = this.processData(data);
16-
this.#lastProcessedStats[data.connection.id] = data;
24+
this.setLastProcessedStats(connectionId, data);
1725
return issues;
1826
}
1927

2028
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
2129
const streamsWithEncodedFrames = data.video.outbound.filter((stats) => stats.framesEncoded > 0);
2230
const issues: IssueDetectorResult = [];
23-
const previousOutboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.outbound;
31+
const previousOutboundRTPVideoStreamsStats = this.getLastProcessedStats(data.connection.id)?.video.outbound;
2432

2533
if (!previousOutboundRTPVideoStreamsStats) {
2634
return issues;
@@ -52,7 +60,7 @@ class FramesEncodedSentIssueDetector implements IssueDetector {
5260
}
5361

5462
const missedFrames = deltaFramesSent / deltaFramesEncoded;
55-
if (missedFrames >= this.#missedFramesTreshold) {
63+
if (missedFrames >= this.#missedFramesThreshold) {
5664
issues.push({
5765
type: IssueType.Network,
5866
reason: IssueReason.OutboundNetworkThroughput,

0 commit comments

Comments
 (0)