@atlaskit/editor-plugin-interactivity 1.0.0 → 1.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +79 -0
  3. package/afm-cc/tsconfig.json +3 -0
  4. package/afm-products/tsconfig.json +3 -0
  5. package/dist/cjs/collections/bounded-list.js +42 -0
  6. package/dist/cjs/collections/bounded-map.js +42 -0
  7. package/dist/cjs/collector/interaction-events.js +0 -7
  8. package/dist/cjs/collector/interaction-group.js +31 -4
  9. package/dist/cjs/collector/interaction-tracker.js +210 -68
  10. package/dist/cjs/collector/interactivity-collector.js +52 -22
  11. package/dist/cjs/collector/interactivity-session.js +3 -0
  12. package/dist/cjs/collector/long-animation-frame-observer.js +62 -0
  13. package/dist/cjs/collector/slow-interaction-list.js +413 -0
  14. package/dist/es2019/collections/bounded-list.js +24 -0
  15. package/dist/es2019/collections/bounded-map.js +25 -0
  16. package/dist/es2019/collector/interaction-events.js +0 -7
  17. package/dist/es2019/collector/interaction-group.js +28 -5
  18. package/dist/es2019/collector/interaction-tracker.js +201 -55
  19. package/dist/es2019/collector/interactivity-collector.js +35 -20
  20. package/dist/es2019/collector/interactivity-session.js +3 -0
  21. package/dist/es2019/collector/long-animation-frame-observer.js +42 -0
  22. package/dist/es2019/collector/slow-interaction-list.js +342 -0
  23. package/dist/esm/collections/bounded-list.js +35 -0
  24. package/dist/esm/collections/bounded-map.js +35 -0
  25. package/dist/esm/collector/interaction-events.js +0 -7
  26. package/dist/esm/collector/interaction-group.js +31 -5
  27. package/dist/esm/collector/interaction-tracker.js +210 -68
  28. package/dist/esm/collector/interactivity-collector.js +52 -22
  29. package/dist/esm/collector/interactivity-session.js +3 -0
  30. package/dist/esm/collector/long-animation-frame-observer.js +55 -0
  31. package/dist/esm/collector/slow-interaction-list.js +407 -0
  32. package/dist/types/analytics/interactivity-snapshot.d.ts +65 -0
  33. package/dist/types/collections/bounded-list.d.ts +9 -0
  34. package/dist/types/collections/bounded-map.d.ts +9 -0
  35. package/dist/types/collector/interaction-events.d.ts +1 -7
  36. package/dist/types/collector/interaction-group.d.ts +18 -5
  37. package/dist/types/collector/interaction-tracker.d.ts +67 -13
  38. package/dist/types/collector/interactivity-collector.d.ts +3 -0
  39. package/dist/types/collector/interactivity-session.d.ts +2 -0
  40. package/dist/types/collector/long-animation-frame-observer.d.ts +28 -0
  41. package/dist/types/collector/slow-interaction-list.d.ts +64 -0
  42. package/docs/0-intro.tsx +2 -1
  43. package/package.json +4 -3
