Bladeren bron

feat: PerformanceObserverStats initial commit

Add a performance stat around long tasks. Chrome supports PerformanceObserver API that lets us
register for long tasks event. Any task that takes longer than 50ms is considered a long task.
dev1
Jaya Allamsetty 5 jaren geleden
bovenliggende
commit
a9ceee2de7

+ 15
- 0
JitsiConference.js Bestand weergeven

@@ -375,6 +375,11 @@ JitsiConference.prototype._init = function(options = {}) {
375 375
         Statistics.analytics.addPermanentProperties({
376 376
             'callstats_name': this._statsCurrentId
377 377
         });
378
+
379
+        // Start performance observer for monitoring long tasks
380
+        if (config.performanceStatsInterval) {
381
+            this.statistics.attachPerformanceStats(this);
382
+        }
378 383
     }
379 384
 
380 385
     this.eventManager.setupChatRoomListeners();
@@ -719,6 +724,16 @@ JitsiConference.prototype.getLocalVideoTrack = function() {
719 724
     return this.rtc ? this.rtc.getLocalVideoTrack() : null;
720 725
 };
721 726
 
727
+/**
728
+ * Obtains the performance statistics.
729
+ * @returns {Object|null}
730
+ */
731
+JitsiConference.prototype.getPerformanceStats = function() {
732
+    return browser.supportsPerformanceObserver()
733
+        ? this.statistics.performanceObserverStats.getPerformanceStats()
734
+        : null;
735
+};
736
+
722 737
 /**
723 738
  * Attaches a handler for events(For example - "participant joined".) in the
724 739
  * conference. All possible event are defined in JitsiConferenceEvents.

+ 10
- 0
modules/browser/BrowserCapabilities.js Bestand weergeven

@@ -122,6 +122,16 @@ export default class BrowserCapabilities extends BrowserDetection {
122 122
         return this.isChromiumBased() || this.isReactNative() || this.isSafari();
123 123
     }
124 124
 
125
+    /**
126
+     * Checks if the current browser supports the Long Tasks API that lets us observe
127
+     * performance measurement events and be notified of tasks that take longer than
128
+     * 50ms to execute on the main thread.
129
+     */
130
+    supportsPerformanceObserver() {
131
+        return typeof window.PerformanceObserver !== 'undefined'
132
+            && PerformanceObserver.supportedEntryTypes.indexOf('longtask') > -1;
133
+    }
134
+
125 135
     /**
126 136
      * Checks if the current browser supports audio level stats on the receivers.
127 137
      */

+ 97
- 0
modules/statistics/PerformanceObserverStats.js Bestand weergeven

