@chipmobilesdk/rn-notification 0.1.2 → 0.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 +96 -42
- package/README.md +399 -306
- package/package.json +2 -1
- package/skills/sdk-rn-notification/SKILL.md +68 -3
- package/src/channel/migrate.ts +86 -12
- package/src/channel/tone-map.ts +130 -90
- package/src/channel/validate-tone.ts +61 -0
- package/src/contract/channel.ts +188 -89
- package/src/contract/diagnostics.ts +102 -102
- package/src/contract/errors.ts +130 -123
- package/src/contract/index.ts +10 -10
- package/src/contract/interaction.ts +62 -62
- package/src/contract/notification.ts +118 -118
- package/src/contract/permission.ts +60 -60
- package/src/contract/reconcile.ts +88 -88
- package/src/contract/recurrence.ts +123 -123
- package/src/diagnostics/burst.ts +101 -101
- package/src/diagnostics/events.ts +72 -72
- package/src/diagnostics/redact.ts +72 -72
- package/src/diagnostics/snapshot.ts +64 -64
- package/src/engine/adapter.ts +172 -79
- package/src/engine/notify-kit-engine.ts +761 -535
- package/src/index.ts +24 -0
- package/src/initialize.ts +174 -174
- package/src/interaction/index.ts +146 -146
- package/src/maintenance/cycle.ts +147 -147
- package/src/maintenance/permission-refresh.ts +41 -41
- package/src/maintenance/reanchor.ts +95 -95
- package/src/maintenance/toggle.ts +60 -60
- package/src/maintenance/verify.ts +83 -83
- package/src/maintenance/window-refresh.ts +61 -61
- package/src/permission/index.ts +183 -183
- package/src/permission/state.ts +77 -77
- package/src/push/envelope.ts +165 -165
- package/src/push/fcm-provider.ts +146 -146
- package/src/push/gate.ts +190 -190
- package/src/push/instructions.ts +109 -109
- package/src/push/provider-adapter.ts +54 -54
- package/src/push/signout.ts +65 -65
- package/src/reconcile/chunk.ts +53 -53
- package/src/reconcile/diff.ts +145 -145
- package/src/reconcile/duplicates.ts +73 -73
- package/src/reconcile/index.ts +14 -1
- package/src/reconcile/truncate.ts +56 -56
- package/src/recurrence/clock-guard.ts +71 -71
- package/src/recurrence/expand.ts +292 -292
- package/src/repeat/expiry.ts +55 -0
- package/src/repeat/index.ts +54 -0
- package/src/repeat/marker.ts +72 -0
- package/src/repeat/place.ts +73 -0
- package/src/schedule/anchor.ts +267 -267
- package/src/schedule/index.ts +184 -173
- package/src/schedule/partition.ts +85 -85
- package/src/schedule/resolve-moment.ts +80 -80
- package/src/schedule/validate-content.ts +111 -111
- package/src/schedule/validate-identifier.ts +126 -114
- package/src/state/caps.ts +71 -71
- package/src/state/interaction-buffer.ts +73 -73
- package/src/state/mmkv-store.ts +75 -71
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +258 -246
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +84 -84
- package/src/state/store.ts +40 -40
- package/src/testing/conformance/index.ts +248 -194
- package/src/testing/fake-engine.ts +469 -284
- package/src/testing/fake-provider.ts +129 -129
- package/src/testing/faults.ts +72 -72
- package/src/testing/measure.ts +337 -334
package/src/testing/measure.ts
CHANGED
|
@@ -1,334 +1,337 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The PERF-001 measurement harness.
|
|
3
|
-
*
|
|
4
|
-
* Every budget in the specification, measured against the real code paths and
|
|
5
|
-
* reported as pass or fail with the number that produced the verdict. Written
|
|
6
|
-
* as a plain async function rather than a Jest suite so the same harness runs
|
|
7
|
-
* in continuous integration and from the demo app on a physical device — which
|
|
8
|
-
* matters, because PERF-003 forbids emulator numbers standing in as evidence.
|
|
9
|
-
*
|
|
10
|
-
* Five budgets are deliberately absent and reported as such: delivery accuracy,
|
|
11
|
-
* cold-start latency, startup delta and frame rate cannot be measured
|
|
12
|
-
* in-process. The first needs the OS to actually fire an alarm, the second a
|
|
13
|
-
* real process death. Pretending to measure them here would be worse than
|
|
14
|
-
* leaving the gap visible.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { NotificationEngineAdapter } from '../engine/adapter';
|
|
18
|
-
import type { NotificationRequest } from '../contract/notification';
|
|
19
|
-
import type { ResolvedTone } from '../contract/channel';
|
|
20
|
-
import type { RecurrenceRule } from '../contract/recurrence';
|
|
21
|
-
import { createScheduler } from '../schedule';
|
|
22
|
-
import { createReconciler } from '../reconcile';
|
|
23
|
-
import { createRecurrence } from '../recurrence';
|
|
24
|
-
import { createPermissions } from '../permission';
|
|
25
|
-
import { createReAnchorStep } from '../maintenance/reanchor';
|
|
26
|
-
import { MaintenanceRegistry } from '../maintenance/cycle';
|
|
27
|
-
import { StateManager } from '../state/record';
|
|
28
|
-
import { createMemoryStore } from '../state/store';
|
|
29
|
-
import { planReAnchor } from '../schedule/anchor';
|
|
30
|
-
import { recordSeen } from '../state/dedup-ledger';
|
|
31
|
-
|
|
32
|
-
/** Every budget PERF-001 states, in milliseconds unless the name says otherwise. */
|
|
33
|
-
export const PERF_BUDGETS = {
|
|
34
|
-
scheduleOne: 50,
|
|
35
|
-
readPending100: 200,
|
|
36
|
-
reconcileNoChange100: 150,
|
|
37
|
-
reconcile500With100Changes: 1_500,
|
|
38
|
-
maintenanceIdleCycle: 300,
|
|
39
|
-
windowRefresh32Series: 500,
|
|
40
|
-
reAnchor100: 1_000,
|
|
41
|
-
reAnchorNoChangeCheck: 20,
|
|
42
|
-
permissionRead: 50,
|
|
43
|
-
dedupCheck: 20,
|
|
44
|
-
interfaceThreadUnit: 16,
|
|
45
|
-
stateFootprintBytes: 1_048_576,
|
|
46
|
-
} as const;
|
|
47
|
-
|
|
48
|
-
export type BudgetName = keyof typeof PERF_BUDGETS;
|
|
49
|
-
|
|
50
|
-
export interface Measurement {
|
|
51
|
-
readonly name: BudgetName;
|
|
52
|
-
readonly label: string;
|
|
53
|
-
/** Milliseconds, or bytes for the footprint budget. */
|
|
54
|
-
readonly measured: number;
|
|
55
|
-
readonly budget: number;
|
|
56
|
-
readonly unit: 'ms' | 'bytes';
|
|
57
|
-
readonly passed: boolean;
|
|
58
|
-
/** Present where the budget also demands zero platform operations. */
|
|
59
|
-
readonly operationCount?: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface MeasurementReport {
|
|
63
|
-
readonly measurements: readonly Measurement[];
|
|
64
|
-
readonly passed: number;
|
|
65
|
-
readonly failed: number;
|
|
66
|
-
/** Budgets this harness structurally cannot measure. Never silently omitted. */
|
|
67
|
-
readonly requiresDevice: readonly string[];
|
|
68
|
-
readonly ranAt: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const TONE: ResolvedTone = {
|
|
72
|
-
toneId: 'perf',
|
|
73
|
-
version: 1,
|
|
74
|
-
android: {
|
|
75
|
-
channelId: 'perf.v1',
|
|
76
|
-
importance: 'default',
|
|
77
|
-
vibration: false,
|
|
78
|
-
badge: false,
|
|
79
|
-
bypassDnd: false,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
'
|
|
330
|
-
'
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The PERF-001 measurement harness.
|
|
3
|
+
*
|
|
4
|
+
* Every budget in the specification, measured against the real code paths and
|
|
5
|
+
* reported as pass or fail with the number that produced the verdict. Written
|
|
6
|
+
* as a plain async function rather than a Jest suite so the same harness runs
|
|
7
|
+
* in continuous integration and from the demo app on a physical device — which
|
|
8
|
+
* matters, because PERF-003 forbids emulator numbers standing in as evidence.
|
|
9
|
+
*
|
|
10
|
+
* Five budgets are deliberately absent and reported as such: delivery accuracy,
|
|
11
|
+
* cold-start latency, startup delta and frame rate cannot be measured
|
|
12
|
+
* in-process. The first needs the OS to actually fire an alarm, the second a
|
|
13
|
+
* real process death. Pretending to measure them here would be worse than
|
|
14
|
+
* leaving the gap visible.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { NotificationEngineAdapter } from '../engine/adapter';
|
|
18
|
+
import type { NotificationRequest } from '../contract/notification';
|
|
19
|
+
import type { ResolvedTone } from '../contract/channel';
|
|
20
|
+
import type { RecurrenceRule } from '../contract/recurrence';
|
|
21
|
+
import { createScheduler } from '../schedule';
|
|
22
|
+
import { createReconciler } from '../reconcile';
|
|
23
|
+
import { createRecurrence } from '../recurrence';
|
|
24
|
+
import { createPermissions } from '../permission';
|
|
25
|
+
import { createReAnchorStep } from '../maintenance/reanchor';
|
|
26
|
+
import { MaintenanceRegistry } from '../maintenance/cycle';
|
|
27
|
+
import { StateManager } from '../state/record';
|
|
28
|
+
import { createMemoryStore } from '../state/store';
|
|
29
|
+
import { planReAnchor } from '../schedule/anchor';
|
|
30
|
+
import { recordSeen } from '../state/dedup-ledger';
|
|
31
|
+
|
|
32
|
+
/** Every budget PERF-001 states, in milliseconds unless the name says otherwise. */
|
|
33
|
+
export const PERF_BUDGETS = {
|
|
34
|
+
scheduleOne: 50,
|
|
35
|
+
readPending100: 200,
|
|
36
|
+
reconcileNoChange100: 150,
|
|
37
|
+
reconcile500With100Changes: 1_500,
|
|
38
|
+
maintenanceIdleCycle: 300,
|
|
39
|
+
windowRefresh32Series: 500,
|
|
40
|
+
reAnchor100: 1_000,
|
|
41
|
+
reAnchorNoChangeCheck: 20,
|
|
42
|
+
permissionRead: 50,
|
|
43
|
+
dedupCheck: 20,
|
|
44
|
+
interfaceThreadUnit: 16,
|
|
45
|
+
stateFootprintBytes: 1_048_576,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export type BudgetName = keyof typeof PERF_BUDGETS;
|
|
49
|
+
|
|
50
|
+
export interface Measurement {
|
|
51
|
+
readonly name: BudgetName;
|
|
52
|
+
readonly label: string;
|
|
53
|
+
/** Milliseconds, or bytes for the footprint budget. */
|
|
54
|
+
readonly measured: number;
|
|
55
|
+
readonly budget: number;
|
|
56
|
+
readonly unit: 'ms' | 'bytes';
|
|
57
|
+
readonly passed: boolean;
|
|
58
|
+
/** Present where the budget also demands zero platform operations. */
|
|
59
|
+
readonly operationCount?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface MeasurementReport {
|
|
63
|
+
readonly measurements: readonly Measurement[];
|
|
64
|
+
readonly passed: number;
|
|
65
|
+
readonly failed: number;
|
|
66
|
+
/** Budgets this harness structurally cannot measure. Never silently omitted. */
|
|
67
|
+
readonly requiresDevice: readonly string[];
|
|
68
|
+
readonly ranAt: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const TONE: ResolvedTone = {
|
|
72
|
+
toneId: 'perf',
|
|
73
|
+
version: 1,
|
|
74
|
+
android: {
|
|
75
|
+
channelId: 'perf.v1',
|
|
76
|
+
importance: 'default',
|
|
77
|
+
vibration: false,
|
|
78
|
+
badge: false,
|
|
79
|
+
bypassDnd: false,
|
|
80
|
+
name: 'Fixture tone',
|
|
81
|
+
loopSound: false,
|
|
82
|
+
alarmCategory: false,
|
|
83
|
+
},
|
|
84
|
+
ios: { categoryId: 'perf', interruptionLevel: 'active', badge: false, repeatRefused: false },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export interface MeasureOptions {
|
|
88
|
+
/** Defaults to the real clock, which is what a device run needs. */
|
|
89
|
+
readonly clock?: () => number;
|
|
90
|
+
/** Identifier prefix, so a device run cannot collide with real notifications. */
|
|
91
|
+
readonly prefix?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function runMeasurementHarness(
|
|
95
|
+
engine: NotificationEngineAdapter,
|
|
96
|
+
options: MeasureOptions = {},
|
|
97
|
+
): Promise<MeasurementReport> {
|
|
98
|
+
const clock = options.clock ?? (() => Date.now());
|
|
99
|
+
const prefix = options.prefix ?? 'perf';
|
|
100
|
+
const measurements: Measurement[] = [];
|
|
101
|
+
|
|
102
|
+
const now = () => Date.now();
|
|
103
|
+
const zone = 'UTC';
|
|
104
|
+
const state = new StateManager({ store: createMemoryStore(), now });
|
|
105
|
+
const resolveTone = () => TONE;
|
|
106
|
+
|
|
107
|
+
const record = (
|
|
108
|
+
name: BudgetName,
|
|
109
|
+
label: string,
|
|
110
|
+
measured: number,
|
|
111
|
+
operationCount?: number,
|
|
112
|
+
): void => {
|
|
113
|
+
const budget = PERF_BUDGETS[name];
|
|
114
|
+
// A budget that also demands zero operations fails on either count.
|
|
115
|
+
const opsOk = operationCount === undefined || operationCount === 0;
|
|
116
|
+
measurements.push({
|
|
117
|
+
name,
|
|
118
|
+
label,
|
|
119
|
+
measured,
|
|
120
|
+
budget,
|
|
121
|
+
unit: name === 'stateFootprintBytes' ? 'bytes' : 'ms',
|
|
122
|
+
passed: measured <= budget && opsOk,
|
|
123
|
+
operationCount,
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Median of three, so one scheduler hiccup does not decide a verdict. */
|
|
128
|
+
const timeOf = async (run: () => Promise<unknown>): Promise<number> => {
|
|
129
|
+
const samples: number[] = [];
|
|
130
|
+
for (let i = 0; i < 3; i += 1) {
|
|
131
|
+
const started = clock();
|
|
132
|
+
await run();
|
|
133
|
+
samples.push(clock() - started);
|
|
134
|
+
}
|
|
135
|
+
return samples.sort((a, b) => a - b)[1];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const request = (id: string, at: number): NotificationRequest => ({
|
|
139
|
+
id: `${prefix}:${id}`,
|
|
140
|
+
content: { title: id, body: 'Measurement harness entry' },
|
|
141
|
+
tone: 'perf',
|
|
142
|
+
moment: { at },
|
|
143
|
+
groupTag: prefix,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const scheduler = createScheduler({
|
|
147
|
+
engine,
|
|
148
|
+
resolveTone,
|
|
149
|
+
timeZone: () => zone,
|
|
150
|
+
now,
|
|
151
|
+
canScheduleExactly: () => true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await engine.cancelAll();
|
|
155
|
+
|
|
156
|
+
// --- Scheduling -----------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
record(
|
|
159
|
+
'scheduleOne',
|
|
160
|
+
'Single schedule operation',
|
|
161
|
+
await timeOf(() => scheduler.schedule(request('single', now() + 3_600_000))),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < 100; i += 1) {
|
|
165
|
+
await scheduler.schedule(request(`bulk:${i}`, now() + (i + 1) * 60_000));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
record('readPending100', 'Read pending set of 100', await timeOf(() => scheduler.getPending()));
|
|
169
|
+
|
|
170
|
+
// --- Reconciliation -------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
const reconciler = createReconciler({
|
|
173
|
+
engine,
|
|
174
|
+
resolveTone,
|
|
175
|
+
timeZone: () => zone,
|
|
176
|
+
now,
|
|
177
|
+
canScheduleExactly: () => true,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const hundred = Array.from({ length: 100 }, (_, i) =>
|
|
181
|
+
request(`recon:${i}`, now() + (i + 1) * 60_000),
|
|
182
|
+
);
|
|
183
|
+
await reconciler.reconcile({ domain: prefix, desired: hundred });
|
|
184
|
+
|
|
185
|
+
let noChangeOps = 0;
|
|
186
|
+
const noChangeMs = await timeOf(async () => {
|
|
187
|
+
const result = await reconciler.reconcile({ domain: prefix, desired: hundred });
|
|
188
|
+
noChangeOps = result.operationCount;
|
|
189
|
+
});
|
|
190
|
+
record('reconcileNoChange100', 'No-change reconcile over 100', noChangeMs, noChangeOps);
|
|
191
|
+
|
|
192
|
+
const fiveHundred = Array.from({ length: 500 }, (_, i) =>
|
|
193
|
+
request(`recon:${i}`, now() + (i + 1) * 60_000),
|
|
194
|
+
);
|
|
195
|
+
await reconciler.reconcile({ domain: prefix, desired: fiveHundred });
|
|
196
|
+
|
|
197
|
+
const changed = fiveHundred.map((entry, i) =>
|
|
198
|
+
i < 100 ? { ...entry, content: { title: `changed-${i}`, body: entry.content.body } } : entry,
|
|
199
|
+
);
|
|
200
|
+
record(
|
|
201
|
+
'reconcile500With100Changes',
|
|
202
|
+
'500-entry reconcile with 100 changes',
|
|
203
|
+
await timeOf(() => reconciler.reconcile({ domain: prefix, desired: changed })),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// --- Re-anchoring ---------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
const pending = await engine.getPending();
|
|
209
|
+
record(
|
|
210
|
+
'reAnchorNoChangeCheck',
|
|
211
|
+
'Re-anchor check with no zone change',
|
|
212
|
+
await timeOf(async () => planReAnchor(pending, zone)),
|
|
213
|
+
planReAnchor(pending, zone).rewrites.length,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const context = { trigger: 'foreground' as const, now: now(), ownedIds: new Set<string>() };
|
|
217
|
+
const travelled = createReAnchorStep({
|
|
218
|
+
engine,
|
|
219
|
+
timeZone: () => 'Asia/Tokyo',
|
|
220
|
+
resolveTone,
|
|
221
|
+
canScheduleExactly: () => true,
|
|
222
|
+
});
|
|
223
|
+
record('reAnchor100', 'Re-anchor after a zone change', await timeOf(() => travelled.run(context)));
|
|
224
|
+
|
|
225
|
+
// --- Maintenance ----------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
const registry = new MaintenanceRegistry();
|
|
228
|
+
registry.register(
|
|
229
|
+
createReAnchorStep({
|
|
230
|
+
engine,
|
|
231
|
+
timeZone: () => zone,
|
|
232
|
+
resolveTone,
|
|
233
|
+
canScheduleExactly: () => true,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
let idleOps = 0;
|
|
238
|
+
const idleMs = await timeOf(async () => {
|
|
239
|
+
const result = await registry.run({ ...context, now: now() });
|
|
240
|
+
idleOps = result.operationCount;
|
|
241
|
+
});
|
|
242
|
+
record('maintenanceIdleCycle', 'Idle maintenance cycle', idleMs, idleOps);
|
|
243
|
+
|
|
244
|
+
// --- Recurrence -----------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
const recurrence = createRecurrence({
|
|
247
|
+
engine,
|
|
248
|
+
state,
|
|
249
|
+
resolveTone,
|
|
250
|
+
timeZone: () => zone,
|
|
251
|
+
now,
|
|
252
|
+
lookahead: { days: 60, occurrencesPerSeries: 32 },
|
|
253
|
+
canScheduleExactly: () => true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const series: RecurrenceRule[] = Array.from({ length: 32 }, (_, i) => ({
|
|
257
|
+
id: `${prefix}:series:${i}`,
|
|
258
|
+
pattern: { kind: 'daily', hour: i % 24, minute: 0 },
|
|
259
|
+
end: { kind: 'count', count: 2 },
|
|
260
|
+
template: {
|
|
261
|
+
content: { title: `Series ${i}`, body: 'Measurement harness series' },
|
|
262
|
+
tone: 'perf',
|
|
263
|
+
groupTag: prefix,
|
|
264
|
+
},
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
for (const rule of series) {
|
|
268
|
+
await recurrence.registerSeries(rule);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
record(
|
|
272
|
+
'windowRefresh32Series',
|
|
273
|
+
'Window refresh across 32 series',
|
|
274
|
+
await timeOf(async () => {
|
|
275
|
+
for (const rule of series) {
|
|
276
|
+
await recurrence.refreshSeries(rule);
|
|
277
|
+
}
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// --- Permissions ----------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
const permissions = createPermissions({
|
|
284
|
+
engine,
|
|
285
|
+
state,
|
|
286
|
+
platform: { os: 'android', apiLevel: 34 },
|
|
287
|
+
now,
|
|
288
|
+
});
|
|
289
|
+
record(
|
|
290
|
+
'permissionRead',
|
|
291
|
+
'Read all permission states',
|
|
292
|
+
await timeOf(() => permissions.getPermissions()),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// --- Deduplication and footprint -----------------------------------------
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < 400; i += 1) {
|
|
298
|
+
recordSeen(state, `${prefix}:dedup:${i}`, now());
|
|
299
|
+
}
|
|
300
|
+
record(
|
|
301
|
+
'dedupCheck',
|
|
302
|
+
'Deduplication ledger check',
|
|
303
|
+
await timeOf(async () => recordSeen(state, `${prefix}:dedup:probe`, now())),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
record(
|
|
307
|
+
'stateFootprintBytes',
|
|
308
|
+
'Persisted state on disk at caps',
|
|
309
|
+
state.health().approximateBytes,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// The interface-thread ceiling is a property of one chunked unit of work, not
|
|
313
|
+
// of a whole pass, so it is measured per entry rather than per reconcile.
|
|
314
|
+
const chunkMs = await timeOf(() =>
|
|
315
|
+
reconciler.reconcile({ domain: prefix, desired: hundred.slice(0, 25) }),
|
|
316
|
+
);
|
|
317
|
+
record('interfaceThreadUnit', 'Single chunked unit of reconcile work', chunkMs / 25);
|
|
318
|
+
|
|
319
|
+
await engine.cancelAll();
|
|
320
|
+
|
|
321
|
+
const passed = measurements.filter(measurement => measurement.passed).length;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
measurements,
|
|
325
|
+
passed,
|
|
326
|
+
failed: measurements.length - passed,
|
|
327
|
+
// Stated rather than silently omitted — these need real hardware.
|
|
328
|
+
requiresDevice: [
|
|
329
|
+
'Delivery accuracy, exact mode (60 s) — needs the OS to fire a real alarm',
|
|
330
|
+
'Delivery accuracy, approximate mode (15 min) — needs a device under no vendor restriction',
|
|
331
|
+
'Cold-start tap to handler (500 ms) — needs a real process death and relaunch',
|
|
332
|
+
'App startup delta (100 ms) — needs a real cold start',
|
|
333
|
+
'Sustained frame rate during reconcile (55 fps) — needs a rendering device',
|
|
334
|
+
],
|
|
335
|
+
ranAt: now(),
|
|
336
|
+
};
|
|
337
|
+
}
|