@atlaskit/editor-plugin-limited-mode 13.0.18 → 14.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 +42 -0
- package/afm-cc/tsconfig.json +9 -0
- package/dist/cjs/limitedModePlugin.js +7 -4
- package/dist/cjs/pm-plugins/main.js +201 -18
- package/dist/cjs/pm-plugins/plugin-key.js +8 -0
- package/dist/cjs/pm-plugins/utils/latch-detector-types.js +5 -0
- package/dist/cjs/pm-plugins/utils/latch-detector.js +108 -0
- package/dist/cjs/pm-plugins/utils/latch-policy-types.js +1 -0
- package/dist/cjs/pm-plugins/utils/latch-policy.js +314 -0
- package/dist/es2019/limitedModePlugin.js +4 -3
- package/dist/es2019/pm-plugins/main.js +195 -16
- package/dist/es2019/pm-plugins/plugin-key.js +2 -0
- package/dist/es2019/pm-plugins/utils/latch-detector-types.js +1 -0
- package/dist/es2019/pm-plugins/utils/latch-detector.js +92 -0
- package/dist/es2019/pm-plugins/utils/latch-policy-types.js +0 -0
- package/dist/es2019/pm-plugins/utils/latch-policy.js +277 -0
- package/dist/esm/limitedModePlugin.js +6 -3
- package/dist/esm/pm-plugins/main.js +199 -16
- package/dist/esm/pm-plugins/plugin-key.js +2 -0
- package/dist/esm/pm-plugins/utils/latch-detector-types.js +1 -0
- package/dist/esm/pm-plugins/utils/latch-detector.js +102 -0
- package/dist/esm/pm-plugins/utils/latch-policy-types.js +0 -0
- package/dist/esm/pm-plugins/utils/latch-policy.js +307 -0
- package/dist/types/limitedModePluginType.d.ts +34 -1
- package/dist/types/pm-plugins/main.d.ts +12 -4
- package/dist/types/pm-plugins/plugin-key.d.ts +2 -0
- package/dist/types/pm-plugins/utils/latch-detector-types.d.ts +30 -0
- package/dist/types/pm-plugins/utils/latch-detector.d.ts +15 -0
- package/dist/types/pm-plugins/utils/latch-policy-types.d.ts +150 -0
- package/dist/types/pm-plugins/utils/latch-policy.d.ts +112 -0
- package/package.json +7 -3
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import { median } from '@atlaskit/editor-common/median';
|
|
3
|
+
import { shouldEnableLimitedModeForDocument } from '@atlaskit/editor-common/should-enable-limited-mode';
|
|
4
|
+
/**
|
|
5
|
+
* The shipped policy.
|
|
6
|
+
*
|
|
7
|
+
* These values add up to: nothing counts for the first 10s; then a window qualifies on either 6 of
|
|
8
|
+
* 12 keystrokes slower than 100ms with that window's median also over 100ms, or 3 long tasks over
|
|
9
|
+
* 600ms within 30s corroborated by a slow keystroke in that same 30s. Two qualifying windows at
|
|
10
|
+
* least 30s apart latch limited mode.
|
|
11
|
+
*
|
|
12
|
+
* `freezeTaskMs` matches `DEFAULT_FREEZE_THRESHOLD` in
|
|
13
|
+
* `editor-plugin-base/src/pm-plugins/frozen-editor.ts`, which backs the existing
|
|
14
|
+
* `ACTION.BROWSER_FREEZE` telemetry, so production dashboards can be used to calibrate it.
|
|
15
|
+
* `slowInputMs` is deliberately tighter than that file's `DEFAULT_SLOW_THRESHOLD` of 300 — this
|
|
16
|
+
* needs to notice a degraded experience, not just an unusable one.
|
|
17
|
+
*
|
|
18
|
+
* `requiredConfirmations` and `confirmationGapMs` are the values that matter most — see the comment
|
|
19
|
+
* on the former.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_LATCH_POLICY_CONFIG = {
|
|
22
|
+
warmUpMs: 10_000,
|
|
23
|
+
slowInputMs: 100,
|
|
24
|
+
latencyWindowSize: 12,
|
|
25
|
+
latencySlowSamplesRequired: 6,
|
|
26
|
+
freezeTaskMs: 600,
|
|
27
|
+
freezeTasksRequired: 3,
|
|
28
|
+
freezeWindowMs: 30_000,
|
|
29
|
+
requiredConfirmations: 2,
|
|
30
|
+
confirmationGapMs: 30_000,
|
|
31
|
+
bulkChangeNodeSize: 100,
|
|
32
|
+
bulkChangeSuppressionMs: 2_000,
|
|
33
|
+
docSizeThreshold: 750_000,
|
|
34
|
+
nodeCountThreshold: 5_000
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decides whether limited mode should be on.
|
|
39
|
+
*
|
|
40
|
+
* The single authority for that question, covering both reasons:
|
|
41
|
+
*
|
|
42
|
+
* - **The document** — too large, too many nodes, or containing a legacy content macro. Evaluated on
|
|
43
|
+
* load and on document replacement, so it can turn back off (a `replaceDocument` onto a smaller
|
|
44
|
+
* page) without costing a full-document walk per transaction.
|
|
45
|
+
* - **The device** — sustained slow keystrokes or repeated long tasks. **One-way**: once the runtime
|
|
46
|
+
* bar is met the policy stops evaluating, so the editor can never oscillate between modes.
|
|
47
|
+
*
|
|
48
|
+
* `isBreached()` is the combined verdict. Everything tunable is in `config`, so the whole high bar is
|
|
49
|
+
* unit-testable without needing to make a real browser slow, and a caller can substitute a
|
|
50
|
+
* differently configured policy. `latch-detector.ts` owns the browser plumbing that feeds the runtime
|
|
51
|
+
* criteria, and takes a policy instance rather than constructing one.
|
|
52
|
+
*/
|
|
53
|
+
export class LatchPolicy {
|
|
54
|
+
constructor({
|
|
55
|
+
now,
|
|
56
|
+
config
|
|
57
|
+
}) {
|
|
58
|
+
/** Public so the detector can read the tunables it needs rather than duplicating them. */
|
|
59
|
+
/** Public so the detector shares one clock with the policy. */
|
|
60
|
+
_defineProperty(this, "latencySamples", []);
|
|
61
|
+
_defineProperty(this, "freezeTimes", []);
|
|
62
|
+
_defineProperty(this, "qualifiedWindows", 0);
|
|
63
|
+
_defineProperty(this, "suppressedUntil", 0);
|
|
64
|
+
_defineProperty(this, "latched", false);
|
|
65
|
+
_defineProperty(this, "documentBreached", false);
|
|
66
|
+
/** Cumulative for the session and never cleared, unlike the evidence buffers. */
|
|
67
|
+
_defineProperty(this, "totalInputSamples", 0);
|
|
68
|
+
_defineProperty(this, "totalSlowInputs", 0);
|
|
69
|
+
_defineProperty(this, "totalFreezes", 0);
|
|
70
|
+
this.now = now;
|
|
71
|
+
this.config = {
|
|
72
|
+
...DEFAULT_LATCH_POLICY_CONFIG,
|
|
73
|
+
...config
|
|
74
|
+
};
|
|
75
|
+
this.startedAt = now();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether limited mode should be on, for either reason. This is the verdict consumers act on.
|
|
80
|
+
*/
|
|
81
|
+
isBreached() {
|
|
82
|
+
return this.documentBreached || this.latched;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What the latch was based on, or `undefined` while un-latched. Intended for telemetry — nothing in
|
|
87
|
+
* the decision reads it back.
|
|
88
|
+
*/
|
|
89
|
+
getLatchDetails() {
|
|
90
|
+
return this.latchDetails;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether the runtime (device) criteria have latched. One-way, and never cleared. */
|
|
94
|
+
isLatched() {
|
|
95
|
+
return this.latched;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Latch the runtime reason directly, without accumulating evidence for it.
|
|
100
|
+
*
|
|
101
|
+
* The policy latches itself when its own criteria are met, so this exists for callers that have
|
|
102
|
+
* already decided: the plugin replaying the detector's latch transaction, and dev tooling forcing
|
|
103
|
+
* the state by hand. Idempotent, and one-way like every other route to `latched`.
|
|
104
|
+
*/
|
|
105
|
+
latch() {
|
|
106
|
+
if (this.latched) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.latched = true;
|
|
110
|
+
this.latchDetails = this.buildDetails('forced', undefined);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether the document currently breaches the thresholds. Can go back to false. */
|
|
114
|
+
isDocumentBreached() {
|
|
115
|
+
return this.documentBreached;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Evaluate the document reason against the size / node-count / legacy-content-macro thresholds.
|
|
120
|
+
*
|
|
121
|
+
* Walks the whole document, so the caller decides when it is worth paying for: `pm-plugins/main.ts`
|
|
122
|
+
* calls this on load and on `replaceDocument` (e.g. live-to-live page navigation) only, never per
|
|
123
|
+
* transaction. Editing therefore cannot turn the document reason on — a page that grows past the
|
|
124
|
+
* thresholds mid-session is only re-judged the next time it loads — but replacement can still turn
|
|
125
|
+
* it back off.
|
|
126
|
+
*/
|
|
127
|
+
evaluateDocument(doc) {
|
|
128
|
+
this.documentBreached = shouldEnableLimitedModeForDocument(doc, {
|
|
129
|
+
docSizeThreshold: this.config.docSizeThreshold,
|
|
130
|
+
nodeCountThreshold: this.config.nodeCountThreshold
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether a `doc.nodeSize` delta is large enough to be bulk work rather than typing. A keystroke
|
|
136
|
+
* moves this by 1; a paste, a bulk replace or a document load moves it far more.
|
|
137
|
+
*/
|
|
138
|
+
isBulkChange(nodeSizeDelta) {
|
|
139
|
+
return Math.abs(nodeSizeDelta) >= this.config.bulkChangeNodeSize;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Discard signals for a window. Called for bulk work, which is expensive but transient and
|
|
144
|
+
* self-limiting, so its cost must not be attributed to the device struggling.
|
|
145
|
+
*/
|
|
146
|
+
suppress() {
|
|
147
|
+
if (this.latched) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.suppressedUntil = this.now() + this.config.bulkChangeSuppressionMs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Feed one keystroke's input latency (dispatch through to the next animation frame).
|
|
155
|
+
*/
|
|
156
|
+
recordInputLatency(durationMs) {
|
|
157
|
+
if (!this.canRecord()) {
|
|
158
|
+
return 'ignored';
|
|
159
|
+
}
|
|
160
|
+
const {
|
|
161
|
+
slowInputMs,
|
|
162
|
+
latencyWindowSize,
|
|
163
|
+
latencySlowSamplesRequired
|
|
164
|
+
} = this.config;
|
|
165
|
+
this.totalInputSamples += 1;
|
|
166
|
+
if (durationMs > slowInputMs) {
|
|
167
|
+
this.totalSlowInputs += 1;
|
|
168
|
+
// Remembered even once the window rolls over, so the freeze criterion below can check that
|
|
169
|
+
// the jank actually coincided with editing.
|
|
170
|
+
this.lastSlowInputAt = this.now();
|
|
171
|
+
}
|
|
172
|
+
this.latencySamples.push(durationMs);
|
|
173
|
+
if (this.latencySamples.length > latencyWindowSize) {
|
|
174
|
+
this.latencySamples.shift();
|
|
175
|
+
}
|
|
176
|
+
if (this.latencySamples.length < latencyWindowSize) {
|
|
177
|
+
return 'recorded';
|
|
178
|
+
}
|
|
179
|
+
const slowSamples = this.latencySamples.filter(sample => sample > slowInputMs).length;
|
|
180
|
+
if (slowSamples < latencySlowSamplesRequired) {
|
|
181
|
+
return 'recorded';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Median rather than mean: a mean is dragged over the threshold by one or two outliers, which
|
|
185
|
+
// is exactly the transient jank this policy is meant to ignore.
|
|
186
|
+
const windowMedian = median(this.latencySamples);
|
|
187
|
+
if (windowMedian <= slowInputMs) {
|
|
188
|
+
return 'recorded';
|
|
189
|
+
}
|
|
190
|
+
return this.qualify('inputLatency', windowMedian);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Feed one `longtask` PerformanceObserver entry.
|
|
195
|
+
*/
|
|
196
|
+
recordLongTask(durationMs) {
|
|
197
|
+
if (!this.canRecord()) {
|
|
198
|
+
return 'ignored';
|
|
199
|
+
}
|
|
200
|
+
const {
|
|
201
|
+
freezeTaskMs,
|
|
202
|
+
freezeWindowMs,
|
|
203
|
+
freezeTasksRequired
|
|
204
|
+
} = this.config;
|
|
205
|
+
if (durationMs <= freezeTaskMs) {
|
|
206
|
+
return 'recorded';
|
|
207
|
+
}
|
|
208
|
+
const now = this.now();
|
|
209
|
+
this.totalFreezes += 1;
|
|
210
|
+
this.freezeTimes.push(now);
|
|
211
|
+
this.freezeTimes = this.freezeTimes.filter(time => now - time <= freezeWindowMs);
|
|
212
|
+
if (this.freezeTimes.length < freezeTasksRequired) {
|
|
213
|
+
return 'recorded';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Corroboration. `longtask` is process-wide, so without this a busy background tab or an
|
|
217
|
+
// unrelated app could latch an editor the user is typing in perfectly happily.
|
|
218
|
+
if (this.lastSlowInputAt === undefined || now - this.lastSlowInputAt > freezeWindowMs) {
|
|
219
|
+
return 'recorded';
|
|
220
|
+
}
|
|
221
|
+
return this.qualify('freeze');
|
|
222
|
+
}
|
|
223
|
+
canRecord() {
|
|
224
|
+
if (this.latched) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const now = this.now();
|
|
228
|
+
if (now - this.startedAt < this.config.warmUpMs) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return now >= this.suppressedUntil;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Snapshot of what the latch was based on. Called before the evidence buffers are cleared. */
|
|
235
|
+
buildDetails(reason, latencyMedianMs) {
|
|
236
|
+
var _this$firstWindowReas;
|
|
237
|
+
const now = this.now();
|
|
238
|
+
return {
|
|
239
|
+
reason,
|
|
240
|
+
firstWindowReason: (_this$firstWindowReas = this.firstWindowReason) !== null && _this$firstWindowReas !== void 0 ? _this$firstWindowReas : reason,
|
|
241
|
+
requiredConfirmations: this.config.requiredConfirmations,
|
|
242
|
+
documentAlreadyBreached: this.documentBreached,
|
|
243
|
+
msFromFirstWindow: this.firstQualifiedAt === undefined ? undefined : Math.round(now - this.firstQualifiedAt),
|
|
244
|
+
latencyMedianMs: latencyMedianMs === undefined ? undefined : Math.round(latencyMedianMs),
|
|
245
|
+
timeToLatchMs: Math.round(now - this.startedAt),
|
|
246
|
+
totalInputSamples: this.totalInputSamples,
|
|
247
|
+
totalSlowInputs: this.totalSlowInputs,
|
|
248
|
+
totalFreezes: this.totalFreezes
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
qualify(reason, latencyMedianMs) {
|
|
252
|
+
const now = this.now();
|
|
253
|
+
|
|
254
|
+
// Each qualifying window must be independent evidence, so the buffers are cleared rather than
|
|
255
|
+
// left to re-trigger off the same samples on the very next keystroke.
|
|
256
|
+
this.latencySamples = [];
|
|
257
|
+
this.freezeTimes = [];
|
|
258
|
+
|
|
259
|
+
// Too soon after the last counted window to be independent of it, so it earns no credit. The
|
|
260
|
+
// buffers above are still cleared, which is what makes the run rebuild from scratch.
|
|
261
|
+
if (this.lastQualifiedAt !== undefined && now - this.lastQualifiedAt < this.config.confirmationGapMs) {
|
|
262
|
+
return 'qualified';
|
|
263
|
+
}
|
|
264
|
+
this.qualifiedWindows += 1;
|
|
265
|
+
this.lastQualifiedAt = now;
|
|
266
|
+
if (this.firstQualifiedAt === undefined) {
|
|
267
|
+
this.firstQualifiedAt = now;
|
|
268
|
+
this.firstWindowReason = reason;
|
|
269
|
+
}
|
|
270
|
+
if (this.qualifiedWindows < this.config.requiredConfirmations) {
|
|
271
|
+
return 'qualified';
|
|
272
|
+
}
|
|
273
|
+
this.latched = true;
|
|
274
|
+
this.latchDetails = this.buildDetails(reason, latencyMedianMs);
|
|
275
|
+
return 'latched';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getNodeIdProvider } from '@atlaskit/editor-common/node-anchor';
|
|
2
2
|
import { usePluginStateEffect } from '@atlaskit/editor-common/use-plugin-state-effect';
|
|
3
3
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
4
|
-
import {
|
|
4
|
+
import { limitedModePluginKey } from './pm-plugins/plugin-key';
|
|
5
|
+
import { createPlugin } from './pm-plugins/main';
|
|
5
6
|
export var limitedModePlugin = function limitedModePlugin(_ref) {
|
|
6
7
|
var api = _ref.api;
|
|
7
8
|
return {
|
|
@@ -9,7 +10,9 @@ export var limitedModePlugin = function limitedModePlugin(_ref) {
|
|
|
9
10
|
pmPlugins: function pmPlugins() {
|
|
10
11
|
return [{
|
|
11
12
|
name: 'limitedModePlugin',
|
|
12
|
-
plugin:
|
|
13
|
+
plugin: function plugin() {
|
|
14
|
+
return createPlugin(api);
|
|
15
|
+
}
|
|
13
16
|
}];
|
|
14
17
|
},
|
|
15
18
|
getSharedState: function getSharedState(editorState) {
|
|
@@ -17,7 +20,7 @@ export var limitedModePlugin = function limitedModePlugin(_ref) {
|
|
|
17
20
|
return {
|
|
18
21
|
get enabled() {
|
|
19
22
|
var _limitedModePluginKey, _limitedModePluginKey2;
|
|
20
|
-
return (_limitedModePluginKey = (_limitedModePluginKey2 = limitedModePluginKey.getState(editorState)) === null || _limitedModePluginKey2 === void 0 ? void 0 : _limitedModePluginKey2.
|
|
23
|
+
return (_limitedModePluginKey = (_limitedModePluginKey2 = limitedModePluginKey.getState(editorState)) === null || _limitedModePluginKey2 === void 0 ? void 0 : _limitedModePluginKey2.enabled) !== null && _limitedModePluginKey !== void 0 ? _limitedModePluginKey : false;
|
|
21
24
|
},
|
|
22
25
|
limitedModePluginKey: limitedModePluginKey
|
|
23
26
|
};
|
|
@@ -1,29 +1,212 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
|
+
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
1
5
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
6
|
import { shouldEnableLimitedModeForDocument } from '@atlaskit/editor-common/should-enable-limited-mode';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
|
|
8
|
+
import { expVal } from '@atlaskit/platform-feature-experiments/exp-val';
|
|
9
|
+
import { limitedModePluginKey } from './plugin-key';
|
|
10
|
+
import { createLatchDetector } from './utils/latch-detector';
|
|
11
|
+
import { LatchPolicy } from './utils/latch-policy';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Meta shape used to latch limited mode at runtime. Dispatching this is the entire delivery
|
|
15
|
+
* mechanism: the transaction flows through `SharedStateAPI.notifyListeners`, which diffs the plugin's
|
|
16
|
+
* shared state and notifies every consumer. No plugin re-registration and no schema rebuild.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hardware hints, reported with a latch so it can be correlated with device class.
|
|
21
|
+
*
|
|
22
|
+
* Telemetry only — the policy no longer takes the hardware into account when deciding, so this
|
|
23
|
+
* exists to answer "which devices are latching" from the data rather than by assumption. Both hints
|
|
24
|
+
* are optional (`deviceMemory` is Chromium-only) and are simply absent where unsupported.
|
|
25
|
+
*/
|
|
26
|
+
var getDeviceHints = function getDeviceHints() {
|
|
27
|
+
if (typeof navigator === 'undefined') {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
var _ref = navigator,
|
|
31
|
+
hardwareConcurrency = _ref.hardwareConcurrency,
|
|
32
|
+
deviceMemory = _ref.deviceMemory;
|
|
33
|
+
return {
|
|
34
|
+
hardwareConcurrency: hardwareConcurrency,
|
|
35
|
+
deviceMemoryGb: deviceMemory
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Control-arm state: the document decision alone, exactly as before the experiment. */
|
|
40
|
+
var documentOnlyState = function documentOnlyState(doc) {
|
|
41
|
+
var documentSizeBreachesThreshold = shouldEnableLimitedModeForDocument(doc);
|
|
42
|
+
return {
|
|
43
|
+
documentSizeBreachesThreshold: documentSizeBreachesThreshold,
|
|
44
|
+
latchPolicyBreached: false,
|
|
45
|
+
enabled: documentSizeBreachesThreshold
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Treatment-arm state: whatever the policy says, for both of its reasons. */
|
|
50
|
+
var policyState = function policyState(policy) {
|
|
51
|
+
return {
|
|
52
|
+
documentSizeBreachesThreshold: policy.isDocumentBreached(),
|
|
53
|
+
latchPolicyBreached: policy.isBreached(),
|
|
54
|
+
enabled: policy.isBreached()
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export var createPlugin = function createPlugin(api, injectedPolicy) {
|
|
58
|
+
var detector;
|
|
59
|
+
var policy;
|
|
60
|
+
if (injectedPolicy) {
|
|
61
|
+
policy = injectedPolicy;
|
|
62
|
+
} else if (isExperimentEnabled('platform_editor_dynamic_limited_mode')) {
|
|
63
|
+
var config = expVal('platform_editor_dynamic_limited_mode', 'policyConfig', {
|
|
64
|
+
warmUpMs: 10000,
|
|
65
|
+
slowInputMs: 100,
|
|
66
|
+
latencyWindowSize: 12,
|
|
67
|
+
latencySlowSamplesRequired: 6,
|
|
68
|
+
freezeTaskMs: 600,
|
|
69
|
+
freezeTasksRequired: 3,
|
|
70
|
+
freezeWindowMs: 30000,
|
|
71
|
+
requiredConfirmations: 2,
|
|
72
|
+
confirmationGapMs: 30000,
|
|
73
|
+
bulkChangeNodeSize: 100,
|
|
74
|
+
bulkChangeSuppressionMs: 2000,
|
|
75
|
+
docSizeThreshold: 750000,
|
|
76
|
+
nodeCountThreshold: 5000
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Resolved once per editor rather than per transaction. When off, no policy is built and the
|
|
80
|
+
// document decision runs inline exactly as it did before this experiment.
|
|
81
|
+
policy = new LatchPolicy({
|
|
82
|
+
now: function now() {
|
|
83
|
+
return performance.now();
|
|
84
|
+
},
|
|
85
|
+
config: config
|
|
86
|
+
});
|
|
87
|
+
}
|
|
6
88
|
return new SafePlugin({
|
|
7
89
|
key: limitedModePluginKey,
|
|
8
|
-
|
|
9
|
-
|
|
90
|
+
props: {
|
|
91
|
+
handleTextInput: function handleTextInput() {
|
|
92
|
+
var _detector;
|
|
93
|
+
(_detector = detector) === null || _detector === void 0 || _detector.measureInput();
|
|
94
|
+
|
|
95
|
+
// Never handle the input — this is measurement only.
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
view: function view(editorView) {
|
|
100
|
+
// No policy means the experiment is off: no observers, no per-keystroke measurement.
|
|
101
|
+
if (!policy) {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
var startedAt = performance.now();
|
|
105
|
+
detector = createLatchDetector({
|
|
106
|
+
policy: policy,
|
|
107
|
+
onLatchCriteriaMet: function onLatchCriteriaMet(details) {
|
|
108
|
+
var _api$analytics;
|
|
109
|
+
// Treatment arm only — the detector does not exist in control. See LimitedModeLatchedAEP.
|
|
110
|
+
//
|
|
111
|
+
// `details` is the policy's own snapshot of what it latched on, taken before it cleared
|
|
112
|
+
// its evidence buffers, so it reports the closing window rather than an empty one.
|
|
113
|
+
api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
|
|
114
|
+
action: ACTION.LIMITED_MODE_LATCHED,
|
|
115
|
+
actionSubject: ACTION_SUBJECT.EDITOR,
|
|
116
|
+
eventType: EVENT_TYPE.OPERATIONAL,
|
|
117
|
+
attributes: _objectSpread(_objectSpread({
|
|
118
|
+
latched: true,
|
|
119
|
+
reason: details.reason,
|
|
120
|
+
firstWindowReason: details.firstWindowReason,
|
|
121
|
+
requiredConfirmations: details.requiredConfirmations,
|
|
122
|
+
documentAlreadyBreached: details.documentAlreadyBreached,
|
|
123
|
+
msFromFirstWindow: details.msFromFirstWindow,
|
|
124
|
+
latencyMedianMs: details.latencyMedianMs,
|
|
125
|
+
totalInputSamples: details.totalInputSamples,
|
|
126
|
+
totalSlowInputs: details.totalSlowInputs,
|
|
127
|
+
totalFreezes: details.totalFreezes,
|
|
128
|
+
nodeSize: editorView.state.doc.nodeSize
|
|
129
|
+
}, getDeviceHints()), {}, {
|
|
130
|
+
// Measured from the plugin view starting, which is a little later than the policy's
|
|
131
|
+
// own `timeToLatchMs` (it starts at plugin construction).
|
|
132
|
+
timeToLatch: performance.now() - startedAt
|
|
133
|
+
})
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// The policy already holds the latch; this transaction only prompts the plugin to
|
|
137
|
+
// re-read it, which is what notifies every consumer through shared state.
|
|
138
|
+
editorView.dispatch(editorView.state.tr.setMeta(limitedModePluginKey, {
|
|
139
|
+
latchPolicyBreached: true
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
destroy: function destroy() {
|
|
145
|
+
var _detector2;
|
|
146
|
+
(_detector2 = detector) === null || _detector2 === void 0 || _detector2.destroy();
|
|
147
|
+
detector = undefined;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
10
150
|
},
|
|
11
151
|
state: {
|
|
12
152
|
init: function init(_config, editorState) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
153
|
+
if (!policy) {
|
|
154
|
+
return documentOnlyState(editorState.doc);
|
|
155
|
+
}
|
|
156
|
+
policy.evaluateDocument(editorState.doc);
|
|
157
|
+
return policyState(policy);
|
|
16
158
|
},
|
|
17
|
-
apply: function apply(tr, currentPluginState,
|
|
18
|
-
|
|
19
|
-
|
|
159
|
+
apply: function apply(tr, currentPluginState, oldState, _newState) {
|
|
160
|
+
var _tr$getMeta;
|
|
161
|
+
var documentReplaced = Boolean(tr.getMeta('replaceDocument'));
|
|
162
|
+
if (!policy) {
|
|
163
|
+
// Control arm, unchanged: skip the traversal once already breached, but always re-check
|
|
164
|
+
// when the document is replaced (e.g. live-to-live page navigation).
|
|
165
|
+
if (currentPluginState.documentSizeBreachesThreshold && !documentReplaced) {
|
|
166
|
+
return currentPluginState;
|
|
167
|
+
}
|
|
168
|
+
return documentOnlyState(tr.doc);
|
|
169
|
+
}
|
|
20
170
|
|
|
21
|
-
|
|
22
|
-
|
|
171
|
+
// The detector's latch arrives as a transaction so that plugin state stays a function of
|
|
172
|
+
// the transaction stream rather than of when `apply` happens to read the policy. Dev
|
|
173
|
+
// tooling dispatches the same meta to force limited mode by hand.
|
|
174
|
+
if ((_tr$getMeta = tr.getMeta(limitedModePluginKey)) !== null && _tr$getMeta !== void 0 && _tr$getMeta.latchPolicyBreached) {
|
|
175
|
+
policy.latch();
|
|
23
176
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
177
|
+
|
|
178
|
+
// Only on replacement, never on an ordinary edit: the check walks the whole document, so
|
|
179
|
+
// running it per transaction is a full-document scan on every keystroke. The trade-off is
|
|
180
|
+
// that a document editing its way past the thresholds is not noticed until it next loads.
|
|
181
|
+
//
|
|
182
|
+
// Deliberately not skipped when limited mode is already on. Replacement is the one moment
|
|
183
|
+
// the document verdict can go *down* — live-to-live navigation onto a smaller page — so
|
|
184
|
+
// skipping it there is what would strand limited mode on forever. It costs one walk per
|
|
185
|
+
// page navigation, which is nothing next to the navigation itself.
|
|
186
|
+
if (documentReplaced) {
|
|
187
|
+
policy.evaluateDocument(tr.doc);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Report bulk work so the policy can discount it. The policy decides what counts as bulk;
|
|
191
|
+
// this only supplies the facts.
|
|
192
|
+
//
|
|
193
|
+
// Known gap: operations that are expensive but barely change document size — table
|
|
194
|
+
// resize, drag-and-drop moves (delete + insert nets to ~0), type-ahead — are not
|
|
195
|
+
// suppressed. `@atlaskit/insm` already tracks exactly these via `startHeavyTask`, but its
|
|
196
|
+
// public facade does not expose `runningHeavyTasks`, so there is no way to read them from
|
|
197
|
+
// here today. Closing that gap needs an accessor on the insm package.
|
|
198
|
+
if (tr.docChanged) {
|
|
199
|
+
var _detector3;
|
|
200
|
+
(_detector3 = detector) === null || _detector3 === void 0 || _detector3.noteDocumentChange({
|
|
201
|
+
nodeSizeDelta: tr.doc.nodeSize - oldState.doc.nodeSize,
|
|
202
|
+
isDocumentReplaced: documentReplaced
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
var next = policyState(policy);
|
|
206
|
+
|
|
207
|
+
// Keep the previous object when nothing changed, so shared-state diffing stays cheap and
|
|
208
|
+
// consumers are not notified for no reason.
|
|
209
|
+
return next.enabled === currentPluginState.enabled && next.documentSizeBreachesThreshold === currentPluginState.documentSizeBreachesThreshold ? currentPluginState : next;
|
|
27
210
|
}
|
|
28
211
|
}
|
|
29
212
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
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; } } }; }
|
|
2
|
+
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; } }
|
|
3
|
+
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; }
|
|
4
|
+
import { isPerformanceAPIAvailable, isPerformanceObserverLongTaskAvailable } from '@atlaskit/editor-common/is-performance-api-available';
|
|
5
|
+
/**
|
|
6
|
+
* Wires browser performance signals into the supplied {@link LatchPolicy}.
|
|
7
|
+
*
|
|
8
|
+
* Holds no configuration and makes no decisions: it measures, and forwards. Everything tunable lives
|
|
9
|
+
* on the policy, including the clock it reads.
|
|
10
|
+
*
|
|
11
|
+
* The measurement mirrors `editor-plugin-base/src/pm-plugins/frozen-editor.ts`, which already runs an
|
|
12
|
+
* identical `longtask` observer and per-keystroke rAF measurement for every session to feed
|
|
13
|
+
* `ACTION.SLOW_INPUT` / `ACTION.BROWSER_FREEZE`. It is duplicated rather than shared so that this
|
|
14
|
+
* experiment stays self-contained inside the limited-mode plugin — no new cross-plugin dependency,
|
|
15
|
+
* and deleting it is a single-directory revert if the experiment does not ship. The marginal cost is
|
|
16
|
+
* one `performance.now()` and one `requestAnimationFrame` per keystroke.
|
|
17
|
+
*/
|
|
18
|
+
export var createLatchDetector = function createLatchDetector(_ref) {
|
|
19
|
+
var onLatchCriteriaMet = _ref.onLatchCriteriaMet,
|
|
20
|
+
policy = _ref.policy;
|
|
21
|
+
// One clock for the whole subsystem, so measured durations and the policy's windows agree.
|
|
22
|
+
var now = policy.now;
|
|
23
|
+
var observer;
|
|
24
|
+
var destroyed = false;
|
|
25
|
+
var handleEvaluation = function handleEvaluation(evaluation) {
|
|
26
|
+
if (evaluation !== 'latched') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// One-way latch: the policy will never evaluate again, so stop paying for the observers.
|
|
31
|
+
teardownObservers();
|
|
32
|
+
var details = policy.getLatchDetails();
|
|
33
|
+
if (details) {
|
|
34
|
+
onLatchCriteriaMet(details);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function teardownObservers() {
|
|
38
|
+
var _observer;
|
|
39
|
+
(_observer = observer) === null || _observer === void 0 || _observer.disconnect();
|
|
40
|
+
observer = undefined;
|
|
41
|
+
}
|
|
42
|
+
if (isPerformanceObserverLongTaskAvailable()) {
|
|
43
|
+
try {
|
|
44
|
+
observer = new PerformanceObserver(function (list) {
|
|
45
|
+
var _iterator = _createForOfIteratorHelper(list.getEntries()),
|
|
46
|
+
_step;
|
|
47
|
+
try {
|
|
48
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
49
|
+
var entry = _step.value;
|
|
50
|
+
if (policy.isLatched()) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
handleEvaluation(policy.recordLongTask(entry.duration));
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
_iterator.e(err);
|
|
57
|
+
} finally {
|
|
58
|
+
_iterator.f();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
observer.observe({
|
|
62
|
+
entryTypes: ['longtask']
|
|
63
|
+
});
|
|
64
|
+
} catch (_unused) {
|
|
65
|
+
// `longtask` is unsupported in some browsers even when PerformanceObserver exists. The
|
|
66
|
+
// latency criterion alone is still a valid trigger, so carry on without freeze detection.
|
|
67
|
+
observer = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
noteDocumentChange: function noteDocumentChange(_ref2) {
|
|
72
|
+
var nodeSizeDelta = _ref2.nodeSizeDelta,
|
|
73
|
+
isDocumentReplaced = _ref2.isDocumentReplaced;
|
|
74
|
+
if (destroyed || policy.isLatched()) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (isDocumentReplaced || policy.isBulkChange(nodeSizeDelta)) {
|
|
78
|
+
policy.suppress();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
measureInput: function measureInput() {
|
|
82
|
+
if (destroyed || policy.isLatched() || !isPerformanceAPIAvailable()) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
var start = now();
|
|
86
|
+
|
|
87
|
+
// Runs after every handleTextInput and all resulting plugin work, but before paint — the
|
|
88
|
+
// same measurement point frozen-editor uses, so the numbers are comparable to existing
|
|
89
|
+
// SLOW_INPUT telemetry.
|
|
90
|
+
requestAnimationFrame(function () {
|
|
91
|
+
if (destroyed || policy.isLatched()) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
handleEvaluation(policy.recordInputLatency(now() - start));
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
destroy: function destroy() {
|
|
98
|
+
destroyed = true;
|
|
99
|
+
teardownObservers();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
File without changes
|