@@ -0,0 +1,97 @@
1
+
2
+import { getLogger } from 'jitsi-meet-logger';
3
+
4
+import * as StatisticsEvents from '../../service/statistics/Events';
5
+import { RunningAverage } from '../util/MathUtil';
6
+
7
+const logger = getLogger(__filename);
8
+const MILLI_SECONDS = 1000;
9
+const SECONDS = 60;
10
+
11
+/**
12
+ * This class creates an observer that monitors browser's performance measurement events
13
+ * as they are recorded in the browser's performance timeline and computes an average and
14
+ * a maximum value for the long task events. Tasks are classified as long tasks if they take
15
+ * longer than 50ms to execute on the main thread.
16
+ */
17
+export class PerformanceObserverStats {
18
+    /**
19
+     * Creates a new instance of Performance observer statistics.
20
+     *
21
+     * @param {*} emitter Event emitter for emitting stats periodically
22
+     * @param {*} statsInterval interval for calculating the stats
23
+     */
24
+    constructor(emitter, statsInterval) {
25
+        this.eventEmitter = emitter;
26
+        this.longTasks = 0;
27
+        this.maxTaskDuration = 0;
28
+        this.performanceStatsInterval = statsInterval;
29
+        this.stats = new RunningAverage();
30
+    }
31
+
32
+    /**
33
+     * Obtains the average rate of long tasks observed per min and the
34
+     * duration of the longest task recorded by the observer.
35
+     * @returns {Object}
36
+     */
37
+    getPerformanceStats() {
38
+        return {
39
+            average: (this.stats.getAverage() * SECONDS).toFixed(2), // calc rate per min
40
+            maxTaskDuration: this.maxTaskDuration
41
+        };
42
+    }
43
+
44
+    /**
45
+     * Starts the performance observer by registering the callback function
46
+     * that calculates the performance statistics periodically.
47
+     * @returns {void}
48
+     */
49
+    startObserver() {
50
+        // Create a handler for when the long task event is fired.
51
+        this.longTaskEventHandler = list => {
52
+            const entries = list.getEntries();
53
+
54
+            for (const task of entries) {
55
+                this.longTasks++;
56
+                this.maxTaskDuration = Math.max(this.maxTaskDuration, task.duration).toFixed(3);
57
+            }
58
+        };
59
+
60
+        // Create an observer for monitoring long tasks.
61
+        logger.info('Creating a Performance Observer for monitoring Long Tasks');
62
+        this.observer = new PerformanceObserver(this.longTaskEventHandler);
63
+        this.observer.observe({ type: 'longtask',
64
+            buffered: true });
65
+        const startTime = Date.now();
66
+
67
+        // Calculate the average # of events/sec and emit a stats event.
68
+        this.longTasksIntervalId = setInterval(() => {
69
+            const now = Date.now();
70
+            const interval = this._lastTimeStamp
71
+                ? (now - this._lastTimeStamp) / MILLI_SECONDS
72
+                : (now - startTime) / MILLI_SECONDS;
73
+            const rate = this.longTasks / interval;
74
+
75
+            this.stats.addNext(rate);
76
+            this.eventEmitter.emit(
77
+                StatisticsEvents.LONG_TASKS_STATS, this.getPerformanceStats());
78
+
79
+            // Reset the counter and start counting events again.
80
+            this.longTasks = 0;
81
+            this._lastTimeStamp = Date.now();
82
+        }, this.performanceStatsInterval);
83
+    }
84
+
85
+    /**
86
+     * Stops the performance observer.
87
+     * @returns {void}
88
+     */
89
+    stopObserver() {
90
+        this.observer && this.observer.disconnect();
91
+        this.longTaskEventHandler = null;
92
+        if (this.longTasksIntervalId) {
93
+            clearInterval(this.longTasksIntervalId);
94
+            this.longTasksIntervalId = null;
95
+        }
96
+    }
97
+}

+ 47
- 0
modules/statistics/PerformanceObserverStats.spec.js Bestand weergeven

@@ -0,0 +1,47 @@
1
+import EventEmitter from 'events';
2
+
3
+import JitsiConference from '../../JitsiConference';
4
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
5
+import browser from '../browser';
6
+
7
+import Statistics from './statistics';
8
+
9
+/**
10
+ * Mock object to be used in place of a real conference.
11
+ *
12
+ * @constructor
13
+ */
14
+function MockConference() {
15
+    this.eventEmitter = new EventEmitter();
16
+}
17
+MockConference.prototype = Object.create(JitsiConference.prototype);
18
+MockConference.prototype.constructor = JitsiConference;
19
+
20
+describe('PerformanceObserverStats', () => {
21
+    beforeEach(() => {
22
+        // works only on chrome.
23
+        spyOn(browser, 'isChrome').and.returnValue(true);
24
+    });
25
+
26
+    it('Emits performance stats every sec', () => {
27
+        const mockConference = new MockConference();
28
+        const statistics = new Statistics();
29
+
30
+        statistics.attachPerformanceStats(mockConference);
31
+
32
+        const startObserverSpy = spyOn(statistics.performanceObserverStats, 'startObserver');
33
+        const stopObserverSpy = spyOn(statistics.performanceObserverStats, 'stopObserver');
34
+        const addNextSpy = spyOn(statistics.performanceObserverStats.stats, 'addNext');
35
+
36
+        mockConference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_JOINED);
37
+        expect(startObserverSpy).toHaveBeenCalled();
38
+        expect(statistics.performanceObserverStats.getPerformanceStats()).toBeTruthy();
39
+
40
+        setTimeout(() => {
41
+            expect(addNextSpy).toHaveBeenCalled();
42
+        }, 1000);
43
+
44
+        mockConference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_LEFT);
45
+        expect(stopObserverSpy).toHaveBeenCalled();
46
+    });
47
+});

