Bläddra i källkod

feat: Measures RTT to a set of stun servers and reports in in analytics.

dev1
Boris Grozev 6 år sedan
förälder
incheckning
1e7c33cfbf

+ 8
- 0
JitsiConference.js Visa fil

@@ -46,6 +46,7 @@ import VideoSIPGW from './modules/videosipgw/VideoSIPGW';
46 46
 import * as VideoSIPGWConstants from './modules/videosipgw/VideoSIPGWConstants';
47 47
 import * as XMPPEvents from './service/xmpp/XMPPEvents';
48 48
 import { JITSI_MEET_MUC_TYPE } from './modules/xmpp/ChatRoom';
49
+import RttMonitor from './modules/rttmonitor/rttmonitor';
49 50
 
50 51
 import SpeakerStatsCollector from './modules/statistics/SpeakerStatsCollector';
51 52
 
@@ -253,6 +254,8 @@ JitsiConference.prototype._init = function(options = {}) {
253 254
 
254 255
     this.room.updateDeviceAvailability(RTC.getDeviceAvailability());
255 256
 
257
+    this.rttMonitor = new RttMonitor(config.rttMonitor || {});
258
+
256 259
     this.e2eping = new E2ePing(
257 260
         this.eventEmitter,
258 261
         config,
@@ -427,6 +430,11 @@ JitsiConference.prototype.leave = function() {
427 430
         this.avgRtpStatsReporter = null;
428 431
     }
429 432
 
433
+    if (this.rttMonitor) {
434
+        this.rttMonitor.stop();
435
+        this.rttMonitor = null;
436
+    }
437
+
430 438
     if (this.e2eping) {
431 439
         this.e2eping.stop();
432 440
         this.e2eping = null;

+ 10
- 0
modules/browser/BrowserCapabilities.js Visa fil

@@ -142,6 +142,16 @@ export default class BrowserCapabilities extends BrowserDetection {
142 142
             || this.isSafariWithWebrtc();
143 143
     }
144 144
 
145
+    /**
146
+     * Checks if the current browser supports RTT statistics for srflx local
147
+     * candidates through the legacy getStats() API.
148
+     */
149
+    supportsLocalCandidateRttStatistics() {
150
+        return this.isChrome()
151
+            || this.isElectron()
152
+            || this.isReactNative();
153
+    }
154
+
145 155
     /**
146 156
      * Checks if the current browser reports round trip time statistics for
147 157
      * the ICE candidate pair.

+ 345
- 0
modules/rttmonitor/rttmonitor.js Visa fil

@@ -0,0 +1,345 @@
1
+import browser from '../browser';
2
+import { createRttByRegionEvent }
3
+    from '../../service/statistics/AnalyticsEvents';
4
+import { getLogger } from 'jitsi-meet-logger';
5
+import RTCUtils from '../RTC/RTCUtils';
6
+import Statistics from '../statistics/statistics';
7
+
8
+const logger = getLogger(__filename);
9
+
10
+/**
11
+ * The options to pass to createOffer (we need to offer to receive *something*
12
+ * for the PC to gather candidates.
13
+ */
14
+const offerOptions = {
15
+    offerToReceiveAudio: 1,
16
+    offerToReceiveVideo: 0
17
+};
18
+
19
+
20
+/**
21
+ * The interval at which the webrtc engine sends STUN keep alive requests.
22
+ * @type {number}
23
+ */
24
+const stunKeepAliveIntervalMs = 10000;
25
+
26
+/**
27
+ * Wraps a PeerConnection with one specific STUN server and measures the RTT
28
+ * to the STUN server.
29
+ */
30
+class PCMonitor {
31
+    /**
32
+     *
33
+     * @param {Object} stunServer the STUN server configuration (address and
34
+     * region).
35
+     * @param {number} getStatsIntervalMs how often to call getStats.
36
+     * @param {number} delay the delay after which the PeerConnection will be
37
+     * started (that is, createOffer and setLocalDescription will be invoked).
38
+     *
39
+     */
40
+    constructor(stunServer, getStatsIntervalMs, delay) {
41
+        this.region = stunServer.region;
42
+        this.getStatsIntervalMs = getStatsIntervalMs;
43
+        this.getStatsInterval = null;
44
+
45
+        // What we consider the current RTT. It is Math.min(this.rtts).
46
+        this.rtt = Infinity;
47
+
48
+        // The RTT measurements we've made from the latest getStats() calls.
49
+        this.rtts = [];
50
+
51
+        const iceServers = [ { 'url': `stun:${stunServer.address}` } ];
52
+
53
+        this.pc = new RTCUtils.RTCPeerConnectionType(
54
+            {
55
+                'iceServers': iceServers
56
+            });
57
+
58
+        // Maps a key consisting of the IP address, port and priority of a
59
+        // candidate to some state related to it. If we have more than one
60
+        // network interface we will might multiple srflx candidates and this
61
+        // helps to distinguish between then.
62
+        this.candidates = {};
63
+
64
+        this.stopped = false;
65
+
66
+        this.start = this.start.bind(this);
67
+        this.stop = this.stop.bind(this);
68
+        this.startStatsInterval = this.startStatsInterval.bind(this);
69
+        this.handleCandidateRtt = this.handleCandidateRtt.bind(this);
70
+
71
+        window.setTimeout(this.start, delay);
72
+    }
73
+
74
+    /**
75
+     * Starts this PCMonitor. That is, invokes createOffer and
76
+     * setLocalDescription on the PeerConnection and starts an interval which
77
+     * calls getStats.
78
+     */
79
+    start() {
80
+        if (this.stopped) {
81
+            return;
82
+        }
83
+
84
+        this.pc.createOffer(offerOptions).then(offer => {
85
+            this.pc.setLocalDescription(
86
+                offer,
87
+                () => {
88
+                    logger.info(
89
+                        `setLocalDescription success for ${this.region}`);
90
+                    this.startStatsInterval();
91
+                },
92
+                error => {
93
+                    logger.warn(
94
+                        `setLocalDescription failed for ${this.region}: ${
95
+                            error}`);
96
+                }
97
+            );
98
+        });
99
+    }
100
+
101
+    /**
102
+     * Starts an interval which invokes getStats on the PeerConnection and
103
+     * measures the RTTs for the different candidates.
104
+     */
105
+    startStatsInterval() {
106
+        this.getStatsInterval = window.setInterval(
107
+            () => {
108
+                // Note that the data that we use to measure the RTT is only
109
+                // available in the legacy (callback based) getStats API.
110
+                this.pc.getStats(stats => {
111
+                    const results = stats.result();
112
+
113
+                    for (let i = 0; i < results.length; ++i) {
114
+                        const res = results[i];
115
+                        const rttTotal
116
+                            = Number(res.stat('stunKeepaliveRttTotal'));
117
+
118
+                        // We recognize the results that we care for (local
119
+                        // candidates of type srflx) by the existance of the
120
+                        // stunKeepaliveRttTotal stat.
121
+                        if (rttTotal > 0) {
122
+                            const candidateKey
123
+                                = `${res.stat('ipAddress')}_${
124
+                                    res.stat('portNumber')}_${
125
+                                    res.stat('priority')}`;
126
+
127
+                            this.handleCandidateRtt(
128
+                                candidateKey,
129
+                                rttTotal,
130
+                                Number(
131
+                                    res.stat('stunKeepaliveResponsesReceived')),
132
+                                Number(
133
+                                    res.stat('stunKeepaliveRequestsSent')));
134
+                        }
135
+                    }
136
+
137
+                    // After we've measured the RTT for all candidates we,
138
+                    // update the state of the PC with the shortest one.
139
+                    let rtt = Infinity;
140
+
141
+                    for (const key in this.candidates) {
142
+                        if (this.candidates.hasOwnProperty(key)
143
+                            && this.candidates[key].rtt > 0) {
144
+                            rtt = Math.min(rtt, this.candidates[key].rtt);
145
+                        }
146
+                    }
147
+
148
+                    // We keep the last 6 measured RTTs and choose the shortest
149
+                    // one to export to analytics. This is because we often see
150
+                    // failures get a real measurement which end up as Infinity.
151
+                    this.rtts.push(rtt);
152
+                    if (this.rtts.length > 6) {
153
+                        this.rtts = this.rtts.splice(1, 7);
154
+                    }
155
+                    this.rtt = Math.min(...this.rtts);
156
+                });
157
+            },
158
+            this.getStatsIntervalMs
159
+        );
160
+    }
161
+
162
+    /* eslint-disable max-params */
163
+    /**
164
+     * Updates the RTT for a candidate identified by "key" based on the values
165
+     * from getStats() and the previously saved state (i.e. old values).
166
+     *
167
+     * @param {String} key the ID for the candidate
168
+     * @param {number} rttTotal the value of the 'stunKeepaliveRttTotal' just
169
+     * measured.
170
+     * @param {number} responsesReceived the value of the
171
+     * 'stunKeepaliveResponsesReceived' stat just measured.
172
+     * @param {number} requestsSent the value of the 'stunKeepaliveRequestsSent'
173
+     * stat just measured.
174
+     */
175
+    handleCandidateRtt(key, rttTotal, responsesReceived, requestsSent) {
176
+        /* eslist-enable max-params */
177
+        if (!this.candidates[key]) {
178
+            this.candidates[key] = {
179
+                rttTotal: 0,
180
+                responsesReceived: 0,
181
+                requestsSent: 0,
182
+                rtt: NaN
183
+            };
184
+        }
185
+
186
+        const rttTotalDiff = rttTotal - this.candidates[key].rttTotal;
187
+        const responsesReceivedDiff
188
+            = responsesReceived - this.candidates[key].responsesReceived;
189
+
190
+        // We observe that when the difference between the number of requests
191
+        // and responses has grown (i.q. when the value below is positive), the
192
+        // the RTT measurements are incorrect (too low). For this reason we
193
+        // ignore these measurement (setting rtt=NaN), but update our state.
194
+        const requestsResponsesDiff
195
+            = (requestsSent - responsesReceived)
196
+            - (this.candidates[key].requestsSent
197
+                - this.candidates[key].responsesReceived);
198
+        let rtt = NaN;
199
+
200
+        if (responsesReceivedDiff > 0 && requestsResponsesDiff === 0) {
201
+            rtt = rttTotalDiff / responsesReceivedDiff;
202
+        }
203
+
204
+        this.candidates[key].rttTotal = rttTotal;
205
+        this.candidates[key].responsesReceived = responsesReceived;
206
+        this.candidates[key].requestsSent = requestsSent;
207
+        this.candidates[key].rtt = rtt;
208
+    }
209
+
210
+
211
+    /**
212
+     * Stops this PCMonitor, clearing its intervals and stopping the
213
+     * PeerConnection.
214
+     */
215
+    stop() {
216
+        if (this.getStatsInterval) {
217
+            window.clearInterval(this.getStatsInterval);
218
+        }
219
+
220
+        this.pc.close();
221
+
222
+        this.stopped = true;
223
+    }
224
+}
225
+
226
+/**
227
+ * A class which monitors the round-trip time (RTT) to a set of STUN servers.
228
+ * The measured RTTs are sent as analytics events. It uses a separate
229
+ * PeerConnection (represented as a PCMonitor) for each STUN server.
230
+ */
231
+export default class RttMonitor {
232
+    /**
233
+     * Initializes a new RttMonitor.
234
+     * @param {Object} config the object holding the configuration.
235
+     */
236
+    constructor(config) {
237
+        if (!config || !config.enabled
238
+            || !browser.supportsLocalCandidateRttStatistics()) {
239
+            return;
240
+        }
241
+
242
+        // Maps a region to the PCMonitor instance for that region.
243
+        this.pcMonitors = {};
244
+
245
+        this.startPCMonitors = this.startPCMonitors.bind(this);
246
+        this.sendAnalytics = this.sendAnalytics.bind(this);
247
+        this.stop = this.stop.bind(this);
248
+
249
+        this.analyticsInterval = null;
250
+        this.stopped = false;
251
+
252
+        const initialDelay = config.initialDelay || 60000;
253
+
254
+
255
+        logger.info(
256
+            `Starting RTT monitor with an initial delay of ${initialDelay}`);
257
+
258
+
259
+        window.setTimeout(
260
+            () => this.startPCMonitors(config),
261
+            initialDelay);
262
+    }
263
+
264
+    /**
265
+     * Starts the PCMonitors according to the configuration.
266
+     */
267
+    startPCMonitors(config) {
268
+        if (!Array.isArray(config.stunServers)) {
269
+            logger.warn('No stun servers configured.');
270
+
271
+            return;
272
+        }
273
+
274
+        if (this.stopped) {
275
+            return;
276
+        }
277
+
278
+        const getStatsIntervalMs
279
+            = config.getStatsInterval || stunKeepAliveIntervalMs;
280
+        const analyticsIntervalMs
281
+            = config.analyticsInterval || getStatsIntervalMs;
282
+        const count = config.stunServers.length;
283
+        const offset = getStatsIntervalMs / count;
284
+
285
+        // We delay the initialization of each PC so that they are uniformly
286
+        // distributed across the getStatsIntervalMs.
287
+        for (let i = 0; i < count; i++) {
288
+            const stunServer = config.stunServers[i];
289
+
290
+            this.pcMonitors[stunServer.region]
291
+                = new PCMonitor(stunServer, getStatsIntervalMs, offset * i);
292
+        }
293
+
294
+        window.setTimeout(
295
+            () => {
296
+                if (!this.stopped) {
297
+                    this.analyticsInterval
298
+                        = window.setInterval(
299
+                        this.sendAnalytics, analyticsIntervalMs);
300
+                }
301
+            },
302
+            1000);
303
+    }
304
+
305
+    /**
306
+     * Sends an analytics event with the measured RTT to each region/STUN
307
+     * server.
308
+     */
309
+    sendAnalytics() {
310
+        const rtts = {};
311
+
312
+        for (const region in this.pcMonitors) {
313
+            if (this.pcMonitors.hasOwnProperty(region)) {
314
+                const rtt = this.pcMonitors[region].rtt;
315
+
316
+                if (!isNaN(rtt) && rtt !== Infinity) {
317
+                    rtts[region.replace('-', '_')] = rtt;
318
+                }
319
+            }
320
+        }
321
+
322
+        if (rtts) {
323
+            Statistics.sendAnalytics(createRttByRegionEvent(rtts));
324
+        }
325
+    }
326
+
327
+    /**
328
+     * Stops this RttMonitor, clearing all intervals and closing all
329
+     * PeerConnections.
330
+     */
331
+    stop() {
332
+        logger.info('Stopping RttMonitor.');
333
+        this.stopped = true;
334
+        for (const region in this.pcMonitors) {
335
+            if (this.pcMonitors.hasOwnProperty(region)) {
336
+                this.pcMonitors[region].stop();
337
+            }
338
+        }
339
+        this.pcMonitors = {};
340
+
341
+        if (this.analyticsInterval) {
342
+            window.clearInterval(this.analyticsInterval);
343
+        }
344
+    }
345
+}

+ 15
- 0
service/statistics/AnalyticsEvents.js Visa fil

@@ -427,6 +427,21 @@ export const createRtpStatsEvent = function(attributes) {
427 427
     };
428 428
 };
429 429
 
430
+/**
431
+ * Creates an event which contains the round trip time (RTT) to a set of
432
+ * regions.
433
+ *
434
+ * @param attributes
435
+ * @returns {{type: string, action: string, attributes: *}}
436
+ */
437
+export const createRttByRegionEvent = function(attributes) {
438
+    return {
439
+        type: TYPE_OPERATIONAL,
440
+        action: 'rtt.by.region',
441
+        attributes
442
+    };
443
+};
444
+
430 445
 /**
431 446
  * Creates an event which indicates the Time To First Media (TTFM).
432 447
  * It is measured in milliseconds relative to the beginning of the document's

Laddar…
Avbryt
Spara