@@ -0,0 +1,24 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ /** An array that forgets its oldest item once it holds more than `limit`. */
3
+ export class BoundedList {
4
+ constructor(limit) {
5
+ _defineProperty(this, "items", []);
6
+ this.limit = limit;
7
+ }
8
+ push(...items) {
9
+ this.items.push(...items);
10
+ // Negative when there is still room, and `splice` then removes nothing.
11
+ this.items.splice(0, this.items.length - this.limit);
12
+ }
13
+ [Symbol.iterator]() {
14
+ return this.items[Symbol.iterator]();
15
+ }
16
+ findLast(matches) {
17
+ for (let index = this.items.length - 1; index >= 0; index -= 1) {
18
+ if (matches(this.items[index])) {
19
+ return this.items[index];
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ }
@@ -0,0 +1,25 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ /** A `Map` that forgets its oldest entry once it holds more than `limit`. */
3
+ export class BoundedMap {
4
+ constructor(limit) {
5
+ _defineProperty(this, "entries", new Map());
6
+ this.limit = limit;
7
+ }
8
+ get(key) {
9
+ return this.entries.get(key);
10
+ }
11
+ forEach(visit) {
12
+ this.entries.forEach(visit);
13
+ }
14
+ set(key, value) {
15
+ this.entries.delete(key);
16
+ this.entries.set(key, value);
17
+ if (this.entries.size <= this.limit) {
18
+ return;
19
+ }
20
+ const oldest = this.entries.keys().next();
21
+ if (!oldest.done) {
22
+ this.entries.delete(oldest.value);
23
+ }
24
+ }
25
+ }
@@ -1,10 +1,3 @@
1
- /**
2
- * The editor group an interaction belongs to, which is also the field the event reports it in.
3
- *
4
- * `editorOther` is the remainder — an interaction the browser counts that is neither typing nor
5
- * pointing — so the three groups together cover whatever the browser calls an interaction.
6
- */
7
-
8
1
  /**
9
2
  * The events an interaction is made of, and the group they belong to. Both sides of the collector
10
3
  * read this, so counting and grouping cannot disagree.
@@ -1,6 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { bucketKeyForMs } from './bucket-boundaries';
3
-
4
3
  /**
5
4
  * Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
6
5
  * latency is rounded up to this step on the way in, which caps the number of distinct values
@@ -20,16 +19,29 @@ const REPORTED_QUANTILES = [0.9, 0.98];
20
19
  * count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
21
20
  * is taken. Nothing is computed while interactions arrive.
22
21
  *
23
- * The two counters count different populations: `add` takes the interactions Event Timing
24
- * measured, `countTotal` takes all of them, including the ones below the 16 ms reporting threshold
25
- * it never delivers. So `totalCount >= observedCount`, and the difference is how many were too
26
- * fast to be measured.
22
+ * The two counters count different populations: `trackInteractionUpdate` takes the interactions
23
+ * Event Timing measured, `countTotal` takes all of them, including the ones below the 16 ms
24
+ * reporting threshold it never delivers. So `totalCount >= observedCount`, and the difference is how
25
+ * many were too fast to be measured.
27
26
  */
28
27
  export class InteractionGroup {
29
28
  constructor() {
30
29
  _defineProperty(this, "countByLatency", new Map());
31
30
  _defineProperty(this, "totalCount", 0);
32
31
  }
32
+ /**
33
+ * Takes in what the tracker now says about an interaction: a new one is counted, and one measured
34
+ * again moves the count it already has.
35
+ *
36
+ * @returns whether the group changed.
37
+ */
38
+ trackInteractionUpdate(update) {
39
+ if (update.type === 'new') {
40
+ this.add(update.latencyMs);
41
+ return true;
42
+ }
43
+ return this.remeasure(update.previousLatencyMs, update.latencyMs);
44
+ }
33
45
  add(latencyMs) {
34
46
  this.increment(latencyMs);
35
47
  }
@@ -41,10 +53,21 @@ export class InteractionGroup {
41
53
  countTotal() {
42
54
  this.totalCount += 1;
43
55
  }
56
+
57
+ /**
58
+ * @returns whether the count moved, which is `false` when both latencies fall in the step the
59
+ * interaction is already counted in — including when the interaction was measured no slower at
60
+ * all and only its boundaries moved.
61
+ */
44
62
  remeasure(previousLatencyMs, latencyMs) {
63
+ if (this.roundLatencyUp(previousLatencyMs) === this.roundLatencyUp(latencyMs)) {
64
+ return false;
65
+ }
66
+
45
67
  // Moved rather than counted again: the count belongs to the same interaction.
46
68
  this.decrement(previousLatencyMs);
47
69
  this.increment(latencyMs);
70
+ return true;
48
71
  }
49
72
 
50
73
  /**
@@ -1,16 +1,51 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { BoundedList } from '../collections/bounded-list';
3
+ import { BoundedMap } from '../collections/bounded-map';
2
4
  import { interactionEventKind } from './interaction-events';
3
5
 
4
6
  /**
5
- * The Event Timing fields this package reads. `interactionId` is missing from the DOM
6
- * typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
7
- * a `PerformanceEntry` from `getEntries()` assignable without a cast.
7
+ * The Event Timing fields this package reads, declared optional so that a `PerformanceEntry` from
8
+ * `getEntries()` is assignable without a cast `interactionId` is missing from the DOM typings'
9
+ * `PerformanceEventTiming` altogether, and the rest are only on it.
10
+ */
11
+
12
+ /**
13
+ * One paint, and the processing of every event it presented.
14
+ *
15
+ * One paint can present several events, and the handlers of all of them ran before it: a
16
+ * `pointerover` handler that was still running when the user clicked held up the paint that showed
17
+ * the click. So an interaction's processing is the processing of its whole paint, not of its own
18
+ * events only — otherwise a handler that is not its own reads as time the user waited for nothing.
19
+ *
20
+ * Event Timing gives a paint no identity. The only thing an entry says about it is
21
+ * `startTime + duration`, the moment it happened, so that is what identifies it.
22
+ */
23
+
24
+ /**
25
+ * The paint an entry was presented by, and whether the entry moved the processing that paint covers.
26
+ * An entry whose handlers ran inside what the paint already covered changes nothing for any
27
+ * interaction reading its boundaries from it.
28
+ */
29
+
30
+ /**
31
+ * The four moments an interaction's latency divides at, in order: the user acted, its handlers
32
+ * started running, they finished, the screen updated.
8
33
  */
9
34
 
10
35
  /**
11
36
  * What an entry did to the interaction it belongs to: either it is the first entry of a new
12
- * interaction, or it measured an interaction that was already counted as slower than it was
13
- * known to be. Both carry the editor group of the interaction, if it is one of the editor's.
37
+ * interaction, or it changed an interaction already known. Both carry the editor group of the
38
+ * interaction, if it is one of the editor's, and the boundaries it now has.
39
+ *
40
+ * A `remeasured` where `previousLatencyMs` equals `latencyMs` is an interaction whose latency stayed
41
+ * as it was and whose boundaries moved: the entry ran in the same paint without being the slowest
42
+ * of them.
43
+ */
44
+
45
+ /**
46
+ * What is kept per interaction. The boundaries are not among these: they are derived from the paint
47
+ * whenever the interaction is reported, because the paint keeps growing as the browser reports the
48
+ * remaining events it presented.
14
49
  */
15
50
 
16
51
  /**
@@ -19,11 +54,17 @@ import { interactionEventKind } from './interaction-events';
19
54
  * anything older than the last few hundred is not needed.
20
55
  */
21
56
  const MAX_TRACKED = 256;
57
+
58
+ /**
59
+ * How many paints entries can still be placed in. The entries of a paint arrive within a batch or
60
+ * two of each other, so this only has to cover the paints in flight; it is what `web-vitals` keeps.
61
+ */
62
+ const MAX_RECENT_PAINTS = 10;
22
63
  /**
23
- * How many are dropped per cleanup. Dropping one at a time would run a cleanup on every new
24
- * interaction once the limit is reached; a batch makes it one cleanup per 64 of them.
64
+ * Event Timing rounds `duration` down to 8 ms, so two events presented by one paint report that
65
+ * paint up to this far apart and nothing else in Event Timing says they share it.
25
66
  */
26
- const PRUNE_BATCH = 64;
67
+ const PRESENTATION_ROUNDING_MS = 8;
27
68
 
28
69
  /**
29
70
  * Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
@@ -49,6 +90,9 @@ function eventKey(type, timeStamp) {
49
90
  *
50
91
  * The editor's events answer what an entry cannot: which interactions were with the editor, and
51
92
  * how many there were, including the ones below the Event Timing reporting threshold.
93
+ *
94
+ * Every entry is also placed in the paint that presented it, which is what says how an
95
+ * interaction's latency divides into waiting, processing and presentation. See `Paint`.
52
96
  */
53
97
  export class InteractionTracker {
54
98
  /**
@@ -59,8 +103,9 @@ export class InteractionTracker {
59
103
  * be counted in both.
60
104
  */
61
105
  constructor(startsAfterInteractionId = 0) {
62
- _defineProperty(this, "interactions", new Map());
63
- _defineProperty(this, "groupByEvent", new Map());
106
+ _defineProperty(this, "interactions", new BoundedMap(MAX_TRACKED));
107
+ _defineProperty(this, "groupByEvent", new BoundedMap(MAX_TRACKED));
108
+ _defineProperty(this, "recentPaints", new BoundedList(MAX_RECENT_PAINTS));
64
109
  _defineProperty(this, "highestInteractionId", 0);
65
110
  this.startsAfterInteractionId = startsAfterInteractionId;
66
111
  }
@@ -73,58 +118,67 @@ export class InteractionTracker {
73
118
  /**
74
119
  * Merges an entry into the interaction it belongs to.
75
120
  *
76
- * @returns what that did to the interaction's latency, or nothing when the entry is not
77
- * part of an interaction, belongs to a previous tracker, or does not change one.
121
+ * @returns what that changed about the interactions this tracker knows, the entry's own first.
122
+ * More than one of them when the paint the entry ran in presented several.
78
123
  */
79
124
  merge(entry) {
125
+ if (!Number.isFinite(entry.duration) || entry.duration < 0) {
126
+ return [];
127
+ }
128
+ const placement = this.paintOf(entry);
129
+ const paint = placement === null || placement === void 0 ? void 0 : placement.paint;
80
130
  const {
81
131
  interactionId
82
132
  } = entry;
83
- // `first-input` and non-interaction events report `interactionId` 0.
133
+
134
+ // Reported only when the entry grew the paint, because otherwise nothing an interaction reads
135
+ // from it moved. Every interaction the paint presented is here, not only the entry's own: a
136
+ // `first-input` or non-interaction event reports `interactionId` 0 and has none of its own,
137
+ // and a second press of the same paint moved where the first one spent its latency.
138
+ //
139
+ // The check below is neither reached with a `0` nor needed: the interactions reported are the
140
+ // ones this tracker holds, and the only way into that map is past the check.
141
+ const remeasuredOthers = placement !== null && placement !== void 0 && placement.grew ? this.remeasuredUpdatesIn(placement.paint, {
142
+ except: interactionId
143
+ }) : [];
84
144
  if (!interactionId) {
85
- return undefined;
145
+ return remeasuredOthers;
86
146
  }
87
147
  if (interactionId <= this.startsAfterInteractionId) {
88
- return undefined;
148
+ // The entry belongs to the session before this one, but its handlers still ran before a
149
+ // paint of this one.
150
+ return remeasuredOthers;
89
151
  }
90
152
  this.highestInteractionId = Math.max(this.highestInteractionId, interactionId);
91
-
92
- // Every latency is counted through here, so this is where one that cannot be measured
93
- // is rejected: a `NaN` getting through becomes a `NaN` bucket key and a `NaN` `sumMs`
94
- // for the rest of the session.
95
- if (!Number.isFinite(entry.duration) || entry.duration < 0) {
96
- return undefined;
97
- }
98
153
  const tracked = this.interactions.get(interactionId);
99
154
  if (tracked === undefined) {
100
155
  // Taken once: an entry that only makes the interaction slower has to move its count
101
156
  // within the group it was counted in, not into another one.
102
157
  const group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
103
- this.interactions.set(interactionId, {
158
+ const interaction = {
159
+ group,
104
160
  latencyMs: entry.duration,
105
- group
106
- });
107
- this.prune(this.interactions);
108
- return {
109
- type: 'new',
110
- latencyMs: entry.duration,
111
- group
161
+ presentedIn: paint,
162
+ startedAt: entry.startTime
112
163
  };
164
+ this.interactions.set(interactionId, interaction);
165
+ return [this.newUpdate(interactionId, interaction), ...remeasuredOthers];
113
166
  }
114
- if (entry.duration <= tracked.latencyMs) {
115
- return undefined;
167
+ if (entry.duration > tracked.latencyMs) {
168
+ const previousLatencyMs = tracked.latencyMs;
169
+ tracked.latencyMs = entry.duration;
170
+ tracked.presentedIn = paint;
171
+ tracked.startedAt = entry.startTime;
172
+ return [this.remeasuredUpdate(interactionId, tracked, previousLatencyMs), ...remeasuredOthers];
116
173
  }
117
- const fromMs = tracked.latencyMs;
118
- tracked.latencyMs = entry.duration;
119
- return {
120
- type: 'remeasured',
121
- fromMs,
122
- toMs: entry.duration,
123
- group: tracked.group
124
- };
125
- }
126
174
 
127
- /** @returns the group of an interaction to count, when this is the event its group counts on. */
175
+ // Not the slowest entry of the interaction, so its latency stands. Its handlers still ran
176
+ // before the same paint, if this is that paint, and so moved where that latency went.
177
+ if (!(placement !== null && placement !== void 0 && placement.grew) || paint !== tracked.presentedIn) {
178
+ return remeasuredOthers;
179
+ }
180
+ return [this.remeasuredUpdate(interactionId, tracked, tracked.latencyMs), ...remeasuredOthers];
181
+ }
128
182
  recordEditorEvent(event) {
129
183
  const kind = interactionEventKind(event.type);
130
184
  if (!kind) {
@@ -134,23 +188,115 @@ export class InteractionTracker {
134
188
  // Every event of the interaction, because any of them can be the one Event Timing reports
135
189
  // as the slowest: for a pointer press that is usually the click.
136
190
  this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
137
- this.prune(this.groupByEvent);
138
191
  return kind.counts ? kind.group : undefined;
139
192
  }
140
- prune(entries) {
141
- if (entries.size <= MAX_TRACKED) {
142
- return;
143
- }
144
193
 
145
- // `Map` preserves insertion order, so the entries inserted first are the least likely to
146
- // see another entry or another event.
147
- let remaining = PRUNE_BATCH;
148
- for (const key of entries.keys()) {
149
- entries.delete(key);
150
- remaining -= 1;
151
- if (remaining === 0) {
152
- return;
194
+ /**
195
+ * Every interaction whose boundaries are read from this paint, reported as measured again at the
196
+ * latency it already had.
197
+ *
198
+ * @param except the interaction the entry measured, which the caller reports itself. `0` or
199
+ * nothing when the entry measured none, and then no interaction is left out.
200
+ */
201
+ remeasuredUpdatesIn(paint, {
202
+ except
203
+ }) {
204
+ const remeasured = [];
205
+ this.interactions.forEach((interaction, interactionId) => {
206
+ if (interaction.presentedIn === paint && interactionId !== except) {
207
+ remeasured.push(this.remeasuredUpdate(interactionId, interaction, interaction.latencyMs));
153
208
  }
209
+ });
210
+ return remeasured;
211
+ }
212
+ newUpdate(interactionId, tracked) {
213
+ return {
214
+ type: 'new',
215
+ interactionId,
216
+ latencyMs: tracked.latencyMs,
217
+ group: tracked.group,
218
+ boundaries: this.boundariesOf(tracked)
219
+ };
220
+ }
221
+ remeasuredUpdate(interactionId, tracked, previousLatencyMs) {
222
+ return {
223
+ type: 'remeasured',
224
+ interactionId,
225
+ previousLatencyMs,
226
+ latencyMs: tracked.latencyMs,
227
+ group: tracked.group,
228
+ boundaries: this.boundariesOf(tracked)
229
+ };
230
+ }
231
+
232
+ /**
233
+ * The four moments of an interaction, read from the paint as it stands now.
234
+ *
235
+ * Limited the way `web-vitals` limits its INP attribution, so the four stay in order: the paint's
236
+ * handlers can have started before the event arrived, and can have finished after the paint the
237
+ * event's rounded-down `duration` points at.
238
+ *
239
+ * @returns nothing when the browser reported no processing timestamps for the interaction, which
240
+ * leaves it in no paint.
241
+ */
242
+ boundariesOf({
243
+ latencyMs,
244
+ presentedIn,
245
+ startedAt
246
+ }) {
247
+ if (!presentedIn) {
248
+ return undefined;
154
249
  }
250
+ const processingStartedAt = Math.max(presentedIn.processingStartedAt, startedAt);
251
+ const presentedAt = Math.max(startedAt + latencyMs, processingStartedAt);
252
+ const processingEndedAt = Math.min(presentedIn.processingEndedAt, presentedAt);
253
+ return {
254
+ startedAt,
255
+ processingStartedAt,
256
+ processingEndedAt,
257
+ presentedAt
258
+ };
259
+ }
260
+
261
+ /**
262
+ * The paint that presented this entry, grown to cover this entry's own processing.
263
+ *
264
+ * The moment being matched is always the one the first entry of the paint reported, so that a
265
+ * run of entries 8 ms apart cannot walk one paint across the next.
266
+ *
267
+ * @returns nothing when the browser reported no processing timestamps for the entry, which
268
+ * leaves nothing to place it by.
269
+ */
270
+ paintOf(entry) {
271
+ const {
272
+ startTime,
273
+ duration,
274
+ processingStart,
275
+ processingEnd
276
+ } = entry;
277
+ if (typeof processingStart !== 'number' || typeof processingEnd !== 'number') {
278
+ return undefined;
279
+ }
280
+ const presentedAt = startTime + duration;
281
+ const knownPaint = this.recentPaints.findLast(paint => Math.abs(presentedAt - paint.presentedAt) <= PRESENTATION_ROUNDING_MS);
282
+ if (knownPaint) {
283
+ const grew = processingStart < knownPaint.processingStartedAt || processingEnd > knownPaint.processingEndedAt;
284
+ knownPaint.processingStartedAt = Math.min(processingStart, knownPaint.processingStartedAt);
285
+ knownPaint.processingEndedAt = Math.max(processingEnd, knownPaint.processingEndedAt);
286
+ return {
287
+ grew,
288
+ paint: knownPaint
289
+ };
290
+ }
291
+ const newPaint = {
292
+ presentedAt,
293
+ processingStartedAt: processingStart,
294
+ processingEndedAt: processingEnd
295
+ };
296
+ this.recentPaints.push(newPaint);
297
+ return {
298
+ grew: true,
299
+ paint: newPaint
300
+ };
155
301
  }
156
302
  }
@@ -1,10 +1,12 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { getDocument } from '@atlaskit/browser-apis';
3
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
3
4
  import { EditorEventObserver } from './editor-event-observer';
4
5
  import { InteractionObserver } from './interaction-observer';
5
6
  import { InteractivitySession } from './interactivity-session';
6
7
  import { SCHEMA_VERSION } from './bucket-boundaries';
7
8
  import { LifecycleObserver } from './lifecycle-observer';
9
+ import { LongAnimationFrameObserver } from './long-animation-frame-observer';
8
10
  import { SnapshotScheduler } from './snapshot-scheduler';
9
11
  /**
10
12
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
@@ -27,6 +29,7 @@ export class InteractivityCollector {
27
29
  this.getObjectId = options.getObjectId;
28
30
  this.getSessionMode = options.getSessionMode;
29
31
  this.interactionObserver = new InteractionObserver(entries => this.recordEntries(entries));
32
+ this.longAnimationFrameObserver = isExperimentEnabled('platform_editor_editor_interactivity_slowest') ? new LongAnimationFrameObserver(frames => this.recordFrames(frames)) : undefined;
30
33
  this.editorEvents = new EditorEventObserver(event => this.recordEditorEvent(event));
31
34
  this.snapshotScheduler = new SnapshotScheduler(() => this.onTimer());
32
35
  this.lifecycleObserver = new LifecycleObserver({
@@ -45,6 +48,7 @@ export class InteractivityCollector {
45
48
  * when it has already been stopped.
46
49
  */
47
50
  start() {
51
+ var _this$longAnimationFr;
48
52
  // Collecting is over for good once stopped: the session it covered has been reported.
49
53
  if (this.stopped) {
50
54
  return false;
@@ -60,6 +64,7 @@ export class InteractivityCollector {
60
64
  // this object was constructed.
61
65
  this.session = this.createSession(0);
62
66
  this.interactionObserver.start();
67
+ (_this$longAnimationFr = this.longAnimationFrameObserver) === null || _this$longAnimationFr === void 0 ? void 0 : _this$longAnimationFr.start();
63
68
  this.editorEvents.observe(this.editorRoot);
64
69
  this.snapshotScheduler.start();
65
70
  this.lifecycleObserver.start();
@@ -69,12 +74,14 @@ export class InteractivityCollector {
69
74
 
70
75
  /** Reports the session as ended by the editor unmounting, and stops collecting. */
71
76
  stop() {
77
+ var _this$longAnimationFr2;
72
78
  if (this.stopped) {
73
79
  return;
74
80
  }
75
81
  this.takeSnapshot('unmount');
76
82
  this.stopped = true;
77
83
  this.interactionObserver.stop();
84
+ (_this$longAnimationFr2 = this.longAnimationFrameObserver) === null || _this$longAnimationFr2 === void 0 ? void 0 : _this$longAnimationFr2.stop();
78
85
  this.editorEvents.stop();
79
86
  this.snapshotScheduler.stop();
80
87
  this.lifecycleObserver.stop();
@@ -200,31 +207,34 @@ export class InteractivityCollector {
200
207
  this.session[group].countTotal();
201
208
  this.session.revision += 1;
202
209
  }
210
+
211
+ /** Long Animation Frames say where the latency of a slow interaction went. */
212
+ recordFrames(frames) {
213
+ var _this$session$slowest;
214
+ if (this.stopped) {
215
+ return;
216
+ }
217
+ if ((_this$session$slowest = this.session.slowest) !== null && _this$session$slowest !== void 0 && _this$session$slowest.trackLongAnimationFrames(frames)) {
218
+ this.session.revision += 1;
219
+ }
220
+ }
203
221
  recordEntries(entries) {
204
- const {
205
- tracker,
206
- page
207
- } = this.session;
208
222
  for (const entry of entries) {
209
- const update = tracker.merge(entry);
210
- if (!update) {
211
- continue;
212
- }
213
- if (update.type === 'new') {
214
- page.add(update.latencyMs);
223
+ for (const update of this.session.tracker.merge(entry)) {
224
+ var _this$session$slowest2;
225
+ let groupsChanged = this.session.page.trackInteractionUpdate(update);
215
226
  if (update.group) {
216
- this.session[update.group].add(update.latencyMs);
227
+ groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged;
217
228
  }
218
- } else {
219
- page.remeasure(update.fromMs, update.toMs);
220
- if (update.group) {
221
- this.session[update.group].remeasure(update.fromMs, update.toMs);
229
+ const slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update);
230
+ if (groupsChanged || slowestChanged) {
231
+ this.session.revision += 1;
222
232
  }
223
233
  }
224
- this.session.revision += 1;
225
234
  }
226
235
  }
227
236
  takeSnapshot(reason) {
237
+ var _this$longAnimationFr3, _session$slowest;
228
238
  if (this.stopped) {
229
239
  return;
230
240
  }
@@ -238,10 +248,11 @@ export class InteractivityCollector {
238
248
  return;
239
249
  }
240
250
 
241
- // Must run before the change check: the browser may be holding entries that have not
242
- // reached the observer callback yet, and on `pagehide` there is no later chance to
243
- // pick them up.
251
+ // Must run before the change check: the browser may be holding entries and frames that have
252
+ // not reached the observer callbacks yet, and on `pagehide` there is no later chance to
253
+ // pick them up. Entries first, so a record the frames answer for exists by then.
244
254
  this.interactionObserver.drain();
255
+ (_this$longAnimationFr3 = this.longAnimationFrameObserver) === null || _this$longAnimationFr3 === void 0 ? void 0 : _this$longAnimationFr3.drain();
245
256
  if (session.revision === session.emittedRevision) {
246
257
  return;
247
258
  }
@@ -253,6 +264,7 @@ export class InteractivityCollector {
253
264
  const now = performance.now();
254
265
  const hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince);
255
266
  const pageTotalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
267
+ const slowest = (_session$slowest = session.slowest) === null || _session$slowest === void 0 ? void 0 : _session$slowest.snapshot();
256
268
  this.emit({
257
269
  schema: SCHEMA_VERSION,
258
270
  interactivitySessionId: session.id,
@@ -268,7 +280,10 @@ export class InteractivityCollector {
268
280
  page: session.page.snapshot(pageTotalCount),
269
281
  editorTyping: session.editorTyping.snapshot(),
270
282
  editorPointer: session.editorPointer.snapshot(),
271
- editorOther: session.editorOther.snapshot()
283
+ editorOther: session.editorOther.snapshot(),
284
+ ...(slowest && {
285
+ slowest
286
+ })
272
287
  });
273
288
  }
274
289
  }
@@ -1,7 +1,9 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
2
3
  import { InteractionObserver } from './interaction-observer';
3
4
  import { InteractionTracker } from './interaction-tracker';
4
5
  import { InteractionGroup } from './interaction-group';
6
+ import { SlowInteractionList } from './slow-interaction-list';
5
7
  function createSessionId() {
6
8
  if (typeof crypto.randomUUID === 'function') {
7
9
  return crypto.randomUUID();
@@ -26,6 +28,7 @@ export class InteractivitySession {
26
28
  _defineProperty(this, "editorTyping", new InteractionGroup());
27
29
  _defineProperty(this, "editorPointer", new InteractionGroup());
28
30
  _defineProperty(this, "editorOther", new InteractionGroup());
31
+ _defineProperty(this, "slowest", isExperimentEnabled('platform_editor_editor_interactivity_slowest') ? new SlowInteractionList() : undefined);
29
32
  /** Increments per snapshot; a query takes the highest one per session. */
30
33
  _defineProperty(this, "seq", 0);
31
34
  _defineProperty(this, "hiddenMs", 0);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Reports the frames the browser took longer than 50 ms to render to `onFrames`. They are what says
3
+ * where the time of a slow interaction went — which script ran the longest while the user waited,
4
+ * and how much of the wait was style and layout — which Event Timing cannot answer.
5
+ *
6
+ * What they say about an interaction is `SlowInteractionList`'s to work out; this only observes.
7
+ */
8
+ export class LongAnimationFrameObserver {
9
+ static isSupported() {
10
+ return typeof PerformanceObserver !== 'undefined' && PerformanceObserver.supportedEntryTypes.includes('long-animation-frame');
11
+ }
12
+ constructor(onFrames) {
13
+ this.onFrames = onFrames;
14
+ }
15
+ start() {
16
+ if (this.observer || !LongAnimationFrameObserver.isSupported()) {
17
+ return;
18
+ }
19
+ this.observer = new PerformanceObserver(list => {
20
+ this.onFrames(list.getEntries());
21
+ });
22
+
23
+ // Buffered, as `web-vitals` observes them: a frame reported before the editor mounted can
24
+ // still be the one an interaction right after it ran in.
25
+ this.observer.observe({
26
+ type: 'long-animation-frame',
27
+ buffered: true
28
+ });
29
+ }
30
+ drain() {
31
+ var _this$observer;
32
+ const frames = (_this$observer = this.observer) === null || _this$observer === void 0 ? void 0 : _this$observer.takeRecords();
33
+ if (frames) {
34
+ this.onFrames(frames);
35
+ }
36
+ }
37
+ stop() {
38
+ var _this$observer2;
39
+ (_this$observer2 = this.observer) === null || _this$observer2 === void 0 ? void 0 : _this$observer2.disconnect();
40
+ this.observer = undefined;
41
+ }
42
+ }