+ 64
- 0
modules/statistics/statistics.js Bestand weergeven

@@ -1,5 +1,6 @@
1 1
 import EventEmitter from 'events';
2 2
 
3
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
3 4
 import JitsiTrackError from '../../JitsiTrackError';
4 5
 import { FEEDBACK } from '../../service/statistics/AnalyticsEvents';
5 6
 import * as StatisticsEvents from '../../service/statistics/Events';
@@ -9,6 +10,7 @@ import ScriptUtil from '../util/ScriptUtil';
9 10
 import analytics from './AnalyticsAdapter';
10 11
 import CallStats from './CallStats';
11 12
 import LocalStats from './LocalStatsCollector';
13
+import { PerformanceObserverStats } from './PerformanceObserverStats';
12 14
 import RTPStats from './RTPStatsCollector';
13 15
 import { CALLSTATS_SCRIPT_URL } from './constants';
14 16
 
@@ -119,6 +121,10 @@ Statistics.init = function(options) {
119 121
         Statistics.audioLevelsInterval = options.audioLevelsInterval;
120 122
     }
121 123
 
124
+    if (typeof options.performanceStatsInterval === 'number') {
125
+        Statistics.performanceStatsInterval = options.performanceStatsInterval;
126
+    }
127
+
122 128
     Statistics.disableThirdPartyRequests = options.disableThirdPartyRequests;
123 129
 };
124 130
 
@@ -187,6 +193,7 @@ export default function Statistics(xmpp, options) {
187 193
 Statistics.audioLevelsEnabled = false;
188 194
 Statistics.audioLevelsInterval = 200;
189 195
 Statistics.pcStatsInterval = 10000;
196
+Statistics.performanceStatsInterval = 10000;
190 197
 Statistics.disableThirdPartyRequests = false;
191 198
 Statistics.analytics = analytics;
192 199
 
@@ -282,6 +289,63 @@ Statistics.prototype.removeByteSentStatsListener = function(listener) {
282 289
         listener);
283 290
 };
284 291
 
