@keepsnappy/react-native 0.1.0-alpha.14 → 0.1.0-alpha.16
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/README.md +12 -0
- package/js/babel-transformer.js +24 -6
- package/js/component-contract.cjs +14 -0
- package/js/component-stamps.js +192 -0
- package/js/sdk/bytes.ts +20 -0
- package/js/sdk/core.ts +55 -14
- package/js/sdk/device-model.ts +10 -0
- package/js/sdk/fiber.ts +282 -28
- package/js/sdk/interactions.ts +23 -20
- package/js/sdk/place.ts +25 -0
- package/js/sdk/profile-export.ts +15 -0
- package/js/sdk/profile.ts +272 -18
- package/js/sdk/push-install.ts +2 -1
- package/js/sdk/push.ts +3 -19
- package/js/sdk/screens.ts +83 -4
- package/metro.js +11 -1
- package/package.json +1 -1
package/js/sdk/profile.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// (actualDuration / selfBaseDuration) and updaters (root.memoizedUpdaters)
|
|
5
5
|
// exist only when the app runs React's profiling renderer.
|
|
6
6
|
|
|
7
|
+
import { MAX_BATCH_BYTES, utf8Length } from './bytes';
|
|
7
8
|
import {
|
|
8
9
|
CLASS_COMPONENT,
|
|
9
10
|
componentName,
|
|
@@ -11,6 +12,9 @@ import {
|
|
|
11
12
|
FORWARD_REF,
|
|
12
13
|
FUNCTION_COMPONENT,
|
|
13
14
|
SIMPLE_MEMO_COMPONENT,
|
|
15
|
+
clientKeyOf,
|
|
16
|
+
isComponentTag,
|
|
17
|
+
OUTSIDE_APP,
|
|
14
18
|
} from './fiber';
|
|
15
19
|
|
|
16
20
|
export interface HookNode {
|
|
@@ -179,7 +183,55 @@ export const updaterNames = (root: ProfiledRoot): string[] | null => {
|
|
|
179
183
|
return names.sort();
|
|
180
184
|
};
|
|
181
185
|
|
|
182
|
-
|
|
186
|
+
const MAX_OWNER_UP = 512;
|
|
187
|
+
const updaterOwners = (root: ProfiledRoot) => {
|
|
188
|
+
if (!(root.memoizedUpdaters instanceof Set)) return { owners: null, unresolved: 0 };
|
|
189
|
+
const known = new Map<Fiber, string>();
|
|
190
|
+
const unresolvedPaths = new Set<Fiber>();
|
|
191
|
+
const owners: string[] = [];
|
|
192
|
+
let unresolved = 0;
|
|
193
|
+
for (const updater of root.memoizedUpdaters) {
|
|
194
|
+
const path: Fiber[] = [];
|
|
195
|
+
let fiber: Fiber | null = updater, owner = OUTSIDE_APP, capped = false;
|
|
196
|
+
for (let hops = 0; fiber !== null && hops < MAX_OWNER_UP; hops++) {
|
|
197
|
+
const cached = known.get(fiber);
|
|
198
|
+
if (cached !== undefined) {
|
|
199
|
+
owner = cached;
|
|
200
|
+
capped = unresolvedPaths.has(fiber);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
path.push(fiber);
|
|
204
|
+
const own = isComponentTag(fiber.tag) ? clientKeyOf(fiber) : null;
|
|
205
|
+
if (own !== null) { owner = own; break; }
|
|
206
|
+
fiber = fiber.return;
|
|
207
|
+
if (fiber !== null && hops === MAX_OWNER_UP - 1) capped = true;
|
|
208
|
+
}
|
|
209
|
+
for (const f of path) {
|
|
210
|
+
known.set(f, owner);
|
|
211
|
+
// A capped path stays unresolved for later updaters joining that path.
|
|
212
|
+
if (capped) unresolvedPaths.add(f);
|
|
213
|
+
}
|
|
214
|
+
if (capped) unresolved++;
|
|
215
|
+
owners.push(owner);
|
|
216
|
+
}
|
|
217
|
+
return { owners, unresolved };
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
interface RolledTime { ms: number; renders: number }
|
|
221
|
+
interface RolledRest { ms: number; n: number; saturated?: true }
|
|
222
|
+
interface RestedLibraries { ms: number; names: Set<string>; saturated: boolean }
|
|
223
|
+
const ROLLUP_LIMITS = { trackedNames: 64, restNames: 64 } as const;
|
|
224
|
+
interface RollupDetail {
|
|
225
|
+
rolled: Map<string, RolledTime> | null;
|
|
226
|
+
rolledRest: RestedLibraries | null;
|
|
227
|
+
}
|
|
228
|
+
interface OutsideProfile extends RollupDetail {
|
|
229
|
+
selfMs: number;
|
|
230
|
+
mountSelfMs: number;
|
|
231
|
+
}
|
|
232
|
+
const emptyOutside = (): OutsideProfile => ({ selfMs: 0, mountSelfMs: 0, rolled: null, rolledRest: null });
|
|
233
|
+
|
|
234
|
+
export interface ComponentProfile extends RollupDetail {
|
|
183
235
|
renders: number;
|
|
184
236
|
mounts: number;
|
|
185
237
|
// Distinct instances (fiber pairs) that rendered.
|
|
@@ -195,15 +247,25 @@ export interface ComponentProfile {
|
|
|
195
247
|
context: number;
|
|
196
248
|
parent: number;
|
|
197
249
|
other: number;
|
|
250
|
+
rolledMs: number;
|
|
251
|
+
// These describe the first parent until a second distinct parent needs `parents`.
|
|
252
|
+
firstParent: string | null;
|
|
253
|
+
parentCount: number;
|
|
254
|
+
parents: Map<string | null, number> | null;
|
|
198
255
|
}
|
|
199
256
|
|
|
200
257
|
export interface CommitProfile {
|
|
201
258
|
// Index of the commit in this process (1-based), for aligning windows.
|
|
202
259
|
seq: number;
|
|
260
|
+
// When the commit landed: the hook ran after React rendered it (performance.now), null when the caller does not know it.
|
|
261
|
+
endMs: number | null;
|
|
203
262
|
timed: boolean;
|
|
204
263
|
renderMs: number | null;
|
|
205
264
|
updaters: string[] | null;
|
|
265
|
+
updaterOwners: string[] | null;
|
|
266
|
+
updatersUnresolved: number;
|
|
206
267
|
components: Map<string, ComponentProfile>;
|
|
268
|
+
outside: OutsideProfile | null;
|
|
207
269
|
}
|
|
208
270
|
|
|
209
271
|
const emptyComponent = (): ComponentProfile => ({
|
|
@@ -220,16 +282,27 @@ const emptyComponent = (): ComponentProfile => ({
|
|
|
220
282
|
context: 0,
|
|
221
283
|
parent: 0,
|
|
222
284
|
other: 0,
|
|
285
|
+
rolledMs: 0,
|
|
286
|
+
rolled: null,
|
|
287
|
+
rolledRest: null,
|
|
288
|
+
firstParent: null,
|
|
289
|
+
parentCount: 0,
|
|
290
|
+
parents: null,
|
|
223
291
|
});
|
|
224
292
|
|
|
225
|
-
export const createCommitProfile = (root: ProfiledRoot, seq = 0): CommitProfile => {
|
|
293
|
+
export const createCommitProfile = (root: ProfiledRoot, seq = 0, endMs: number | null = null): CommitProfile => {
|
|
226
294
|
const timed = isProfilingFiber(root.current);
|
|
295
|
+
const updaters = timed ? updaterOwners(root) : { owners: null, unresolved: 0 };
|
|
227
296
|
return {
|
|
228
297
|
seq,
|
|
298
|
+
endMs,
|
|
229
299
|
timed,
|
|
230
300
|
renderMs: timed ? totalMs(root.current) : null,
|
|
231
301
|
updaters: timed ? updaterNames(root) : null,
|
|
302
|
+
updaterOwners: updaters.owners,
|
|
303
|
+
updatersUnresolved: updaters.unresolved,
|
|
232
304
|
components: new Map(),
|
|
305
|
+
outside: timed ? emptyOutside() : null,
|
|
233
306
|
};
|
|
234
307
|
};
|
|
235
308
|
|
|
@@ -237,12 +310,84 @@ const bump = (map: Record<string, number>, key: string, by = 1) => {
|
|
|
237
310
|
map[key] = (map[key] ?? 0) + by;
|
|
238
311
|
};
|
|
239
312
|
|
|
313
|
+
const noteParent = (entry: ComponentProfile, parent: string | null, count = 1) => {
|
|
314
|
+
if (entry.parents !== null) {
|
|
315
|
+
entry.parents.set(parent, (entry.parents.get(parent) ?? 0) + count);
|
|
316
|
+
} else if (entry.parentCount === 0) {
|
|
317
|
+
entry.firstParent = parent;
|
|
318
|
+
entry.parentCount = count;
|
|
319
|
+
} else if (entry.firstParent === parent) {
|
|
320
|
+
entry.parentCount += count;
|
|
321
|
+
} else {
|
|
322
|
+
entry.parents = new Map([[entry.firstParent, entry.parentCount], [parent, count]]);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const restBucket = (into: RollupDetail) =>
|
|
327
|
+
into.rolledRest ??= { ms: 0, names: new Set<string>(), saturated: false };
|
|
328
|
+
|
|
329
|
+
const noteRestName = (rest: RestedLibraries, name: string) => {
|
|
330
|
+
if (!rest.names.has(name)) {
|
|
331
|
+
if (rest.names.size < ROLLUP_LIMITS.restNames) rest.names.add(name);
|
|
332
|
+
else rest.saturated = true;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Keep the same lexicographically first names at both layers. A name omitted
|
|
337
|
+
// from a commit has 64 smaller names ahead of it, so it cannot enter the
|
|
338
|
+
// window's tracked set either. Tracked totals stay complete and can be ranked
|
|
339
|
+
// by time even after the bounded distinct-rest counter saturates.
|
|
340
|
+
const addRolled = (
|
|
341
|
+
into: RollupDetail,
|
|
342
|
+
name: string, ms: number, renders: number,
|
|
343
|
+
) => {
|
|
344
|
+
const known = into.rolled?.get(name);
|
|
345
|
+
if (known) { known.ms += ms; known.renders += renders; return; }
|
|
346
|
+
const rolled = into.rolled ??= new Map();
|
|
347
|
+
if (rolled.size < ROLLUP_LIMITS.trackedNames) { rolled.set(name, { ms, renders }); return; }
|
|
348
|
+
let last: [string, RolledTime] | null = null;
|
|
349
|
+
for (const pair of rolled) if (!last || pair[0] > last[0]) last = pair;
|
|
350
|
+
const rest = restBucket(into);
|
|
351
|
+
if (last && name < last[0]) {
|
|
352
|
+
rolled.delete(last[0]);
|
|
353
|
+
rolled.set(name, { ms, renders });
|
|
354
|
+
rest.ms += last[1].ms;
|
|
355
|
+
noteRestName(rest, last[0]);
|
|
356
|
+
} else {
|
|
357
|
+
rest.ms += ms;
|
|
358
|
+
noteRestName(rest, name);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export const recordRolledUp = (
|
|
363
|
+
commit: CommitProfile, owner: string, parent: string | null, name: string, fiber: ProfiledFiber,
|
|
364
|
+
) => {
|
|
365
|
+
if (!commit.timed) return;
|
|
366
|
+
const self = selfMs(fiber);
|
|
367
|
+
if (self === null) return;
|
|
368
|
+
if (owner === OUTSIDE_APP) {
|
|
369
|
+
const outside = commit.outside as OutsideProfile;
|
|
370
|
+
outside.selfMs += self;
|
|
371
|
+
if (fiber.alternate === null) outside.mountSelfMs += self;
|
|
372
|
+
addRolled(outside, name, self, 1);
|
|
373
|
+
} else {
|
|
374
|
+
let entry = commit.components.get(owner);
|
|
375
|
+
if (!entry) { entry = emptyComponent(); commit.components.set(owner, entry); }
|
|
376
|
+
entry.selfMs += self;
|
|
377
|
+
entry.rolledMs += self;
|
|
378
|
+
if (fiber.alternate === null) entry.mountSelfMs += self;
|
|
379
|
+
noteParent(entry, parent);
|
|
380
|
+
addRolled(entry, name, self, 1);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
240
384
|
export const recordFiber = (
|
|
241
385
|
commit: CommitProfile,
|
|
242
386
|
name: string,
|
|
243
387
|
fiber: ProfiledFiber,
|
|
244
388
|
cause: UpdateCause,
|
|
245
389
|
instance?: number,
|
|
390
|
+
parent: string | null = null,
|
|
246
391
|
) => {
|
|
247
392
|
let entry = commit.components.get(name);
|
|
248
393
|
if (!entry) {
|
|
@@ -250,6 +395,7 @@ export const recordFiber = (
|
|
|
250
395
|
commit.components.set(name, entry);
|
|
251
396
|
}
|
|
252
397
|
entry.renders++;
|
|
398
|
+
noteParent(entry, parent);
|
|
253
399
|
if (instance !== undefined) {
|
|
254
400
|
entry.instances.add(instance);
|
|
255
401
|
}
|
|
@@ -299,8 +445,12 @@ export interface WindowProfile {
|
|
|
299
445
|
lastSeq: number | null;
|
|
300
446
|
timedCommits: number;
|
|
301
447
|
renderMs: number[];
|
|
448
|
+
// The judged commit that set the largest render time (the earliest seq on a tie), kept as the max is updated.
|
|
449
|
+
slowest: { seq: number; endMs: number | null; ms: number } | null;
|
|
302
450
|
updaters: Record<string, number>;
|
|
451
|
+
updatersUnresolved: number;
|
|
303
452
|
components: Map<string, ComponentProfile>;
|
|
453
|
+
outside: OutsideProfile;
|
|
304
454
|
}
|
|
305
455
|
|
|
306
456
|
export const createWindowProfile = (): WindowProfile => ({
|
|
@@ -309,8 +459,11 @@ export const createWindowProfile = (): WindowProfile => ({
|
|
|
309
459
|
lastSeq: null,
|
|
310
460
|
timedCommits: 0,
|
|
311
461
|
renderMs: [],
|
|
462
|
+
slowest: null,
|
|
312
463
|
updaters: {},
|
|
464
|
+
updatersUnresolved: 0,
|
|
313
465
|
components: new Map(),
|
|
466
|
+
outside: emptyOutside(),
|
|
314
467
|
});
|
|
315
468
|
|
|
316
469
|
const mergeCounts = (into: Record<string, number>, from: Record<string, number>) => {
|
|
@@ -319,6 +472,16 @@ const mergeCounts = (into: Record<string, number>, from: Record<string, number>)
|
|
|
319
472
|
}
|
|
320
473
|
};
|
|
321
474
|
|
|
475
|
+
const mergeRolled = (into: RollupDetail, from: RollupDetail) => {
|
|
476
|
+
for (const [name, time] of from.rolled ?? []) addRolled(into, name, time.ms, time.renders);
|
|
477
|
+
if (from.rolledRest) {
|
|
478
|
+
const rest = restBucket(into);
|
|
479
|
+
for (const name of from.rolledRest.names) noteRestName(rest, name);
|
|
480
|
+
rest.ms += from.rolledRest.ms;
|
|
481
|
+
rest.saturated ||= from.rolledRest.saturated;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
322
485
|
export const addCommitProfile = (window: WindowProfile, commit: CommitProfile) => {
|
|
323
486
|
window.commits++;
|
|
324
487
|
if (commit.seq > 0) {
|
|
@@ -330,10 +493,20 @@ export const addCommitProfile = (window: WindowProfile, commit: CommitProfile) =
|
|
|
330
493
|
}
|
|
331
494
|
if (commit.renderMs !== null) {
|
|
332
495
|
window.renderMs.push(commit.renderMs);
|
|
496
|
+
const slowest = window.slowest;
|
|
497
|
+
if (commit.seq > 0 && (slowest === null || commit.renderMs > slowest.ms || (commit.renderMs === slowest.ms && commit.seq < slowest.seq))) {
|
|
498
|
+
window.slowest = { seq: commit.seq, endMs: commit.endMs, ms: commit.renderMs };
|
|
499
|
+
}
|
|
333
500
|
}
|
|
334
|
-
for (const name of commit.
|
|
501
|
+
for (const name of new Set(commit.updaterOwners ?? [])) {
|
|
335
502
|
bump(window.updaters, name);
|
|
336
503
|
}
|
|
504
|
+
window.updatersUnresolved += commit.updatersUnresolved;
|
|
505
|
+
if (commit.outside) {
|
|
506
|
+
window.outside.selfMs += commit.outside.selfMs;
|
|
507
|
+
window.outside.mountSelfMs += commit.outside.mountSelfMs;
|
|
508
|
+
mergeRolled(window.outside, commit.outside);
|
|
509
|
+
}
|
|
337
510
|
for (const [name, from] of commit.components) {
|
|
338
511
|
let into = window.components.get(name);
|
|
339
512
|
if (!into) {
|
|
@@ -353,6 +526,13 @@ export const addCommitProfile = (window: WindowProfile, commit: CommitProfile) =
|
|
|
353
526
|
into.context += from.context;
|
|
354
527
|
into.parent += from.parent;
|
|
355
528
|
into.other += from.other;
|
|
529
|
+
into.rolledMs += from.rolledMs;
|
|
530
|
+
mergeRolled(into, from);
|
|
531
|
+
if (from.parents) {
|
|
532
|
+
for (const [parent, count] of from.parents) noteParent(into, parent, count);
|
|
533
|
+
} else if (from.parentCount > 0) {
|
|
534
|
+
noteParent(into, from.firstParent, from.parentCount);
|
|
535
|
+
}
|
|
356
536
|
mergeCounts(into.props, from.props);
|
|
357
537
|
mergeCounts(into.hooks, from.hooks);
|
|
358
538
|
}
|
|
@@ -368,9 +548,18 @@ const sortedCounts = (map: Record<string, number>) => {
|
|
|
368
548
|
return out;
|
|
369
549
|
};
|
|
370
550
|
|
|
371
|
-
// Sizes of the per-window summary (plan 40 §3.1): component rows
|
|
372
|
-
//
|
|
373
|
-
|
|
551
|
+
// Sizes of the per-window summary (plan 40 §3.1, plan 79 S9): every component gets a row until the valve (`rows` or `rowBytes`; the smallest are dropped
|
|
552
|
+
// and counted in `omitted`). The first `detailRows` rows, reported in the summary, also get causes, shares and roll-up: the top `detailTop` and every row at
|
|
553
|
+
// FULL_ROW_MS, as far as rows and detail together fit `profileBytes`. Half the batch cap, so a window line is never dropped.
|
|
554
|
+
export const PROFILE_LIMITS = {
|
|
555
|
+
rows: 512, rowBytes: 64 * 1024, detailTop: 30, profileBytes: MAX_BATCH_BYTES / 2,
|
|
556
|
+
causeKeys: 3, updaters: 5,
|
|
557
|
+
rollupTop: 5,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// A component with at least this much self time (as sent, rounded) keeps cause, share and roll-up detail in the window summary. Half the lowest ms floor of
|
|
561
|
+
// the component rules (verdict-core clause T-WARN-MS); a test pins it, because the SDK cannot import the clause table.
|
|
562
|
+
export const FULL_ROW_MS = 2.5;
|
|
374
563
|
|
|
375
564
|
// The budgets every timed window counts commits against (120 Hz and 60 Hz),
|
|
376
565
|
// plus the device's own when it is neither.
|
|
@@ -457,14 +646,44 @@ export const overBudgetCounts = (renderMs: number[], budgets: number[]) => {
|
|
|
457
646
|
const DEFAULT_BUDGET: FrameBudget = { ms: 16.7, hz: null, source: 'default' };
|
|
458
647
|
|
|
459
648
|
// Component rows: [name, renders, mounts, instances, selfMs, totalMs]; times
|
|
460
|
-
// are null without the profiling renderer. Rows are
|
|
461
|
-
// time when timed, else by renders (then name),
|
|
462
|
-
// with the same renders prints the same rows.
|
|
649
|
+
// are null without the profiling renderer. Rows are every component by self
|
|
650
|
+
// time when timed, else by renders (then name), up to the valve in PROFILE_LIMITS,
|
|
651
|
+
// so a production-renderer run with the same renders prints the same rows.
|
|
463
652
|
export type ComponentRow = [string, number, number, number, number | null, number | null];
|
|
464
653
|
|
|
654
|
+
const rolledSummary = (entry: RollupDetail, limit: number) => {
|
|
655
|
+
const all = [...(entry.rolled ?? [])].sort((a, b) => b[1].ms - a[1].ms || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
656
|
+
return {
|
|
657
|
+
rolledUp: all.slice(0, limit).map(([name, time]): [string, number, number] => [name, round(time.ms), time.renders]),
|
|
658
|
+
rolledUpRest: {
|
|
659
|
+
ms: round(all.slice(limit).reduce((sum, [, time]) => sum + time.ms, entry.rolledRest?.ms ?? 0)),
|
|
660
|
+
n: (entry.rolledRest?.names.size ?? 0) + Math.max(0, all.length - limit),
|
|
661
|
+
...(entry.rolledRest?.saturated ? { saturated: true as const } : {}),
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const dominantParent = (entry: ComponentProfile): string | null => {
|
|
667
|
+
if (entry.parents === null) return entry.firstParent;
|
|
668
|
+
let parent: string | null = null, count = -1;
|
|
669
|
+
for (const [candidate, occurrences] of entry.parents) {
|
|
670
|
+
if (occurrences > count || (occurrences === count && (candidate ?? '') < (parent ?? ''))) {
|
|
671
|
+
parent = candidate; count = occurrences;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return parent;
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
interface RollupSummary {
|
|
678
|
+
parent: string | null;
|
|
679
|
+
rolledUpMs: number;
|
|
680
|
+
rolledUp: [string, number, number][];
|
|
681
|
+
rolledUpRest: RolledRest;
|
|
682
|
+
}
|
|
683
|
+
|
|
465
684
|
export const summarizeWindowProfile = (
|
|
466
685
|
window: WindowProfile,
|
|
467
|
-
options: { budget?: FrameBudget; limits?: Partial<typeof PROFILE_LIMITS
|
|
686
|
+
options: { budget?: FrameBudget; limits?: Partial<typeof PROFILE_LIMITS>; t0?: number } = {},
|
|
468
687
|
) => {
|
|
469
688
|
const limits = { ...PROFILE_LIMITS, ...options.limits };
|
|
470
689
|
const budget = options.budget ?? DEFAULT_BUDGET;
|
|
@@ -475,31 +694,54 @@ export const summarizeWindowProfile = (
|
|
|
475
694
|
b[1].renders - a[1].renders ||
|
|
476
695
|
(a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0),
|
|
477
696
|
);
|
|
478
|
-
const
|
|
479
|
-
const components: ComponentRow[] = kept.map(([name, entry]) => [
|
|
697
|
+
const rowOf = ([name, entry]: [string, ComponentProfile]): ComponentRow => [
|
|
480
698
|
name,
|
|
481
699
|
entry.renders,
|
|
482
700
|
entry.mounts,
|
|
483
701
|
entry.instances.size,
|
|
484
702
|
timed ? round(entry.selfMs) : null,
|
|
485
|
-
timed ? round(entry.totalMs) : null,
|
|
486
|
-
]
|
|
703
|
+
timed && entry.timed > 0 ? round(entry.totalMs) : null,
|
|
704
|
+
];
|
|
705
|
+
const components: ComponentRow[] = [];
|
|
706
|
+
let rowBytes = 0;
|
|
707
|
+
for (const item of all) {
|
|
708
|
+
const row = rowOf(item);
|
|
709
|
+
const bytes = utf8Length(JSON.stringify(row)) + 1;
|
|
710
|
+
if (components.length >= limits.rows || rowBytes + bytes > limits.rowBytes) break;
|
|
711
|
+
components.push(row);
|
|
712
|
+
rowBytes += bytes;
|
|
713
|
+
}
|
|
714
|
+
const kept = all.slice(0, components.length);
|
|
487
715
|
const causes: Record<string, CauseSummary> = {};
|
|
488
716
|
const shares: Record<string, Record<string, number>> = {};
|
|
489
|
-
|
|
717
|
+
const rollup: Record<string, RollupSummary> = {};
|
|
718
|
+
// Detail is a prefix of the rows: sorted by self time, the rows at FULL_ROW_MS follow the top rows directly.
|
|
719
|
+
let detailRows = 0, detailBytes = 0;
|
|
720
|
+
for (const [index, [name, entry]] of kept.entries()) {
|
|
721
|
+
if (index >= limits.detailTop && !(timed && round(entry.selfMs) >= FULL_ROW_MS)) break;
|
|
490
722
|
const summary = causeSummary(entry, limits.causeKeys);
|
|
723
|
+
const share = causeShares(entry, limits.causeKeys);
|
|
724
|
+
const parent = dominantParent(entry);
|
|
725
|
+
const rolled = timed && (parent !== null || entry.rolledMs > 0)
|
|
726
|
+
? { parent, rolledUpMs: round(entry.rolledMs), ...rolledSummary(entry, limits.rollupTop) }
|
|
727
|
+
: null;
|
|
728
|
+
const bytes = 3 * utf8Length(name) + utf8Length(JSON.stringify([summary, share, rolled])) + 12;
|
|
729
|
+
if (rowBytes + detailBytes + bytes > limits.profileBytes) break;
|
|
730
|
+
detailBytes += bytes;
|
|
731
|
+
detailRows++;
|
|
491
732
|
if (Object.keys(summary).length > 0) {
|
|
492
733
|
causes[name] = summary;
|
|
493
734
|
}
|
|
494
|
-
shares[name] =
|
|
735
|
+
shares[name] = share;
|
|
736
|
+
if (rolled) rollup[name] = rolled;
|
|
495
737
|
}
|
|
496
738
|
const updaters = Object.entries(window.updaters).sort(ranked);
|
|
497
739
|
const renderSum = window.renderMs.reduce((s, n) => s + n, 0);
|
|
498
740
|
const selfTotal = timed
|
|
499
|
-
? [...window.components.values()].reduce((s, e) => s + e.selfMs,
|
|
741
|
+
? [...window.components.values()].reduce((s, e) => s + e.selfMs, window.outside.selfMs)
|
|
500
742
|
: null;
|
|
501
743
|
const mountSelfTotal = timed
|
|
502
|
-
? [...window.components.values()].reduce((s, e) => s + e.mountSelfMs,
|
|
744
|
+
? [...window.components.values()].reduce((s, e) => s + e.mountSelfMs, window.outside.mountSelfMs)
|
|
503
745
|
: null;
|
|
504
746
|
return {
|
|
505
747
|
renderer: timed ? 'profiling' : 'production',
|
|
@@ -508,6 +750,10 @@ export const summarizeWindowProfile = (
|
|
|
508
750
|
window.firstSeq === null || window.lastSeq === null
|
|
509
751
|
? null
|
|
510
752
|
: [window.firstSeq, window.lastSeq],
|
|
753
|
+
slowestCommitSeq: timed ? window.slowest?.seq ?? null : null,
|
|
754
|
+
// When that commit landed, ms after the window start; negative for a commit a later-opened window took by backfill.
|
|
755
|
+
slowestCommitEndAtMs:
|
|
756
|
+
timed && window.slowest?.endMs != null && options.t0 !== undefined ? round(window.slowest.endMs - options.t0) : null,
|
|
511
757
|
renderMs: timed
|
|
512
758
|
? {
|
|
513
759
|
sum: round(renderSum),
|
|
@@ -521,9 +767,17 @@ export const summarizeWindowProfile = (
|
|
|
521
767
|
: null,
|
|
522
768
|
selfMsTotal: selfTotal === null ? null : round(selfTotal),
|
|
523
769
|
mountSelfMs: mountSelfTotal === null ? null : round(mountSelfTotal),
|
|
770
|
+
stamped: all.filter(([, entry]) => entry.renders > 0).length,
|
|
771
|
+
...(timed ? {
|
|
772
|
+
rollup,
|
|
773
|
+
outside: { selfMs: round(window.outside.selfMs), mountSelfMs: round(window.outside.mountSelfMs), ...rolledSummary(window.outside, limits.rollupTop) },
|
|
774
|
+
rolledUpMsTotal: round(all.reduce((sum, [, entry]) => sum + entry.rolledMs, window.outside.selfMs)),
|
|
775
|
+
} : {}),
|
|
524
776
|
updaters: timed ? sortedCounts(Object.fromEntries(updaters.slice(0, limits.updaters))) : null,
|
|
525
777
|
updatersOmitted: timed ? Math.max(0, updaters.length - limits.updaters) : 0,
|
|
778
|
+
updatersUnresolved: window.updatersUnresolved,
|
|
526
779
|
components,
|
|
780
|
+
detailRows,
|
|
527
781
|
causes,
|
|
528
782
|
shares,
|
|
529
783
|
omitted: all.length - kept.length,
|
package/js/sdk/push-install.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { AppState, Platform } from 'react-native';
|
|
6
6
|
|
|
7
7
|
import { MODE } from './activation';
|
|
8
|
+
import { deviceModelOf } from './device-model';
|
|
8
9
|
import { type ArtifactUploadNative, createArtifactUploader } from './artifacts';
|
|
9
10
|
import {
|
|
10
11
|
addStartupSection,
|
|
@@ -41,7 +42,7 @@ const readDevice = async (): Promise<DeviceInfo> => {
|
|
|
41
42
|
let build: string | null = null;
|
|
42
43
|
try {
|
|
43
44
|
const Device = require('expo-device');
|
|
44
|
-
model = Device
|
|
45
|
+
model = deviceModelOf(Device);
|
|
45
46
|
os = Device.osVersion ?? null;
|
|
46
47
|
} catch {
|
|
47
48
|
// device fields stay null
|
package/js/sdk/push.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// rewritten to the live queue whenever it outgrows twice the queue caps, so it
|
|
12
12
|
// stays bounded while the ingest is unreachable.
|
|
13
13
|
|
|
14
|
+
import { MAX_BATCH_BYTES, utf8Length } from './bytes';
|
|
15
|
+
|
|
14
16
|
export interface DeviceInfo {
|
|
15
17
|
model: string | null;
|
|
16
18
|
os: string | null;
|
|
@@ -208,31 +210,13 @@ export const readSpool = (content: string | null): { pending: PendingLine[]; dro
|
|
|
208
210
|
export const pendingFromSpool = (content: string | null): PendingLine[] =>
|
|
209
211
|
readSpool(content).pending;
|
|
210
212
|
|
|
211
|
-
const utf8Length = (text: string) => {
|
|
212
|
-
let n = 0;
|
|
213
|
-
for (let i = 0; i < text.length; i++) {
|
|
214
|
-
const c = text.charCodeAt(i);
|
|
215
|
-
if (c < 0x80) {
|
|
216
|
-
n += 1;
|
|
217
|
-
} else if (c < 0x800) {
|
|
218
|
-
n += 2;
|
|
219
|
-
} else if (c >= 0xd800 && c <= 0xdbff) {
|
|
220
|
-
n += 4;
|
|
221
|
-
i++;
|
|
222
|
-
} else {
|
|
223
|
-
n += 3;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return n;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
213
|
// Retrying cannot fix these; the batch is dropped instead of blocking the queue.
|
|
230
214
|
const isPermanent = (status: number) =>
|
|
231
215
|
status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
232
216
|
|
|
233
217
|
export const createPushClient = (deps: PushDeps) => {
|
|
234
218
|
const idleFlushMs = deps.idleFlushMs ?? 10_000;
|
|
235
|
-
const maxBatchBytes = deps.maxBatchBytes ??
|
|
219
|
+
const maxBatchBytes = deps.maxBatchBytes ?? MAX_BATCH_BYTES;
|
|
236
220
|
const maxBatchItems = deps.maxBatchItems ?? 400;
|
|
237
221
|
const maxQueueLines = deps.maxQueueLines ?? 2000;
|
|
238
222
|
const maxQueueBytes = deps.maxQueueBytes ?? 8 * 1024 * 1024;
|
package/js/sdk/screens.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from './core';
|
|
27
27
|
import { retainFloorOf } from './retain-floor';
|
|
28
28
|
import { type CauseRoot, currentCause, latestActionAt, launchedAt, launchRootId } from './causes';
|
|
29
|
-
import { routeOfScreenHost, routes } from './fiber';
|
|
29
|
+
import { buildPlaceId, routeOfScreenHost, routes, screenKeyForRoute, setScreenKeyProvider, currentFiber, type ScreenRouteResolution, type Fiber } from './fiber';
|
|
30
30
|
import {
|
|
31
31
|
lastPressStart,
|
|
32
32
|
type NativeScreenEvent,
|
|
@@ -64,6 +64,11 @@ interface Visit {
|
|
|
64
64
|
chainKey: string;
|
|
65
65
|
keyChain: string;
|
|
66
66
|
visit: number;
|
|
67
|
+
screenKey: string | null;
|
|
68
|
+
legacyName: string;
|
|
69
|
+
pendingCommits: number;
|
|
70
|
+
resolution: ScreenRouteResolution;
|
|
71
|
+
lazyBoundary: Fiber | null;
|
|
67
72
|
t0: number;
|
|
68
73
|
openedAt: number;
|
|
69
74
|
endedAt: number | null;
|
|
@@ -89,6 +94,7 @@ interface Resolution {
|
|
|
89
94
|
let current: Visit | null = null;
|
|
90
95
|
let seq = 0;
|
|
91
96
|
const visitsByChain = new Map<string, number>();
|
|
97
|
+
const visitsByScreen = new Map<string, number>();
|
|
92
98
|
let lastActionAt = -Infinity;
|
|
93
99
|
interface AppLink {
|
|
94
100
|
t: number;
|
|
@@ -248,6 +254,7 @@ const endVisit = (reason: string, at?: number) => {
|
|
|
248
254
|
if (!visit || visit.endedAt !== null) {
|
|
249
255
|
return;
|
|
250
256
|
}
|
|
257
|
+
if (visit.pendingCommits > 0) resolveVisit(visit, true);
|
|
251
258
|
const t = Math.max(visit.t0, at ?? now());
|
|
252
259
|
visit.endedAt = t;
|
|
253
260
|
stats.ends[reason] = (stats.ends[reason] ?? 0) + 1;
|
|
@@ -332,6 +339,28 @@ const isStrictExtension = (prev: RoutePart[], next: RoutePart[]) =>
|
|
|
332
339
|
(part.key ?? null) === (next[i]?.key ?? null),
|
|
333
340
|
);
|
|
334
341
|
|
|
342
|
+
const screenResolution = (chain: RoutePart[]) => {
|
|
343
|
+
const leaf = chain[chain.length - 1]!;
|
|
344
|
+
return screenKeyForRoute(leaf.key ?? null, leaf.name);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const moveScreenCounter = (visit: Visit, screen: string) => {
|
|
348
|
+
if (visit.screenKey === screen) return;
|
|
349
|
+
if (visit.screenKey !== null) visitsByScreen.set(visit.screenKey, (visitsByScreen.get(visit.screenKey) ?? 1) - 1);
|
|
350
|
+
visit.visit = (visitsByScreen.get(screen) ?? 0) + 1;
|
|
351
|
+
visitsByScreen.set(screen, visit.visit);
|
|
352
|
+
visit.screenKey = screen;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const resolveVisit = (visit: Visit, final = false) => {
|
|
356
|
+
const resolved = visit.resolution;
|
|
357
|
+
if (resolved.screenPending && !final) return;
|
|
358
|
+
moveScreenCounter(visit, resolved.screen);
|
|
359
|
+
visit.name = buildPlaceId({ kind: 'screen', screen: resolved.screen, visit: visit.visit });
|
|
360
|
+
visit.pendingCommits = 0;
|
|
361
|
+
markers.rename(visit.key, visit.name, visit.legacyName);
|
|
362
|
+
};
|
|
363
|
+
|
|
335
364
|
const extend = (visit: Visit, chain: RoutePart[], chainKey: string) => {
|
|
336
365
|
const previous = visitsByChain.get(visit.chainKey) ?? 1;
|
|
337
366
|
visitsByChain.set(visit.chainKey, previous - 1);
|
|
@@ -341,9 +370,19 @@ const extend = (visit: Visit, chain: RoutePart[], chainKey: string) => {
|
|
|
341
370
|
visit.chain = chain;
|
|
342
371
|
visit.chainKey = chainKey;
|
|
343
372
|
visit.keyChain = keyChainOf(chain);
|
|
344
|
-
visit.
|
|
345
|
-
|
|
346
|
-
|
|
373
|
+
visit.legacyName = nameOf(chainKey, n);
|
|
374
|
+
const resolution = screenResolution(chain);
|
|
375
|
+
visit.resolution = resolution;
|
|
376
|
+
visit.lazyBoundary = resolution.suspended ?? null;
|
|
377
|
+
if (resolution.screenPending) {
|
|
378
|
+
if (visit.screenKey !== null) {
|
|
379
|
+
visitsByScreen.set(visit.screenKey, (visitsByScreen.get(visit.screenKey) ?? 1) - 1);
|
|
380
|
+
visit.screenKey = null;
|
|
381
|
+
}
|
|
382
|
+
visit.name = visit.legacyName;
|
|
383
|
+
visit.pendingCommits = 3;
|
|
384
|
+
markers.rename(visit.key, visit.name, visit.legacyName);
|
|
385
|
+
} else resolveVisit(visit);
|
|
347
386
|
stats.extended++;
|
|
348
387
|
};
|
|
349
388
|
|
|
@@ -398,6 +437,7 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
398
437
|
const n = (visitsByChain.get(chainKey) ?? 0) + 1;
|
|
399
438
|
visitsByChain.set(chainKey, n);
|
|
400
439
|
seq++;
|
|
440
|
+
const resolved = screenResolution(chain);
|
|
401
441
|
const visit: Visit = {
|
|
402
442
|
key: `screen#${seq}`,
|
|
403
443
|
name: nameOf(chainKey, n),
|
|
@@ -405,6 +445,11 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
405
445
|
chainKey,
|
|
406
446
|
keyChain,
|
|
407
447
|
visit: n,
|
|
448
|
+
legacyName: nameOf(chainKey, n),
|
|
449
|
+
screenKey: null,
|
|
450
|
+
pendingCommits: 3,
|
|
451
|
+
resolution: resolved,
|
|
452
|
+
lazyBoundary: resolved.suspended ?? null,
|
|
408
453
|
t0: start.t0,
|
|
409
454
|
openedAt: detectedAt,
|
|
410
455
|
endedAt: null,
|
|
@@ -417,6 +462,11 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
417
462
|
launch,
|
|
418
463
|
cause: snapshot.cause,
|
|
419
464
|
};
|
|
465
|
+
if (!resolved.screenPending) {
|
|
466
|
+
moveScreenCounter(visit, resolved.screen);
|
|
467
|
+
visit.name = buildPlaceId({ kind: 'screen', screen: resolved.screen, visit: visit.visit });
|
|
468
|
+
visit.pendingCommits = 0;
|
|
469
|
+
}
|
|
420
470
|
current = visit;
|
|
421
471
|
pressesSinceVisit = [];
|
|
422
472
|
stats.visits++;
|
|
@@ -429,6 +479,12 @@ const onSnapshot = (snapshot: NavSnapshot, nameSource: NameSource) => {
|
|
|
429
479
|
markers.start(visit.name, {
|
|
430
480
|
key: visit.key,
|
|
431
481
|
kind: 'screen',
|
|
482
|
+
legacyName: visit.legacyName,
|
|
483
|
+
beforeEnd: () => {
|
|
484
|
+
if (visit.pendingCommits > 0) resolveVisit(visit, true);
|
|
485
|
+
visit.lazyBoundary = null;
|
|
486
|
+
visit.resolution = { screen: visit.screenKey ?? visit.resolution.screen };
|
|
487
|
+
},
|
|
432
488
|
at: visit.t0,
|
|
433
489
|
details: details(visit),
|
|
434
490
|
// The launch window belongs to the launch, whatever action came before the first state (REQ-MEAS-22).
|
|
@@ -530,6 +586,7 @@ export const screenStats = () => ({
|
|
|
530
586
|
visitsByChain: Object.fromEntries(
|
|
531
587
|
[...visitsByChain].filter(([, n]) => n > 0).slice(0, MAX_LISTED),
|
|
532
588
|
),
|
|
589
|
+
visitsByScreen: Object.fromEntries([...visitsByScreen].filter(([, n]) => n > 0).slice(0, MAX_LISTED)),
|
|
533
590
|
trackedRoutes: routes.size,
|
|
534
591
|
nav: navStats(),
|
|
535
592
|
native: {
|
|
@@ -569,6 +626,10 @@ export const installScreens = () => {
|
|
|
569
626
|
return;
|
|
570
627
|
}
|
|
571
628
|
installed = true;
|
|
629
|
+
setScreenKeyProvider(() => current ? {
|
|
630
|
+
screen: current.screenKey ?? current.resolution.screen,
|
|
631
|
+
...(current.pendingCommits > 0 || current.lazyBoundary ? { screenPending: true } : {}),
|
|
632
|
+
} : { screen: '(no screen)' });
|
|
572
633
|
setRetainFloor(screenRetainFloor);
|
|
573
634
|
onWindowsEnd(onWindowsEnded);
|
|
574
635
|
onNavSnapshot(snapshot => onSnapshot(snapshot, 'state'));
|
|
@@ -578,6 +639,24 @@ export const installScreens = () => {
|
|
|
578
639
|
setTimeout(() => logNativeEvent(event), RESOLVE_DELAY_MS);
|
|
579
640
|
});
|
|
580
641
|
onAfterCommit(() => {
|
|
642
|
+
const visit = current;
|
|
643
|
+
if (visit && isWindowOpen(visit.key)) {
|
|
644
|
+
if (visit.pendingCommits > 0) {
|
|
645
|
+
visit.resolution = screenResolution(visit.chain);
|
|
646
|
+
visit.lazyBoundary = visit.resolution.suspended ?? null;
|
|
647
|
+
visit.pendingCommits--;
|
|
648
|
+
resolveVisit(visit, visit.pendingCommits === 0);
|
|
649
|
+
} else if (visit.lazyBoundary) {
|
|
650
|
+
const boundary = currentFiber(visit.lazyBoundary);
|
|
651
|
+
if (!boundary || boundary.memoizedState == null) {
|
|
652
|
+
visit.lazyBoundary = null;
|
|
653
|
+
if (boundary) {
|
|
654
|
+
visit.resolution = screenResolution(visit.chain);
|
|
655
|
+
resolveVisit(visit, true);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
581
660
|
if (!checkScheduled && containerCount() === 0) {
|
|
582
661
|
checkScheduled = true;
|
|
583
662
|
setTimeout(checkRouteProps, 0);
|