@atlaskit/insm 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +203 -5
- package/dist/cjs/index.js +55 -1
- package/dist/cjs/inp-measurers/inp.js +184 -0
- package/dist/cjs/insm-period.js +345 -0
- package/dist/cjs/insm-session.js +183 -0
- package/dist/cjs/insm.js +129 -0
- package/dist/cjs/period-measurers/afps.js +193 -0
- package/dist/cjs/types.js +5 -0
- package/dist/es2019/index.js +54 -1
- package/dist/es2019/inp-measurers/inp.js +142 -0
- package/dist/es2019/insm-period.js +246 -0
- package/dist/es2019/insm-session.js +149 -0
- package/dist/es2019/insm.js +105 -0
- package/dist/es2019/period-measurers/afps.js +153 -0
- package/dist/es2019/types.js +1 -0
- package/dist/esm/index.js +54 -1
- package/dist/esm/inp-measurers/inp.js +177 -0
- package/dist/esm/insm-period.js +339 -0
- package/dist/esm/insm-session.js +177 -0
- package/dist/esm/insm.js +122 -0
- package/dist/esm/period-measurers/afps.js +186 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/inp-measurers/inp.d.ts +37 -0
- package/dist/types/insm-period.d.ts +72 -0
- package/dist/types/insm-session.d.ts +91 -0
- package/dist/types/insm.d.ts +61 -0
- package/dist/types/period-measurers/afps.d.ts +57 -0
- package/dist/types/types.d.ts +81 -0
- package/dist/types-ts4.5/index.d.ts +10 -1
- package/dist/types-ts4.5/inp-measurers/inp.d.ts +37 -0
- package/dist/types-ts4.5/insm-period.d.ts +72 -0
- package/dist/types-ts4.5/insm-session.d.ts +91 -0
- package/dist/types-ts4.5/insm.d.ts +61 -0
- package/dist/types-ts4.5/period-measurers/afps.d.ts +57 -0
- package/dist/types-ts4.5/types.d.ts +81 -0
- package/package.json +6 -5
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.AnimationFPSIM = void 0;
|
|
8
|
+
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
9
|
+
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
10
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
11
|
+
var AnimationFPSIM = exports.AnimationFPSIM = /*#__PURE__*/function () {
|
|
12
|
+
function AnimationFPSIM() {
|
|
13
|
+
(0, _classCallCheck2.default)(this, AnimationFPSIM);
|
|
14
|
+
/**
|
|
15
|
+
* AFPS stands for Animation Frames Per Second
|
|
16
|
+
*/
|
|
17
|
+
(0, _defineProperty2.default)(this, "name", 'afps');
|
|
18
|
+
(0, _defineProperty2.default)(this, "monitor", new AnimationFPSMonitor());
|
|
19
|
+
}
|
|
20
|
+
return (0, _createClass2.default)(AnimationFPSIM, [{
|
|
21
|
+
key: "start",
|
|
22
|
+
value: function start(paused) {
|
|
23
|
+
var result = this.monitor.startNewWindow(paused);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
}, {
|
|
27
|
+
key: "end",
|
|
28
|
+
value: function end() {
|
|
29
|
+
var result = this.monitor.end();
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
}, {
|
|
33
|
+
key: "pause",
|
|
34
|
+
value: function pause() {
|
|
35
|
+
this.monitor.pause();
|
|
36
|
+
}
|
|
37
|
+
}, {
|
|
38
|
+
key: "resume",
|
|
39
|
+
value: function resume() {
|
|
40
|
+
this.monitor.resume();
|
|
41
|
+
}
|
|
42
|
+
}]);
|
|
43
|
+
}();
|
|
44
|
+
var AnimationFPSMonitor = /*#__PURE__*/function () {
|
|
45
|
+
function AnimationFPSMonitor() {
|
|
46
|
+
(0, _classCallCheck2.default)(this, AnimationFPSMonitor);
|
|
47
|
+
(0, _defineProperty2.default)(this, "paused", false);
|
|
48
|
+
(0, _defineProperty2.default)(this, "currentState", {
|
|
49
|
+
numerator: 0,
|
|
50
|
+
denominator: 0,
|
|
51
|
+
max: 0,
|
|
52
|
+
average: 0,
|
|
53
|
+
min: 0
|
|
54
|
+
});
|
|
55
|
+
// Current measurement window
|
|
56
|
+
(0, _defineProperty2.default)(this, "windowFrameCount", 0);
|
|
57
|
+
(0, _defineProperty2.default)(this, "windowTotalTime", 0);
|
|
58
|
+
(0, _defineProperty2.default)(this, "currentFrameStart", 0);
|
|
59
|
+
}
|
|
60
|
+
return (0, _createClass2.default)(AnimationFPSMonitor, [{
|
|
61
|
+
key: "measureWindowFPS",
|
|
62
|
+
value: function measureWindowFPS() {
|
|
63
|
+
// Calculate FPS for this window
|
|
64
|
+
// Note: windowFrameCount and windowTotalTime is reset after each window measurement
|
|
65
|
+
var windowSeconds = this.windowTotalTime / 1000;
|
|
66
|
+
// frames / seconds
|
|
67
|
+
var windowFPS = this.windowFrameCount / windowSeconds;
|
|
68
|
+
|
|
69
|
+
// Update overall tracking
|
|
70
|
+
this.currentState.numerator += this.windowFrameCount;
|
|
71
|
+
// The denominator is seconds not ms
|
|
72
|
+
this.currentState.denominator += windowSeconds;
|
|
73
|
+
|
|
74
|
+
// Calculate overall average FPS: (total frames / total time measured in seconds
|
|
75
|
+
this.currentState.average = this.currentState.numerator / this.currentState.denominator;
|
|
76
|
+
if (this.currentState.max === 0 || windowFPS > this.currentState.max) {
|
|
77
|
+
this.currentState.max = windowFPS;
|
|
78
|
+
}
|
|
79
|
+
if (this.currentState.min === 0 || windowFPS < this.currentState.min) {
|
|
80
|
+
this.currentState.min = windowFPS;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* If there is running tracking - it will be reset
|
|
86
|
+
*/
|
|
87
|
+
}, {
|
|
88
|
+
key: "startWindowTracking",
|
|
89
|
+
value: function startWindowTracking() {
|
|
90
|
+
var _this = this;
|
|
91
|
+
// Clear any previous tracking state
|
|
92
|
+
this.resetOverallTracking();
|
|
93
|
+
var _measureFrame = function measureFrame() {
|
|
94
|
+
_this.currentFrameStart = global.performance.now();
|
|
95
|
+
_this.animationFrame = requestAnimationFrame(function () {
|
|
96
|
+
// While measurement is paused -- we don't count the animation frame towards the monitored
|
|
97
|
+
// period.
|
|
98
|
+
if (!_this.paused) {
|
|
99
|
+
var frameDuration = performance.now() - _this.currentFrameStart;
|
|
100
|
+
|
|
101
|
+
// Add frame to current window
|
|
102
|
+
_this.windowFrameCount++;
|
|
103
|
+
_this.windowTotalTime += frameDuration;
|
|
104
|
+
|
|
105
|
+
// Check if window is >= 1 second (1000ms)
|
|
106
|
+
// When the page is running smoothly it's expected this will not be hit frequently
|
|
107
|
+
if (_this.windowTotalTime >= 1000) {
|
|
108
|
+
_this.measureWindowFPS();
|
|
109
|
+
// Reset window for next measurement
|
|
110
|
+
_this.resetWindow();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
_measureFrame();
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
_measureFrame();
|
|
117
|
+
}
|
|
118
|
+
}, {
|
|
119
|
+
key: "startNewWindow",
|
|
120
|
+
value: function startNewWindow(paused) {
|
|
121
|
+
var lastWindowResult = {
|
|
122
|
+
numerator: this.currentState.numerator,
|
|
123
|
+
denominator: this.currentState.denominator,
|
|
124
|
+
max: this.currentState.max,
|
|
125
|
+
min: this.currentState.min,
|
|
126
|
+
average: this.currentState.average
|
|
127
|
+
};
|
|
128
|
+
this.paused = paused;
|
|
129
|
+
this.startWindowTracking();
|
|
130
|
+
return lastWindowResult;
|
|
131
|
+
}
|
|
132
|
+
}, {
|
|
133
|
+
key: "resetWindow",
|
|
134
|
+
value: function resetWindow() {
|
|
135
|
+
this.windowFrameCount = 0;
|
|
136
|
+
this.windowTotalTime = 0;
|
|
137
|
+
// Note this.currentFrameStart is over ridden
|
|
138
|
+
// as part of tracking - so does not need to be
|
|
139
|
+
// reset.
|
|
140
|
+
}
|
|
141
|
+
}, {
|
|
142
|
+
key: "endWindowTracking",
|
|
143
|
+
value: function endWindowTracking() {
|
|
144
|
+
if (this.animationFrame) {
|
|
145
|
+
cancelAnimationFrame(this.animationFrame);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, {
|
|
149
|
+
key: "resetOverallTracking",
|
|
150
|
+
value: function resetOverallTracking() {
|
|
151
|
+
this.resetWindow();
|
|
152
|
+
this.endWindowTracking();
|
|
153
|
+
this.currentState = {
|
|
154
|
+
numerator: 0,
|
|
155
|
+
denominator: 0,
|
|
156
|
+
max: 0,
|
|
157
|
+
min: 0,
|
|
158
|
+
average: 0
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}, {
|
|
162
|
+
key: "end",
|
|
163
|
+
value: function end() {
|
|
164
|
+
try {
|
|
165
|
+
this.measureWindowFPS();
|
|
166
|
+
return this.currentState;
|
|
167
|
+
} finally {
|
|
168
|
+
this.resetOverallTracking();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}, {
|
|
172
|
+
key: "pause",
|
|
173
|
+
value: function pause() {
|
|
174
|
+
// Note - we leave the tracking animationFrame monitoring running, and on resume
|
|
175
|
+
// simply set the currentFrameStart and paused to true
|
|
176
|
+
// This works because the tracking builds windows of tracked time rather than
|
|
177
|
+
// basing itself off a window start time and time elapsed time since then.
|
|
178
|
+
this.paused = true;
|
|
179
|
+
}
|
|
180
|
+
}, {
|
|
181
|
+
key: "resume",
|
|
182
|
+
value: function resume() {
|
|
183
|
+
// Note - We don't need to handle the gap in measurement - as the raf logic builds/increments a window time
|
|
184
|
+
// based on the currentFrameStart (and any previous measurements which can only happen while the tracking
|
|
185
|
+
// is not paused).
|
|
186
|
+
this.currentFrameStart = performance.now();
|
|
187
|
+
this.paused = false;
|
|
188
|
+
if (!this.animationFrame) {
|
|
189
|
+
this.startWindowTracking();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}]);
|
|
193
|
+
}();
|
package/dist/es2019/index.js
CHANGED
|
@@ -1,4 +1,57 @@
|
|
|
1
|
+
import { INSM } from './insm';
|
|
2
|
+
let initialisedInsm;
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Initializes the INSM (Interactivity Session Measurement) tooling
|
|
3
6
|
*/
|
|
4
|
-
export function init() {
|
|
7
|
+
export function init(options) {
|
|
8
|
+
initialisedInsm = new INSM(options);
|
|
9
|
+
}
|
|
10
|
+
function insmInitialised() {
|
|
11
|
+
if (!initialisedInsm) {
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.error('INSM used when not initialised');
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* **In**teractivity **s**ession **m**onitoring
|
|
21
|
+
*/
|
|
22
|
+
export const insm = {
|
|
23
|
+
startHeavyTask(heavyTaskName) {
|
|
24
|
+
if (insmInitialised()) {
|
|
25
|
+
initialisedInsm.startHeavyTask(heavyTaskName);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
endHeavyTask(heavyTaskName) {
|
|
29
|
+
if (insmInitialised()) {
|
|
30
|
+
initialisedInsm.endHeavyTask(heavyTaskName);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
start(experienceKey, experienceProperties) {
|
|
34
|
+
if (insmInitialised()) {
|
|
35
|
+
initialisedInsm.start(experienceKey, experienceProperties);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
stopEarly(reasonKey, description) {
|
|
39
|
+
if (insmInitialised()) {
|
|
40
|
+
initialisedInsm.stopEarly(reasonKey, description);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// We only expose details and feature start/stop to consumers
|
|
44
|
+
// as the other properties are internals for the insm and InsmPeriod
|
|
45
|
+
// to interact with the running session.
|
|
46
|
+
get session() {
|
|
47
|
+
if (insmInitialised()) {
|
|
48
|
+
return initialisedInsm.runningSession;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
// @ts-expect-error Private method for testing purposes
|
|
52
|
+
__setAnalyticsWebClient(analyticsWebClient) {
|
|
53
|
+
if (initialisedInsm) {
|
|
54
|
+
initialisedInsm.analyticsWebClient = analyticsWebClient;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
export class INPTracker {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
/**
|
|
5
|
+
* INP stands for Interaction to Next Paint
|
|
6
|
+
*/
|
|
7
|
+
_defineProperty(this, "name", 'inp');
|
|
8
|
+
this.includedInteractions = (options === null || options === void 0 ? void 0 : options.includedInteractions) || ['pointerdown', 'pointerup', 'click', 'keydown', 'keyup'];
|
|
9
|
+
this.monitor = new InteractionTracker(this.includedInteractions);
|
|
10
|
+
}
|
|
11
|
+
start(paused) {
|
|
12
|
+
const result = this.monitor.start(paused);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
end() {
|
|
16
|
+
const result = this.monitor.end();
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
pause() {
|
|
20
|
+
this.monitor.pause();
|
|
21
|
+
}
|
|
22
|
+
resume() {
|
|
23
|
+
this.monitor.resume();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class InteractionResult {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.min = Infinity;
|
|
29
|
+
this.max = 0;
|
|
30
|
+
this.average = 0;
|
|
31
|
+
this.numerator = 0;
|
|
32
|
+
this.count = 0;
|
|
33
|
+
}
|
|
34
|
+
update(duration) {
|
|
35
|
+
if (duration > this.max) {
|
|
36
|
+
this.max = duration;
|
|
37
|
+
}
|
|
38
|
+
if (duration < this.min) {
|
|
39
|
+
this.min = duration;
|
|
40
|
+
}
|
|
41
|
+
this.numerator += this.average * (this.count - 1) + duration;
|
|
42
|
+
this.count += 1;
|
|
43
|
+
this.average = this.numerator / this.count;
|
|
44
|
+
}
|
|
45
|
+
toMeasure() {
|
|
46
|
+
return {
|
|
47
|
+
min: this.count === 0 ? 0 : this.min,
|
|
48
|
+
max: this.max,
|
|
49
|
+
average: this.average,
|
|
50
|
+
numerator: this.numerator,
|
|
51
|
+
denominator: this.count
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class InteractionTracker {
|
|
56
|
+
constructor(includedInteractions) {
|
|
57
|
+
_defineProperty(this, "paused", false);
|
|
58
|
+
this.performanceObserver = null;
|
|
59
|
+
this.interactionResult = new InteractionResult();
|
|
60
|
+
this.includedInteractions = includedInteractions;
|
|
61
|
+
}
|
|
62
|
+
stopTracking() {
|
|
63
|
+
var _this$performanceObse;
|
|
64
|
+
(_this$performanceObse = this.performanceObserver) === null || _this$performanceObse === void 0 ? void 0 : _this$performanceObse.disconnect();
|
|
65
|
+
this.performanceObserver = null;
|
|
66
|
+
}
|
|
67
|
+
startTracking() {
|
|
68
|
+
this.performanceObserver = new PerformanceObserver(list => {
|
|
69
|
+
// Note: find link to actual safari issue .. good to get rid of this if Safari has fixed this
|
|
70
|
+
// Delay by a microtask to workaround a bug in Safari where the
|
|
71
|
+
// callback is invoked immediately, rather than in a separate task.
|
|
72
|
+
// See: https://github.com/GoogleChrome/web-vitals/issues/277
|
|
73
|
+
Promise.resolve().then(() => {
|
|
74
|
+
const entries = list.getEntries();
|
|
75
|
+
entries.forEach(entry => {
|
|
76
|
+
// Skip further processing for entries that cannot be INP candidates.
|
|
77
|
+
//
|
|
78
|
+
// When a user interacts with a web page, a user interaction (for example a click) usually triggers a sequence of events.
|
|
79
|
+
// To measure the latency of this series of events, the events share the same interactionId.
|
|
80
|
+
// An interactionId is only computed for the following event types belonging to a user interaction. It is 0 otherwise.
|
|
81
|
+
// * click / tap / drag events: 'pointerdown', 'pointerup', 'click'
|
|
82
|
+
// * keypress events: 'keydown', 'keyup'
|
|
83
|
+
//
|
|
84
|
+
if (!entry.interactionId) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (this.includedInteractions.includes(entry.name)) {
|
|
88
|
+
this.interactionResult.update(entry.duration);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Event Timing entries have their durations rounded to the nearest 8ms,
|
|
95
|
+
// so a duration of 40ms would be any event that spans 2.5 or more frames
|
|
96
|
+
// at 60Hz. This threshold is chosen to strike a balance between usefulness
|
|
97
|
+
// and performance. Running this callback for any interaction that spans
|
|
98
|
+
// just one or two frames is likely not worth the insight that could be
|
|
99
|
+
// gained.
|
|
100
|
+
if (PerformanceObserver.supportedEntryTypes.includes('event')) {
|
|
101
|
+
this.performanceObserver.observe({
|
|
102
|
+
type: 'event',
|
|
103
|
+
buffered: true,
|
|
104
|
+
durationThreshold: 40
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
reset() {
|
|
109
|
+
this.stopTracking();
|
|
110
|
+
this.interactionResult = new InteractionResult();
|
|
111
|
+
}
|
|
112
|
+
start(paused) {
|
|
113
|
+
const lastResult = this.interactionResult.toMeasure();
|
|
114
|
+
this.reset();
|
|
115
|
+
this.paused = paused;
|
|
116
|
+
if (!paused) {
|
|
117
|
+
this.startTracking();
|
|
118
|
+
}
|
|
119
|
+
return lastResult;
|
|
120
|
+
}
|
|
121
|
+
end() {
|
|
122
|
+
try {
|
|
123
|
+
return this.interactionResult.toMeasure();
|
|
124
|
+
} finally {
|
|
125
|
+
this.reset();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
pause() {
|
|
129
|
+
if (this.paused) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.paused = true;
|
|
133
|
+
this.stopTracking();
|
|
134
|
+
}
|
|
135
|
+
resume() {
|
|
136
|
+
if (!this.paused) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.paused = false;
|
|
140
|
+
this.startTracking();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
/* eslint-disable @repo/internal/dom-events/no-unsafe-event-listeners */
|
|
3
|
+
|
|
4
|
+
export class PeriodTracking {
|
|
5
|
+
constructor(session) {
|
|
6
|
+
_defineProperty(this, "periodMeasurements", {
|
|
7
|
+
active: {
|
|
8
|
+
features: new Set(),
|
|
9
|
+
heavyTasks: new Set(),
|
|
10
|
+
measurements: {},
|
|
11
|
+
duration: 0,
|
|
12
|
+
count: 0
|
|
13
|
+
},
|
|
14
|
+
inactive: {
|
|
15
|
+
features: new Set(),
|
|
16
|
+
heavyTasks: new Set(),
|
|
17
|
+
measurements: {},
|
|
18
|
+
duration: 0,
|
|
19
|
+
count: 0
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
_defineProperty(this, "state", 'inactive');
|
|
23
|
+
_defineProperty(this, "pauses", new Set());
|
|
24
|
+
/**
|
|
25
|
+
* Warning: this can be reset mid period when pausing/resuming.
|
|
26
|
+
* It's intended use is to build the `periodMeasurements` duration.
|
|
27
|
+
*/
|
|
28
|
+
_defineProperty(this, "currentPeriodStart", performance.now());
|
|
29
|
+
_defineProperty(this, "activeStartListeners", []);
|
|
30
|
+
_defineProperty(this, "activeEndListeners", []);
|
|
31
|
+
/**
|
|
32
|
+
* This works by;
|
|
33
|
+
* On setup
|
|
34
|
+
* - starts activity listeners
|
|
35
|
+
* - on activity received
|
|
36
|
+
* - possible end active count down started.
|
|
37
|
+
* 3 animation frames or 3 seconds of inactivity - whichever comes first
|
|
38
|
+
* - any existing end count down ended
|
|
39
|
+
*/
|
|
40
|
+
_defineProperty(this, "activeEndCountDownAbortController", new AbortController());
|
|
41
|
+
this.session = session;
|
|
42
|
+
this.latestPeriodFeatures = new Set(session.runningFeatures);
|
|
43
|
+
this.latestHeavyTasks = new Set(session.insm.runningHeavyTasks);
|
|
44
|
+
this.pauses = new Set(session.insm.runningHeavyTasks);
|
|
45
|
+
const startInteractivityMeasuresPaused = this.latestHeavyTasks.size !== 0;
|
|
46
|
+
for (const periodMeasurer of session.insm.periodMeasurers) {
|
|
47
|
+
periodMeasurer.start(startInteractivityMeasuresPaused);
|
|
48
|
+
this.periodMeasurements.active.measurements[periodMeasurer.name] = {
|
|
49
|
+
numerator: 0,
|
|
50
|
+
denominator: 0,
|
|
51
|
+
max: 0,
|
|
52
|
+
min: 0,
|
|
53
|
+
average: 0
|
|
54
|
+
};
|
|
55
|
+
this.periodMeasurements.inactive.measurements[periodMeasurer.name] = {
|
|
56
|
+
numerator: 0,
|
|
57
|
+
denominator: 0,
|
|
58
|
+
max: 0,
|
|
59
|
+
min: 0,
|
|
60
|
+
average: 0
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
this.setupActiveStartInteractionListeners();
|
|
64
|
+
}
|
|
65
|
+
startFeature(featureName) {
|
|
66
|
+
this.latestPeriodFeatures.add(featureName);
|
|
67
|
+
this.periodMeasurements[this.state].features.add(featureName);
|
|
68
|
+
}
|
|
69
|
+
startHeavyTask(heavyTaskName) {
|
|
70
|
+
this.latestHeavyTasks.add(heavyTaskName);
|
|
71
|
+
this.periodMeasurements[this.state].heavyTasks.add(heavyTaskName);
|
|
72
|
+
this.pause(heavyTaskName);
|
|
73
|
+
}
|
|
74
|
+
endHeavyTask(heavyTaskName) {
|
|
75
|
+
this.resume(heavyTaskName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sets a pause based on a key. If this is the first pause, then it will also halt
|
|
80
|
+
* running interactivity measures, and update the current measurements duration.
|
|
81
|
+
*/
|
|
82
|
+
pause(pauseName) {
|
|
83
|
+
if (this.pauses.size === 0) {
|
|
84
|
+
this.periodMeasurements[this.state].duration = this.periodMeasurements[this.state].duration + (performance.now() - this.currentPeriodStart);
|
|
85
|
+
for (const periodMeasurer of this.session.insm.periodMeasurers) {
|
|
86
|
+
periodMeasurer.pause();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.pauses.add(pauseName);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Releases a pause for a key.
|
|
94
|
+
*
|
|
95
|
+
* **NOTE**: The session will only resume if this was the only
|
|
96
|
+
* currently tracked pause key.
|
|
97
|
+
*/
|
|
98
|
+
resume(pauseName) {
|
|
99
|
+
this.pauses.delete(pauseName);
|
|
100
|
+
if (this.pauses.size === 0) {
|
|
101
|
+
this.currentPeriodStart = performance.now();
|
|
102
|
+
for (const periodMeasurer of this.session.insm.periodMeasurers) {
|
|
103
|
+
periodMeasurer.resume();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
get endResults() {
|
|
108
|
+
this.changePeriodAndTrackLast(this.state);
|
|
109
|
+
return this.periodMeasurements;
|
|
110
|
+
}
|
|
111
|
+
setupActiveStartInteractionListeners() {
|
|
112
|
+
const events = ['mousemove', 'pointerdown', 'pointerup', 'click', 'keydown', 'keyup', 'scroll'];
|
|
113
|
+
for (const event of events) {
|
|
114
|
+
const eventListener = () => {
|
|
115
|
+
// start event
|
|
116
|
+
this.changePeriodAndTrackLast('inactive', 'active');
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener(event, eventListener, {
|
|
119
|
+
once: true
|
|
120
|
+
});
|
|
121
|
+
this.activeStartListeners.push([event, eventListener]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
setupEndActiveInteractionListeners() {
|
|
125
|
+
this.activeEndCountDownVisibilityListener = () => {
|
|
126
|
+
if (document.visibilityState === 'hidden') {
|
|
127
|
+
endSession();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
window.addEventListener('visibilitychange', this.activeEndCountDownVisibilityListener);
|
|
131
|
+
const events = ['mousemove', 'pointerdown', 'pointerup', 'click', 'keydown', 'keyup', 'scroll'];
|
|
132
|
+
for (const event of events) {
|
|
133
|
+
const eventListener = () => {
|
|
134
|
+
this.activeEndCountDownAbortController.abort();
|
|
135
|
+
this.activeEndCountDownAbortController = new AbortController();
|
|
136
|
+
startPeriodEndCountDown(this.activeEndCountDownAbortController.signal, () => {
|
|
137
|
+
endSession();
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
window.addEventListener(event, eventListener);
|
|
141
|
+
this.activeEndListeners.push([event, eventListener]);
|
|
142
|
+
}
|
|
143
|
+
const endSession = () => {
|
|
144
|
+
this.activeEndCountDownAbortController.abort();
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
146
|
+
window.removeEventListener('visibilitychange', this.activeEndCountDownVisibilityListener);
|
|
147
|
+
for (const activeEndListener of this.activeEndListeners) {
|
|
148
|
+
window.removeEventListener(activeEndListener[0], activeEndListener[1]);
|
|
149
|
+
}
|
|
150
|
+
this.changePeriodAndTrackLast('active', 'inactive');
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// When first setting up -- there has just been an interaction -- so we start a countdown
|
|
154
|
+
this.activeEndCountDownAbortController.abort();
|
|
155
|
+
this.activeEndCountDownAbortController = new AbortController();
|
|
156
|
+
startPeriodEndCountDown(this.activeEndCountDownAbortController.signal, () => {
|
|
157
|
+
endSession();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
changePeriodAndTrackLast(lastPeriod, newPeriod) {
|
|
161
|
+
var _this$activeEndCountD;
|
|
162
|
+
this.periodMeasurements[lastPeriod].duration = this.periodMeasurements[lastPeriod].duration + (performance.now() - this.currentPeriodStart);
|
|
163
|
+
this.periodMeasurements[lastPeriod].count++;
|
|
164
|
+
const interactivityMeasuresPaused = this.latestHeavyTasks.size !== 0;
|
|
165
|
+
for (const interactivityMeasure of this.session.insm.periodMeasurers) {
|
|
166
|
+
const finalResult = newPeriod ? interactivityMeasure.start(interactivityMeasuresPaused) : interactivityMeasure.end();
|
|
167
|
+
const tracked = this.periodMeasurements[lastPeriod].measurements[interactivityMeasure.name];
|
|
168
|
+
if (finalResult.average === 0) {
|
|
169
|
+
// In this case -- we haven't had any measurements complete since last measurement
|
|
170
|
+
// so we do nothing
|
|
171
|
+
} else if (tracked.average === 0) {
|
|
172
|
+
// In this case -- we haven't had any entries come through yet - so we replace any existing entries
|
|
173
|
+
this.periodMeasurements[lastPeriod].measurements[interactivityMeasure.name] = finalResult;
|
|
174
|
+
} else {
|
|
175
|
+
// In this case there is a new measurement which we merge with the previous measurement
|
|
176
|
+
const merged = {
|
|
177
|
+
numerator: finalResult.numerator + tracked.numerator,
|
|
178
|
+
denominator: finalResult.denominator + tracked.denominator,
|
|
179
|
+
average: (finalResult.numerator + tracked.numerator) / (finalResult.denominator + tracked.denominator),
|
|
180
|
+
max: tracked.max === 0 || finalResult.max > tracked.max ? finalResult.max : tracked.max,
|
|
181
|
+
min: tracked.min === 0 || finalResult.min < tracked.min ? finalResult.min : tracked.min
|
|
182
|
+
};
|
|
183
|
+
this.periodMeasurements[lastPeriod].measurements[interactivityMeasure.name] = merged;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const listener of this.activeStartListeners) {
|
|
187
|
+
window.removeEventListener(listener[0], listener[1]);
|
|
188
|
+
}
|
|
189
|
+
for (const listener of this.activeEndListeners) {
|
|
190
|
+
window.removeEventListener(listener[0], listener[1]);
|
|
191
|
+
}
|
|
192
|
+
(_this$activeEndCountD = this.activeEndCountDownAbortController) === null || _this$activeEndCountD === void 0 ? void 0 : _this$activeEndCountD.abort();
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
194
|
+
window.removeEventListener('visibilitychange', this.activeEndCountDownVisibilityListener);
|
|
195
|
+
if (newPeriod) {
|
|
196
|
+
this.latestPeriodFeatures = new Set(this.session.runningFeatures);
|
|
197
|
+
this.session.runningFeatures.forEach(featureName => {
|
|
198
|
+
this.periodMeasurements[newPeriod].features.add(featureName);
|
|
199
|
+
});
|
|
200
|
+
this.latestHeavyTasks = new Set(this.session.insm.runningHeavyTasks);
|
|
201
|
+
this.currentPeriodStart = performance.now();
|
|
202
|
+
switch (newPeriod) {
|
|
203
|
+
case 'active':
|
|
204
|
+
this.setupEndActiveInteractionListeners();
|
|
205
|
+
this.state = 'active';
|
|
206
|
+
break;
|
|
207
|
+
case 'inactive':
|
|
208
|
+
this.setupActiveStartInteractionListeners();
|
|
209
|
+
this.state = 'inactive';
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function startPeriodEndCountDown(signal, handleEnd) {
|
|
216
|
+
let animationFramesSinceLastInteraction = 0;
|
|
217
|
+
let inactiveTimeReached = false;
|
|
218
|
+
let timer;
|
|
219
|
+
let animationFrameHandler;
|
|
220
|
+
const monitorAnimationFrames = () => {
|
|
221
|
+
animationFrameHandler = requestAnimationFrame(() => {
|
|
222
|
+
animationFramesSinceLastInteraction++;
|
|
223
|
+
if (animationFramesSinceLastInteraction < 3) {
|
|
224
|
+
monitorAnimationFrames();
|
|
225
|
+
} else {
|
|
226
|
+
if (inactiveTimeReached) {
|
|
227
|
+
handleEnd();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
const monitorTime = () => {
|
|
233
|
+
timer = setTimeout(() => {
|
|
234
|
+
inactiveTimeReached = true;
|
|
235
|
+
if (animationFramesSinceLastInteraction === 3) {
|
|
236
|
+
handleEnd();
|
|
237
|
+
}
|
|
238
|
+
}, 3000);
|
|
239
|
+
};
|
|
240
|
+
monitorTime();
|
|
241
|
+
monitorAnimationFrames();
|
|
242
|
+
signal.addEventListener('abort', () => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
cancelAnimationFrame(animationFrameHandler);
|
|
245
|
+
});
|
|
246
|
+
}
|