292
+/**
293
+ * Add a listener that would be notified on a LONG_TASKS_STATS event.
294
+ *
295
+ * @param {Function} listener a function that would be called when notified.
296
+ * @returns {void}
297
+ */
298
+Statistics.prototype.addPerformanceStatsListener = function(listener) {
299
+    this.eventEmitter.on(StatisticsEvents.LONG_TASKS_STATS, listener);
300
+};
301
+
302
+/**
303
+ * Creates an instance of {@link PerformanceObserverStats} and starts the
304
+ * observer that records the stats periodically.
305
+ *
306
+ * @returns {void}
307
+ */
308
+Statistics.prototype.attachPerformanceStats = function(conference) {
309
+    if (!browser.supportsPerformanceObserver()) {
310
+        logger.warn('Performance observer for long tasks not supported by browser!');
311
+
312
+        return;
313
+    }
314
+
315
+    this.performanceObserverStats = new PerformanceObserverStats(
316
+        this.eventEmitter,
317
+        Statistics.performanceStatsInterval);
318
+
319
+    conference.on(
320
+        JitsiConferenceEvents.CONFERENCE_JOINED,
321
+        () => this.performanceObserverStats.startObserver());
322
+    conference.on(
323
+        JitsiConferenceEvents.CONFERENCE_LEFT,
324
+        () => this.performanceObserverStats.stopObserver());
325
+};
326
+
327
+/**
328
+ * Obtains the current value of the performance statistics.
329
+ *
330
+ * @returns {Object|null} stats object if the observer has been
331
+ * created, null otherwise.
332
+ */
333
+Statistics.prototype.getPerformanceStats = function() {
334
+    return this.performanceObserverStats
335
+        ? this.performanceObserverStats.getPerformanceStats()
336
+        : null;
337
+};
338
+
339
+/**
340
+ * Removes the given listener for the LONG_TASKS_STATS event.
341
+ *
342
+ * @param {Function} listener the listener we want to remove.
343
+ * @returns {void}
344
+ */
345
+Statistics.prototype.removePerformanceStatsListener = function(listener) {
346
+    this.eventEmitter.removeListener(StatisticsEvents.LONG_TASKS_STATS, listener);
347
+};
348
+
285 349
 Statistics.prototype.dispose = function() {
286 350
     try {
287 351
         // NOTE Before reading this please see the comment in stopCallStats...

+ 36
- 1
modules/util/MathUtil.js Bestand weergeven

@@ -1,5 +1,4 @@
1 1
 
2
-
3 2
 /**
4 3
  * The method will increase the given number by 1. If the given counter is equal
5 4
  * or greater to {@link Number.MAX_SAFE_INTEGER} then it will be rolled back to
@@ -38,3 +37,39 @@ export function calculateAverage(valueArray) {
38 37
 export function filterPositiveValues(valueArray) {
39 38
     return valueArray.filter(value => value >= 0);
40 39
 }
40
+
41
+/**
42
+ * This class calculates a simple running average that continually changes
43
+ * as more data points are collected and added.
44
+ */
45
+export class RunningAverage {
46
+    /**
47
+     * Creates an instance of the running average calculator.
48
+     */
49
+    constructor() {
50
+        this.average = 0;
51
+        this.n = 0;
52
+    }
53
+
54
+    /**
55
+     * Adds a new data point to the existing set of values and recomputes
56
+     * the running average.
57
+     * @param {number} value
58
+     * @returns {void}
59
+     */
60
+    addNext(value) {
61
+        if (typeof value !== 'number') {
62
+            return;
63
+        }
64
+        this.n += 1;
65
+        this.average = this.average + ((value - this.average) / this.n);
66
+    }
67
+
68
+    /**
69
+     * Obtains the average value for the current subset of values.
70
+     * @returns {number} - computed average.
71
+     */
72
+    getAverage() {
73
+        return this.average;
74
+    }
75
+}

+ 27
- 0
modules/util/MathUtil.spec.js Bestand weergeven

@@ -0,0 +1,27 @@
1
+import { RunningAverage } from './MathUtil';
2
+
3
+describe('running average', () => {
4
+    it('should work', () => {
5
+        const rAvg = new RunningAverage();
6
+
7
+        // 1 / 1
8
+        rAvg.addNext(1);
9
+        expect(rAvg.getAverage()).toBe(1);
10
+
11
+        // 4 / 2
12
+        rAvg.addNext(3);
13
+        expect(rAvg.getAverage()).toBe(2);
14
+
15
+        // 6 / 3
16
+        rAvg.addNext(2);
17
+        expect(rAvg.getAverage()).toBe(2);
18
+
19
+        // 12 / 4
20
+        rAvg.addNext(6);
21
+        expect(rAvg.getAverage()).toBe(3);
22
+
23
+        // 20 / 5
24
+        rAvg.addNext(8);
25
+        expect(rAvg.getAverage()).toBe(4);
26
+    });
27
+});

+ 5
- 0
service/statistics/Events.js Bestand weergeven

@@ -30,3 +30,8 @@ export const BYTE_SENT_STATS = 'statistics.byte_sent_stats';
30 30
  * <tt>resolution</tt>, and <tt>transport</tt>.
31 31
  */
32 32
 export const CONNECTION_STATS = 'statistics.connectionstats';
33
+
34
+/**
35
+ * An event carrying performance stats.
36
+ */
37
+export const LONG_TASKS_STATS = 'statistics.long_tasks_stats';

Laden…
Annuleren
Opslaan