@atlaskit/editor-plugin-interactivity 1.1.0 → 1.2.1
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 +26 -0
- package/README.md +46 -1
- package/dist/cjs/collections/bounded-list.js +42 -0
- package/dist/cjs/collections/bounded-map.js +42 -0
- package/dist/cjs/collector/interaction-events.js +4 -0
- package/dist/cjs/collector/interaction-group.js +31 -4
- package/dist/cjs/collector/interaction-tracker.js +207 -67
- package/dist/cjs/collector/interactivity-collector.js +50 -35
- package/dist/cjs/collector/long-animation-frame-observer.js +62 -0
- package/dist/cjs/collector/slow-interaction-list.js +297 -56
- package/dist/es2019/collections/bounded-list.js +24 -0
- package/dist/es2019/collections/bounded-map.js +25 -0
- package/dist/es2019/collector/interaction-events.js +4 -0
- package/dist/es2019/collector/interaction-group.js +28 -5
- package/dist/es2019/collector/interaction-tracker.js +198 -54
- package/dist/es2019/collector/interactivity-collector.js +35 -34
- package/dist/es2019/collector/long-animation-frame-observer.js +42 -0
- package/dist/es2019/collector/slow-interaction-list.js +251 -57
- package/dist/esm/collections/bounded-list.js +35 -0
- package/dist/esm/collections/bounded-map.js +35 -0
- package/dist/esm/collector/interaction-events.js +4 -0
- package/dist/esm/collector/interaction-group.js +31 -5
- package/dist/esm/collector/interaction-tracker.js +207 -67
- package/dist/esm/collector/interactivity-collector.js +50 -35
- package/dist/esm/collector/long-animation-frame-observer.js +55 -0
- package/dist/esm/collector/slow-interaction-list.js +297 -56
- package/dist/types/analytics/interactivity-snapshot.d.ts +32 -1
- package/dist/types/collections/bounded-list.d.ts +9 -0
- package/dist/types/collections/bounded-map.d.ts +9 -0
- package/dist/types/collector/interaction-group.d.ts +18 -5
- package/dist/types/collector/interaction-tracker.d.ts +58 -9
- package/dist/types/collector/interactivity-collector.d.ts +3 -0
- package/dist/types/collector/long-animation-frame-observer.d.ts +28 -0
- package/dist/types/collector/slow-interaction-list.d.ts +44 -18
- package/package.json +3 -39
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
1
2
|
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
2
3
|
import _createClass from "@babel/runtime/helpers/createClass";
|
|
3
4
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
5
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
6
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
7
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
8
|
+
import { BoundedList } from '../collections/bounded-list';
|
|
4
9
|
/** Fixed by the schema, so the event stays a bounded size. */
|
|
5
10
|
var MAX_RECORDS = 5;
|
|
6
11
|
|
|
@@ -10,6 +15,12 @@ var MAX_RECORDS = 5;
|
|
|
10
15
|
*/
|
|
11
16
|
var MIN_LATENCY_MS = 200;
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* How many frames are kept to attribute records from. An interaction spans one paint, so a few dozen
|
|
20
|
+
* cover even a second of a janky page.
|
|
21
|
+
*/
|
|
22
|
+
var MAX_FRAMES = 64;
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* The attributes a target may be named by, all of them ours: `data-vc` for visual completion, the
|
|
15
26
|
* test ids for tests. Ids, roles, class names, text and accessibility labels are left out because
|
|
@@ -20,13 +31,42 @@ var MAX_TARGET_ELEMENTS = 4;
|
|
|
20
31
|
var MAX_ATTRIBUTE_VALUE_LENGTH = 32;
|
|
21
32
|
var MAX_TARGET_LENGTH = 120;
|
|
22
33
|
|
|
23
|
-
/**
|
|
34
|
+
/** How long a reported script or function name may be. */
|
|
35
|
+
var MAX_NAME_LENGTH = 64;
|
|
36
|
+
var QUERY_OR_HASH = /[#\?]/;
|
|
37
|
+
|
|
38
|
+
/** The part of a record that only the frames the interaction ran in can fill in. */
|
|
24
39
|
|
|
25
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Whether two attributions say the same thing. Shallow, because every field of one is a number or a
|
|
42
|
+
* string; by the field names of both, so that a field going missing counts as a change rather than
|
|
43
|
+
* as nothing to see.
|
|
44
|
+
*/
|
|
45
|
+
function sameAttribution(one, other) {
|
|
46
|
+
var fields = Object.keys(one);
|
|
47
|
+
return fields.length === Object.keys(other).length && fields.every(function (field) {
|
|
48
|
+
return one[field] === other[field];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One recorded interaction. The reported phases are not among its fields: they are the gaps between
|
|
54
|
+
* the boundaries, worked out when the record is reported, which is also where the latency is
|
|
55
|
+
* rounded into the `durationMs` the event carries.
|
|
56
|
+
*
|
|
57
|
+
* The attribution is kept whole rather than spread across the record, so that a record can never
|
|
58
|
+
* hold half of what one set of frames said and half of what another did.
|
|
59
|
+
*
|
|
60
|
+
* `interactionId` identifies the interaction across the entries measuring it, and `boundaries` is
|
|
61
|
+
* also what its frames are matched to it by. Neither is reported.
|
|
62
|
+
*/
|
|
26
63
|
|
|
27
64
|
/**
|
|
28
65
|
* The slowest interactions of one session, which is what the event's `slowest` records are.
|
|
29
66
|
*
|
|
67
|
+
* Interactions arrive from the tracker and frames from the Long Animation Frame observer, and this
|
|
68
|
+
* is where the two meet: a record says both how long the user waited and where that time went.
|
|
69
|
+
*
|
|
30
70
|
* A record is built from the entry that measured the interaction, as that entry arrives:
|
|
31
71
|
* `entry.target` is `null` once the element has left the document.
|
|
32
72
|
*/
|
|
@@ -35,42 +75,88 @@ export var SlowInteractionList = /*#__PURE__*/function () {
|
|
|
35
75
|
_classCallCheck(this, SlowInteractionList);
|
|
36
76
|
/** Slowest first. */
|
|
37
77
|
_defineProperty(this, "records", []);
|
|
78
|
+
_defineProperty(this, "frames", new BoundedList(MAX_FRAMES));
|
|
38
79
|
}
|
|
39
80
|
return _createClass(SlowInteractionList, [{
|
|
40
|
-
key: "
|
|
41
|
-
value:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
key: "trackInteractionUpdate",
|
|
82
|
+
value:
|
|
83
|
+
/**
|
|
84
|
+
* Takes in what the tracker now says about an interaction, keeping it when it is one of the
|
|
85
|
+
* slowest of the session.
|
|
86
|
+
*
|
|
87
|
+
* @returns whether that changed what a snapshot would carry.
|
|
88
|
+
*/
|
|
89
|
+
function trackInteractionUpdate(entry, update) {
|
|
90
|
+
var boundaries = update.boundaries,
|
|
91
|
+
interactionId = update.interactionId,
|
|
92
|
+
latencyMs = update.latencyMs;
|
|
93
|
+
var index = this.records.findIndex(function (record) {
|
|
94
|
+
return record.interactionId === interactionId;
|
|
46
95
|
});
|
|
47
|
-
var
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
var knownRecord = index === -1 ? undefined : this.records[index];
|
|
97
|
+
if (knownRecord && latencyMs <= knownRecord.latencyMs) {
|
|
98
|
+
var _knownRecord$boundari, _knownRecord$boundari2;
|
|
99
|
+
// The interaction at the latency it already had, so its name and target still come from
|
|
100
|
+
// the entry that measured it at its slowest — and so do `startedAt` and `presentedAt`,
|
|
101
|
+
// which leaves the processing as the only pair that can have moved.
|
|
102
|
+
if (((_knownRecord$boundari = knownRecord.boundaries) === null || _knownRecord$boundari === void 0 ? void 0 : _knownRecord$boundari.processingStartedAt) === (boundaries === null || boundaries === void 0 ? void 0 : boundaries.processingStartedAt) && ((_knownRecord$boundari2 = knownRecord.boundaries) === null || _knownRecord$boundari2 === void 0 ? void 0 : _knownRecord$boundari2.processingEndedAt) === (boundaries === null || boundaries === void 0 ? void 0 : boundaries.processingEndedAt)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
knownRecord.boundaries = boundaries;
|
|
106
|
+
this.attribute(knownRecord);
|
|
107
|
+
return true;
|
|
57
108
|
}
|
|
58
109
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
110
|
+
// Everything below builds a record out of `entry`, so it has to be an entry of this
|
|
111
|
+
// interaction. The tracker also reports an interaction whose boundaries moved because of an
|
|
112
|
+
// event that is no interaction of its own, and that event names something else entirely.
|
|
113
|
+
if (entry.interactionId !== interactionId) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (knownRecord) {
|
|
117
|
+
// An interaction measured as slower replaces itself rather than taking a second place.
|
|
118
|
+
this.records[index] = this.toRecord(entry, update);
|
|
64
119
|
} else {
|
|
65
|
-
this.records[
|
|
120
|
+
var toBeatMs = this.records.length === MAX_RECORDS ? this.records[MAX_RECORDS - 1].latencyMs : MIN_LATENCY_MS;
|
|
121
|
+
if (latencyMs <= toBeatMs) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
this.records.push(this.toRecord(entry, update));
|
|
66
125
|
}
|
|
67
126
|
this.records.sort(function (a, b) {
|
|
68
|
-
return b.
|
|
127
|
+
return b.latencyMs - a.latencyMs;
|
|
69
128
|
});
|
|
70
129
|
this.records.splice(MAX_RECORDS);
|
|
130
|
+
return true;
|
|
71
131
|
}
|
|
72
132
|
|
|
73
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Takes in the frames the browser has just reported and works out again what the frames say about
|
|
135
|
+
* every record — again, because the frames of one interaction can be reported in several batches
|
|
136
|
+
* and the first of them may hold neither its longest script nor all of its style and layout.
|
|
137
|
+
*
|
|
138
|
+
* @returns whether that changed what a snapshot would carry.
|
|
139
|
+
*/
|
|
140
|
+
}, {
|
|
141
|
+
key: "trackLongAnimationFrames",
|
|
142
|
+
value: function trackLongAnimationFrames(frames) {
|
|
143
|
+
var _this$frames;
|
|
144
|
+
(_this$frames = this.frames).push.apply(_this$frames, _toConsumableArray(frames));
|
|
145
|
+
var changed = false;
|
|
146
|
+
var _iterator = _createForOfIteratorHelper(this.records),
|
|
147
|
+
_step;
|
|
148
|
+
try {
|
|
149
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
150
|
+
var record = _step.value;
|
|
151
|
+
changed = this.attribute(record) || changed;
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
_iterator.e(err);
|
|
155
|
+
} finally {
|
|
156
|
+
_iterator.f();
|
|
157
|
+
}
|
|
158
|
+
return changed;
|
|
159
|
+
}
|
|
74
160
|
}, {
|
|
75
161
|
key: "snapshot",
|
|
76
162
|
value: function snapshot() {
|
|
@@ -78,50 +164,205 @@ export var SlowInteractionList = /*#__PURE__*/function () {
|
|
|
78
164
|
return undefined;
|
|
79
165
|
}
|
|
80
166
|
return this.records.map(function (record) {
|
|
167
|
+
var _record$attribution, _record$attribution2, _record$attribution3, _record$attribution4, _record$attribution5, _record$attribution6, _record$attribution7, _record$attribution8, _record$attribution9;
|
|
168
|
+
var boundaries = record.boundaries;
|
|
81
169
|
return {
|
|
82
170
|
group: record.group,
|
|
83
171
|
name: record.name,
|
|
84
|
-
durationMs: record.
|
|
85
|
-
inputDelayMs:
|
|
86
|
-
processingMs:
|
|
87
|
-
presentationDelayMs:
|
|
88
|
-
target: record.target
|
|
172
|
+
durationMs: Math.round(record.latencyMs),
|
|
173
|
+
inputDelayMs: boundaries && Math.round(boundaries.processingStartedAt - boundaries.startedAt),
|
|
174
|
+
processingMs: boundaries && Math.round(boundaries.processingEndedAt - boundaries.processingStartedAt),
|
|
175
|
+
presentationDelayMs: boundaries && Math.round(boundaries.presentedAt - boundaries.processingEndedAt),
|
|
176
|
+
target: record.target,
|
|
177
|
+
functionName: (_record$attribution = record.attribution) === null || _record$attribution === void 0 ? void 0 : _record$attribution.functionName,
|
|
178
|
+
invokerType: (_record$attribution2 = record.attribution) === null || _record$attribution2 === void 0 ? void 0 : _record$attribution2.invokerType,
|
|
179
|
+
longestScriptMs: (_record$attribution3 = record.attribution) === null || _record$attribution3 === void 0 ? void 0 : _record$attribution3.longestScriptMs,
|
|
180
|
+
scriptName: (_record$attribution4 = record.attribution) === null || _record$attribution4 === void 0 ? void 0 : _record$attribution4.scriptName,
|
|
181
|
+
scriptSubpart: (_record$attribution5 = record.attribution) === null || _record$attribution5 === void 0 ? void 0 : _record$attribution5.scriptSubpart,
|
|
182
|
+
totalPaintDurationMs: (_record$attribution6 = record.attribution) === null || _record$attribution6 === void 0 ? void 0 : _record$attribution6.totalPaintDurationMs,
|
|
183
|
+
totalScriptDurationMs: (_record$attribution7 = record.attribution) === null || _record$attribution7 === void 0 ? void 0 : _record$attribution7.totalScriptDurationMs,
|
|
184
|
+
totalStyleAndLayoutDurationMs: (_record$attribution8 = record.attribution) === null || _record$attribution8 === void 0 ? void 0 : _record$attribution8.totalStyleAndLayoutDurationMs,
|
|
185
|
+
totalUnattributedDurationMs: (_record$attribution9 = record.attribution) === null || _record$attribution9 === void 0 ? void 0 : _record$attribution9.totalUnattributedDurationMs
|
|
89
186
|
};
|
|
90
187
|
});
|
|
91
188
|
}
|
|
189
|
+
}, {
|
|
190
|
+
key: "toRecord",
|
|
191
|
+
value: function toRecord(entry, update) {
|
|
192
|
+
var _update$group;
|
|
193
|
+
// The target is read only once the interaction has earned a place: naming it walks the DOM,
|
|
194
|
+
// and this runs while the page is already slow.
|
|
195
|
+
var record = {
|
|
196
|
+
attribution: undefined,
|
|
197
|
+
boundaries: update.boundaries,
|
|
198
|
+
interactionId: update.interactionId,
|
|
199
|
+
// An interaction the editor never reported an event for is not the editor's as far as we
|
|
200
|
+
// know.
|
|
201
|
+
group: (_update$group = update.group) !== null && _update$group !== void 0 ? _update$group : 'outsideEditor',
|
|
202
|
+
name: entry.name,
|
|
203
|
+
latencyMs: update.latencyMs,
|
|
204
|
+
target: this.describeTarget(entry.target)
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Frames reported before this entry already answer for it.
|
|
208
|
+
this.attribute(record);
|
|
209
|
+
return record;
|
|
210
|
+
}
|
|
211
|
+
}, {
|
|
212
|
+
key: "attribute",
|
|
213
|
+
value: function attribute(record) {
|
|
214
|
+
var attribution = record.boundaries && this.attributionFor(record.boundaries);
|
|
215
|
+
if (!attribution) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (record.attribution && sameAttribution(record.attribution, attribution)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
record.attribution = attribution;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
92
224
|
|
|
93
225
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
226
|
+
* What the frames say about an interaction, attributed the way `web-vitals` attributes INP: every
|
|
227
|
+
* frame overlapping the interaction counts, the script that counts is the one with the longest
|
|
228
|
+
* part inside it, and style and layout is summed across those frames.
|
|
229
|
+
*
|
|
230
|
+
* @returns nothing when no frame overlaps the interaction — the browser reports frames above
|
|
231
|
+
* 50 ms only.
|
|
98
232
|
*/
|
|
99
233
|
}, {
|
|
100
|
-
key: "
|
|
101
|
-
value: function
|
|
102
|
-
var
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
var
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
var
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
234
|
+
key: "attributionFor",
|
|
235
|
+
value: function attributionFor(boundaries) {
|
|
236
|
+
var _longestScript, _longestScript2, _longestScript3;
|
|
237
|
+
var overlapped = false;
|
|
238
|
+
var lastFrameEndTime = 0;
|
|
239
|
+
var totalScriptDurationMs = 0;
|
|
240
|
+
var totalStyleAndLayoutDurationMs = 0;
|
|
241
|
+
var longestScript;
|
|
242
|
+
var longestScriptMs = 0;
|
|
243
|
+
var _iterator2 = _createForOfIteratorHelper(this.frames),
|
|
244
|
+
_step2;
|
|
245
|
+
try {
|
|
246
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
247
|
+
var _frame$scripts;
|
|
248
|
+
var frame = _step2.value;
|
|
249
|
+
// Frames come in the order they were rendered, so once one starts after the interaction,
|
|
250
|
+
// so does every frame after it.
|
|
251
|
+
if (frame.startTime > boundaries.processingEndedAt) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
var frameEndTime = frame.startTime + frame.duration;
|
|
255
|
+
if (frameEndTime < boundaries.startedAt) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
overlapped = true;
|
|
259
|
+
lastFrameEndTime = frameEndTime;
|
|
260
|
+
totalStyleAndLayoutDurationMs += this.styleAndLayoutOf(frame);
|
|
261
|
+
var _iterator3 = _createForOfIteratorHelper((_frame$scripts = frame.scripts) !== null && _frame$scripts !== void 0 ? _frame$scripts : []),
|
|
262
|
+
_step3;
|
|
263
|
+
try {
|
|
264
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
265
|
+
var _script$forcedStyleAn;
|
|
266
|
+
var script = _step3.value;
|
|
267
|
+
var scriptEndTime = script.startTime + script.duration;
|
|
268
|
+
if (scriptEndTime < boundaries.startedAt) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
var insideInteractionMs = scriptEndTime - Math.max(boundaries.startedAt, script.startTime);
|
|
272
|
+
// `forcedStyleAndLayoutDuration` carries no timestamps, so the part of it inside the
|
|
273
|
+
// interaction is apportioned. It counts as style and layout rather than script time,
|
|
274
|
+
// the same split DevTools shows.
|
|
275
|
+
var forcedInsideMs = script.duration ? insideInteractionMs / script.duration * ((_script$forcedStyleAn = script.forcedStyleAndLayoutDuration) !== null && _script$forcedStyleAn !== void 0 ? _script$forcedStyleAn : 0) : 0;
|
|
276
|
+
totalScriptDurationMs += insideInteractionMs - forcedInsideMs;
|
|
277
|
+
totalStyleAndLayoutDurationMs += forcedInsideMs;
|
|
278
|
+
if (insideInteractionMs > longestScriptMs) {
|
|
279
|
+
longestScript = script;
|
|
280
|
+
longestScriptMs = insideInteractionMs;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
_iterator3.e(err);
|
|
285
|
+
} finally {
|
|
286
|
+
_iterator3.f();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
_iterator2.e(err);
|
|
291
|
+
} finally {
|
|
292
|
+
_iterator2.f();
|
|
293
|
+
}
|
|
294
|
+
if (!overlapped) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// What the browser did after the last frame of the interaction, so it only counts when that
|
|
299
|
+
// frame ended no earlier than the handlers did.
|
|
300
|
+
var totalPaintDurationMs = lastFrameEndTime >= boundaries.processingEndedAt ? Math.max(0, boundaries.presentedAt - lastFrameEndTime) : 0;
|
|
301
|
+
// Every total is brought to what it is reported as before this subtraction, so that the four
|
|
302
|
+
// of them add up to the latency rather than to more than it: a frame whose render phase runs
|
|
303
|
+
// past the interaction would otherwise leave a negative here to be counted twice.
|
|
304
|
+
totalScriptDurationMs = Math.max(0, totalScriptDurationMs);
|
|
305
|
+
totalStyleAndLayoutDurationMs = Math.max(0, totalStyleAndLayoutDurationMs);
|
|
306
|
+
// Whatever is left of the latency: the thread was busy with something the frames attributed
|
|
307
|
+
// to no script, to no style and layout, and to no paint.
|
|
308
|
+
var totalUnattributedDurationMs = Math.max(0, boundaries.presentedAt - boundaries.startedAt - totalScriptDurationMs - totalStyleAndLayoutDurationMs - totalPaintDurationMs);
|
|
113
309
|
return {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
310
|
+
functionName: this.truncated((_longestScript = longestScript) === null || _longestScript === void 0 ? void 0 : _longestScript.sourceFunctionName),
|
|
311
|
+
invokerType: this.truncated((_longestScript2 = longestScript) === null || _longestScript2 === void 0 ? void 0 : _longestScript2.invokerType),
|
|
312
|
+
longestScriptMs: longestScript && Math.round(longestScriptMs),
|
|
313
|
+
scriptName: this.truncated(this.fileName((_longestScript3 = longestScript) === null || _longestScript3 === void 0 ? void 0 : _longestScript3.sourceURL)),
|
|
314
|
+
scriptSubpart: longestScript && this.subpartOf(longestScript, boundaries),
|
|
315
|
+
totalPaintDurationMs: Math.round(totalPaintDurationMs),
|
|
316
|
+
totalScriptDurationMs: Math.round(totalScriptDurationMs),
|
|
317
|
+
totalStyleAndLayoutDurationMs: Math.round(totalStyleAndLayoutDurationMs),
|
|
318
|
+
totalUnattributedDurationMs: Math.round(totalUnattributedDurationMs)
|
|
122
319
|
};
|
|
123
320
|
}
|
|
124
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Style, layout and paint of the frame, which the browser reports as starting at 0 when the
|
|
324
|
+
* frame did none.
|
|
325
|
+
*/
|
|
326
|
+
}, {
|
|
327
|
+
key: "styleAndLayoutOf",
|
|
328
|
+
value: function styleAndLayoutOf(frame) {
|
|
329
|
+
var styleAndLayoutStart = frame.styleAndLayoutStart;
|
|
330
|
+
if (typeof styleAndLayoutStart !== 'number' || styleAndLayoutStart === 0) {
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
var frameEndTime = frame.startTime + frame.duration;
|
|
334
|
+
return Math.max(0, frameEndTime - styleAndLayoutStart);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Which phase of the interaction the script ran in, by where it started. */
|
|
338
|
+
}, {
|
|
339
|
+
key: "subpartOf",
|
|
340
|
+
value: function subpartOf(script, boundaries) {
|
|
341
|
+
if (script.startTime < boundaries.processingStartedAt) {
|
|
342
|
+
return 'inputDelay';
|
|
343
|
+
}
|
|
344
|
+
return script.startTime >= boundaries.processingEndedAt ? 'presentationDelay' : 'processing';
|
|
345
|
+
}
|
|
346
|
+
}, {
|
|
347
|
+
key: "truncated",
|
|
348
|
+
value: function truncated(name) {
|
|
349
|
+
return name ? name.slice(0, MAX_NAME_LENGTH) : undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The file as the browser named it, content hash and all: that is what identifies the artefact
|
|
354
|
+
* and its source map, and a query can be grouped away downstream.
|
|
355
|
+
*/
|
|
356
|
+
}, {
|
|
357
|
+
key: "fileName",
|
|
358
|
+
value: function fileName(sourceURL) {
|
|
359
|
+
if (!sourceURL) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
var path = sourceURL.split(QUERY_OR_HASH)[0];
|
|
363
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
364
|
+
}
|
|
365
|
+
|
|
125
366
|
/**
|
|
126
367
|
* Names the element an interaction happened on — `div[data-vc="x"] > p > span`, outermost first.
|
|
127
368
|
* The path climbs until an element carries an allow-listed attribute, because that is what says
|
|
@@ -36,21 +36,52 @@ export type SlowInteractionGroup = EditorInteractionGroupName | 'outsideEditor';
|
|
|
36
36
|
* One of the slowest interactions of the session: which target was slow, and where the time went,
|
|
37
37
|
* neither of which the histograms can answer.
|
|
38
38
|
*
|
|
39
|
-
* The three phases divide `durationMs` and add up to it within rounding.
|
|
39
|
+
* The three phases divide `durationMs` and add up to it within rounding, and so do the four totals.
|
|
40
|
+
* Everything but the phases comes from the Long Animation Frames the interaction ran in, so all of
|
|
41
|
+
* it is absent when the browser reported none — and the fields describing one script are absent as
|
|
42
|
+
* well when no script of those frames overlapped the interaction.
|
|
40
43
|
*/
|
|
41
44
|
export type SlowInteraction = {
|
|
42
45
|
durationMs: number;
|
|
46
|
+
/** Function the interaction's slowest script ran in. */
|
|
47
|
+
functionName?: string;
|
|
43
48
|
group: SlowInteractionGroup;
|
|
44
49
|
/** Time between the event arriving and its handlers starting to run. */
|
|
45
50
|
inputDelayMs?: number;
|
|
51
|
+
/**
|
|
52
|
+
* What ran that script, as the browser names it: `event-listener`, `user-callback`,
|
|
53
|
+
* `resolve-promise`, `classic-script` and so on.
|
|
54
|
+
*/
|
|
55
|
+
invokerType?: string;
|
|
56
|
+
/** How much of the interaction's slowest script fell inside the interaction. */
|
|
57
|
+
longestScriptMs?: number;
|
|
46
58
|
/** Type of the event the latency was measured on, which for a pointer press is usually `click`. */
|
|
47
59
|
name: string;
|
|
48
60
|
/** Time between the handlers finishing and the next frame being presented. */
|
|
49
61
|
presentationDelayMs?: number;
|
|
50
62
|
/** Time spent running the event's handlers. */
|
|
51
63
|
processingMs?: number;
|
|
64
|
+
/** The bundle that script came from, without its origin or query. */
|
|
65
|
+
scriptName?: string;
|
|
66
|
+
/** The phase of the interaction that script ran in. */
|
|
67
|
+
scriptSubpart?: 'inputDelay' | 'presentationDelay' | 'processing';
|
|
52
68
|
/** A short DOM path. Absent once the element has been removed from the document. */
|
|
53
69
|
target?: string;
|
|
70
|
+
/** Time between the last frame of the interaction ending and the screen updating. */
|
|
71
|
+
totalPaintDurationMs?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Script time inside the interaction across its frames, the part a script forced into style and
|
|
74
|
+
* layout excluded.
|
|
75
|
+
*/
|
|
76
|
+
totalScriptDurationMs?: number;
|
|
77
|
+
/** Style and layout across the frames of the interaction, the part forced from a script included. */
|
|
78
|
+
totalStyleAndLayoutDurationMs?: number;
|
|
79
|
+
/**
|
|
80
|
+
* The part of the latency the frames account for nothing in — the main thread was busy with
|
|
81
|
+
* something no Long Animation Frame attributed to a script, to style and layout, or to paint.
|
|
82
|
+
* Not idle time: the browser reports no frame under 50 ms, so the work of those lands here too.
|
|
83
|
+
*/
|
|
84
|
+
totalUnattributedDurationMs?: number;
|
|
54
85
|
};
|
|
55
86
|
/**
|
|
56
87
|
* Session-to-date latency distribution for one group of interactions.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** An array that forgets its oldest item once it holds more than `limit`. */
|
|
2
|
+
export declare class BoundedList<Item> {
|
|
3
|
+
private readonly limit;
|
|
4
|
+
private readonly items;
|
|
5
|
+
constructor(limit: number);
|
|
6
|
+
push(...items: Item[]): void;
|
|
7
|
+
[Symbol.iterator](): IterableIterator<Item>;
|
|
8
|
+
findLast(matches: (item: Item) => boolean): Item | undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A `Map` that forgets its oldest entry once it holds more than `limit`. */
|
|
2
|
+
export declare class BoundedMap<Key, Value> {
|
|
3
|
+
private readonly limit;
|
|
4
|
+
private readonly entries;
|
|
5
|
+
constructor(limit: number);
|
|
6
|
+
get(key: Key): Value | undefined;
|
|
7
|
+
forEach(visit: (value: Value, key: Key) => void): void;
|
|
8
|
+
set(key: Key, value: Value): void;
|
|
9
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InteractionGroupSnapshot } from '../analytics/interactivity-snapshot';
|
|
2
|
+
import type { InteractionUpdate } from './interaction-tracker';
|
|
2
3
|
/**
|
|
3
4
|
* The latencies of one set of interactions — every interaction on the page, or only the ones
|
|
4
5
|
* inside the editor — reported as one object in the event.
|
|
@@ -8,21 +9,33 @@ import type { InteractionGroupSnapshot } from '../analytics/interactivity-snapsh
|
|
|
8
9
|
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
|
|
9
10
|
* is taken. Nothing is computed while interactions arrive.
|
|
10
11
|
*
|
|
11
|
-
* The two counters count different populations: `
|
|
12
|
-
* measured, `countTotal` takes all of them, including the ones below the 16 ms
|
|
13
|
-
* it never delivers. So `totalCount >= observedCount`, and the difference is how
|
|
14
|
-
* fast to be measured.
|
|
12
|
+
* The two counters count different populations: `trackInteractionUpdate` takes the interactions
|
|
13
|
+
* Event Timing measured, `countTotal` takes all of them, including the ones below the 16 ms
|
|
14
|
+
* reporting threshold it never delivers. So `totalCount >= observedCount`, and the difference is how
|
|
15
|
+
* many were too fast to be measured.
|
|
15
16
|
*/
|
|
16
17
|
export declare class InteractionGroup {
|
|
17
18
|
private countByLatency;
|
|
18
19
|
private totalCount;
|
|
20
|
+
/**
|
|
21
|
+
* Takes in what the tracker now says about an interaction: a new one is counted, and one measured
|
|
22
|
+
* again moves the count it already has.
|
|
23
|
+
*
|
|
24
|
+
* @returns whether the group changed.
|
|
25
|
+
*/
|
|
26
|
+
trackInteractionUpdate(update: InteractionUpdate): boolean;
|
|
19
27
|
add(latencyMs: number): void;
|
|
20
28
|
/**
|
|
21
29
|
* Counts an interaction towards the group's total, measured or not. `page` has no use for it:
|
|
22
30
|
* `performance.interactionCount` counts the page's interactions.
|
|
23
31
|
*/
|
|
24
32
|
countTotal(): void;
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @returns whether the count moved, which is `false` when both latencies fall in the step the
|
|
35
|
+
* interaction is already counted in — including when the interaction was measured no slower at
|
|
36
|
+
* all and only its boundaries moved.
|
|
37
|
+
*/
|
|
38
|
+
remeasure(previousLatencyMs: number, latencyMs: number): boolean;
|
|
26
39
|
/**
|
|
27
40
|
* @param totalCount every interaction of the group, including those below the Event Timing
|
|
28
41
|
* reporting threshold. Defaults to what `countTotal` was told, which is where an editor
|
|
@@ -10,21 +10,37 @@ export type InteractionEntry = PerformanceEntry & {
|
|
|
10
10
|
processingStart?: number;
|
|
11
11
|
target?: Node | null;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* The four moments an interaction's latency divides at, in order: the user acted, its handlers
|
|
15
|
+
* started running, they finished, the screen updated.
|
|
16
|
+
*/
|
|
17
|
+
export type InteractionBoundaries = {
|
|
18
|
+
presentedAt: number;
|
|
19
|
+
processingEndedAt: number;
|
|
20
|
+
processingStartedAt: number;
|
|
21
|
+
startedAt: number;
|
|
22
|
+
};
|
|
13
23
|
/**
|
|
14
24
|
* What an entry did to the interaction it belongs to: either it is the first entry of a new
|
|
15
|
-
* interaction, or it
|
|
16
|
-
*
|
|
25
|
+
* interaction, or it changed an interaction already known. Both carry the editor group of the
|
|
26
|
+
* interaction, if it is one of the editor's, and the boundaries it now has.
|
|
27
|
+
*
|
|
28
|
+
* A `remeasured` where `previousLatencyMs` equals `latencyMs` is an interaction whose latency stayed
|
|
29
|
+
* as it was and whose boundaries moved: the entry ran in the same paint without being the slowest
|
|
30
|
+
* of them.
|
|
17
31
|
*/
|
|
18
32
|
export type InteractionUpdate = {
|
|
33
|
+
boundaries: InteractionBoundaries | undefined;
|
|
19
34
|
group: EditorInteractionGroupName | undefined;
|
|
20
35
|
interactionId: number;
|
|
21
36
|
latencyMs: number;
|
|
22
37
|
type: 'new';
|
|
23
38
|
} | {
|
|
24
|
-
|
|
39
|
+
boundaries: InteractionBoundaries | undefined;
|
|
25
40
|
group: EditorInteractionGroupName | undefined;
|
|
26
41
|
interactionId: number;
|
|
27
|
-
|
|
42
|
+
latencyMs: number;
|
|
43
|
+
previousLatencyMs: number;
|
|
28
44
|
type: 'remeasured';
|
|
29
45
|
};
|
|
30
46
|
/**
|
|
@@ -41,11 +57,15 @@ export type InteractionUpdate = {
|
|
|
41
57
|
*
|
|
42
58
|
* The editor's events answer what an entry cannot: which interactions were with the editor, and
|
|
43
59
|
* how many there were, including the ones below the Event Timing reporting threshold.
|
|
60
|
+
*
|
|
61
|
+
* Every entry is also placed in the paint that presented it, which is what says how an
|
|
62
|
+
* interaction's latency divides into waiting, processing and presentation. See `Paint`.
|
|
44
63
|
*/
|
|
45
64
|
export declare class InteractionTracker {
|
|
46
65
|
private readonly startsAfterInteractionId;
|
|
47
66
|
private readonly interactions;
|
|
48
67
|
private readonly groupByEvent;
|
|
68
|
+
private readonly recentPaints;
|
|
49
69
|
private highestInteractionId;
|
|
50
70
|
/**
|
|
51
71
|
* @param startsAfterInteractionId interactions up to and including this one belong to the
|
|
@@ -60,11 +80,40 @@ export declare class InteractionTracker {
|
|
|
60
80
|
/**
|
|
61
81
|
* Merges an entry into the interaction it belongs to.
|
|
62
82
|
*
|
|
63
|
-
* @returns what that
|
|
64
|
-
*
|
|
83
|
+
* @returns what that changed about the interactions this tracker knows, the entry's own first.
|
|
84
|
+
* More than one of them when the paint the entry ran in presented several.
|
|
65
85
|
*/
|
|
66
|
-
merge(entry: InteractionEntry): InteractionUpdate
|
|
67
|
-
/** @returns the group of an interaction to count, when this is the event its group counts on. */
|
|
86
|
+
merge(entry: InteractionEntry): InteractionUpdate[];
|
|
68
87
|
recordEditorEvent(event: Event): EditorInteractionGroupName | undefined;
|
|
69
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Every interaction whose boundaries are read from this paint, reported as measured again at the
|
|
90
|
+
* latency it already had.
|
|
91
|
+
*
|
|
92
|
+
* @param except the interaction the entry measured, which the caller reports itself. `0` or
|
|
93
|
+
* nothing when the entry measured none, and then no interaction is left out.
|
|
94
|
+
*/
|
|
95
|
+
private remeasuredUpdatesIn;
|
|
96
|
+
private newUpdate;
|
|
97
|
+
private remeasuredUpdate;
|
|
98
|
+
/**
|
|
99
|
+
* The four moments of an interaction, read from the paint as it stands now.
|
|
100
|
+
*
|
|
101
|
+
* Limited the way `web-vitals` limits its INP attribution, so the four stay in order: the paint's
|
|
102
|
+
* handlers can have started before the event arrived, and can have finished after the paint the
|
|
103
|
+
* event's rounded-down `duration` points at.
|
|
104
|
+
*
|
|
105
|
+
* @returns nothing when the browser reported no processing timestamps for the interaction, which
|
|
106
|
+
* leaves it in no paint.
|
|
107
|
+
*/
|
|
108
|
+
private boundariesOf;
|
|
109
|
+
/**
|
|
110
|
+
* The paint that presented this entry, grown to cover this entry's own processing.
|
|
111
|
+
*
|
|
112
|
+
* The moment being matched is always the one the first entry of the paint reported, so that a
|
|
113
|
+
* run of entries 8 ms apart cannot walk one paint across the next.
|
|
114
|
+
*
|
|
115
|
+
* @returns nothing when the browser reported no processing timestamps for the entry, which
|
|
116
|
+
* leaves nothing to place it by.
|
|
117
|
+
*/
|
|
118
|
+
private paintOf;
|
|
70
119
|
}
|