@keepsnappy/react-native 0.1.0-alpha.15 → 0.1.0-alpha.17

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/js/sdk/core.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // keepsnappy core: settings, windows, the commit walk, observers, faults, logs.
2
2
  // Loaded by boot.ts, which Metro runs before the app's entry module.
3
3
 
4
+ import { setPlaceLog } from './place';
5
+
4
6
  import {
5
7
  type ActivationStore,
6
8
  type Decision,
@@ -12,6 +14,8 @@ import {
12
14
  } from './activation';
13
15
  import {
14
16
  componentName,
17
+ clientKeyOf,
18
+ OUTSIDE_APP,
15
19
  type Fiber,
16
20
  type FiberRoot,
17
21
  isComponentTag,
@@ -66,6 +70,7 @@ import {
66
70
  type ProfiledFiber,
67
71
  type ProfiledRoot,
68
72
  recordFiber,
73
+ recordRolledUp,
69
74
  selfMs,
70
75
  summarizeWindowProfile,
71
76
  totalMs,
@@ -364,6 +369,8 @@ export const perfLog = (message: string, sink: LogSink = settings.log) => {
364
369
  }
365
370
  }
366
371
  };
372
+ setPlaceLog(perfLog);
373
+
367
374
 
368
375
  // Requests started per window (fetch/XHR hooks in net-install.ts feed it).
369
376
  export const net = createNetCounter({ now, onStart: noteRequestSent });
@@ -466,8 +473,10 @@ interface OpenSampleState {
466
473
  // - so does a window that starts within OPEN_SAMPLE_TOLERANCE_MS of the close sample (restarted key);
467
474
  // - a window detected long after its t0 (a screen window opened by a press, seen after the new screen rendered) gets
468
475
  // none: its bound is the press's open sample;
469
- // - else the sampler is asked to sample on its own thread.
470
- const openSampleFor = (id: number, t0: number): OpenSampleState => {
476
+ // - else the sampler is asked to sample on its own thread. A press always is, however late its touch reached JS: with
477
+ // no window before it (after a GC command, the background or the flush) the sample before the touch can be seconds
478
+ // old, so its own sample, a negative lead past t0, is the closer bound.
479
+ const openSampleFor = (id: number, t0: number, press: boolean): OpenSampleState => {
471
480
  const none = { openRecord: null, openPending: null };
472
481
  if (samplerState.mode === 'off') {
473
482
  return none;
@@ -480,7 +489,7 @@ const openSampleFor = (id: number, t0: number): OpenSampleState => {
480
489
  ) {
481
490
  return { openRecord: lastClose.record, openPending: null };
482
491
  }
483
- if (now() - samplerState.offsetMs - t0Native > OPEN_SAMPLE_TOLERANCE_MS) {
492
+ if (!press && now() - samplerState.offsetMs - t0Native > OPEN_SAMPLE_TOLERANCE_MS) {
484
493
  return none;
485
494
  }
486
495
  const reason = `window-start:${id}`;
@@ -611,6 +620,8 @@ const diffStats = (
611
620
  export type WindowKind = 'marker' | 'press' | 'screen' | 'background';
612
621
 
613
622
  interface MarkerWindow {
623
+ legacyName: string | null;
624
+ beforeEnd: (() => void) | null;
614
625
  id: number;
615
626
  key: string;
616
627
  name: string;
@@ -839,9 +850,12 @@ export const lastCommitAt = (key: string) =>
839
850
  export const isWindowOpen = (key?: string) =>
840
851
  key ? openWindows.has(key) : openWindows.size > 0;
841
852
 
842
- const traceName = (name: string) => `keepsnappy:${name}`;
853
+ const traceName = (name: string) => `keepsnappy:${name}`.slice(0, 120);
854
+ const fileNamePart = (name: string) => name.replace(/[^\w.:+-]/g, '_').slice(0, 64);
843
855
 
844
856
  interface StartOptions {
857
+ legacyName?: string | null;
858
+ beforeEnd?: () => void;
845
859
  key?: string;
846
860
  kind?: WindowKind;
847
861
  // Backdates t0 (e.g. to the hardware touch time). performance.now() timebase.
@@ -879,6 +893,8 @@ const backgroundWindow = (source: string, t: number): MarkerWindow => {
879
893
  key: name,
880
894
  name,
881
895
  kind: 'background',
896
+ legacyName: null,
897
+ beforeEnd: null,
882
898
  trace: traceName(name),
883
899
  t0: t,
884
900
  t1: null,
@@ -924,7 +940,14 @@ const routeToBackground = (record: CommitRecord) => {
924
940
 
925
941
  // Segments written and not yet listed for upload (listed at the next flush).
926
942
  const pendingStreamFiles: WrittenFile[] = [];
927
- // A segment is cut at this many commits even between flushes, so a long stretch outside a run stays bounded.
943
+ // What the run lost since the last flush, which reports it. Commit-stream segments that failed to write are missing data
944
+ // (the backend splits work by cause from them). Kept React exports dropped at the run cap or not written, and stream fibers
945
+ // past a segment's cap, leave every measured value complete: counted notes only.
946
+ const runLosses = { stream: 0, exportsCapped: 0, exportsUnwritten: 0, streamFibersDropped: 0 };
947
+ export const setStreamFiberCapForTest = (fibers: number) => commitStream.setMaxFibers(fibers);
948
+ // A segment is cut at this many commits even between flushes, so a long stretch outside a run stays bounded. In a run
949
+ // the cut waits while a window is open (no file I/O inside a measured window, REQ-MEAS-23): every window ends at the
950
+ // flush, which cuts it, and a segment past its fiber cap keeps only the commits' facts.
928
951
  const MAX_SEGMENT_COMMITS = 5000;
929
952
 
930
953
  const cutStream = () => {
@@ -940,7 +963,10 @@ const cutStream = () => {
940
963
  }
941
964
  if (path) {
942
965
  pendingStreamFiles.push({ name: segment.name, windows: [], bytes: segment.json.length });
966
+ } else if (inRun()) {
967
+ runLosses.stream++;
943
968
  }
969
+ if (inRun()) runLosses.streamFibersDropped += segment.fibersDropped;
944
970
  perfLog(`stream ${JSON.stringify({ name: segment.name, commits: segment.commits, bytes: segment.json.length, fibersDropped: segment.fibersDropped, written: path !== null })}`);
945
971
  };
946
972
 
@@ -984,12 +1010,14 @@ const start = (name: string, options: StartOptions = {}) => {
984
1010
  const statsStart = readHermesStats();
985
1011
  const id = nextWindowId++;
986
1012
  const startT = options.at ?? now();
987
- const openSample = openSampleFor(id, startT);
1013
+ const openSample = openSampleFor(id, startT, options.kind === 'press');
988
1014
  const window: MarkerWindow = {
989
1015
  id,
990
1016
  key,
991
1017
  name,
992
1018
  kind: options.kind ?? 'marker',
1019
+ legacyName: options.legacyName ?? null,
1020
+ beforeEnd: options.beforeEnd ?? null,
993
1021
  trace: traceName(name),
994
1022
  t0: startT,
995
1023
  t1: null,
@@ -1043,6 +1071,7 @@ const end = (key: string, reason = 'end', at?: number) => {
1043
1071
  if (!window) {
1044
1072
  return;
1045
1073
  }
1074
+ window.beforeEnd?.();
1046
1075
  window.t1 = at ?? now();
1047
1076
  window.endReason = reason;
1048
1077
  openWindows.delete(key);
@@ -1086,8 +1115,9 @@ const endAll = (reason: string) => {
1086
1115
  }
1087
1116
  };
1088
1117
 
1089
- const rename = (key: string, name: string) => {
1118
+ const rename = (key: string, name: string, legacyName?: string | null) => {
1090
1119
  const window = openWindows.get(key);
1120
+ if (window && legacyName !== undefined) window.legacyName = legacyName;
1091
1121
  if (window && window.name !== name) {
1092
1122
  perfLog(`PERF_MARK ${window.name} renamed=${name} id=${window.id}`);
1093
1123
  window.name = name;
@@ -1135,30 +1165,43 @@ const walkCommit = (
1135
1165
  causes: CommitCauses,
1136
1166
  ) => {
1137
1167
  const stack: Fiber[] = [root.current];
1168
+ const owners: string[] = [OUTSIDE_APP];
1169
+ const parents: (string | null)[] = [null];
1138
1170
  const renders: Record<string, number> = {};
1139
1171
  let mounts = 0;
1140
1172
  let visited = 0;
1141
1173
  const ancestors = exported ? createAncestorPaths() : null;
1142
1174
  while (stack.length > 0) {
1143
1175
  const fiber = stack.pop() as Fiber;
1176
+ const inherited = owners.pop() as string;
1177
+ const inheritedParent = parents.pop() as string | null;
1178
+ let owner = inherited, parentOfOwner = inheritedParent;
1144
1179
  visited++;
1145
1180
  const mounted = fiber.alternate === null;
1146
1181
  if (mounted) {
1147
1182
  noteMounted(fiber);
1148
1183
  }
1149
1184
  noteHostFiber(fiber, causes);
1150
- if (isComponentTag(fiber.tag) && (fiber.flags & PERFORMED_WORK) !== 0) {
1185
+ const component = isComponentTag(fiber.tag);
1186
+ const own = component ? clientKeyOf(fiber) : null;
1187
+ if (own !== null) {
1188
+ owner = own;
1189
+ parentOfOwner = inherited === OUTSIDE_APP ? null : inherited;
1190
+ }
1191
+ if (component && (fiber.flags & PERFORMED_WORK) !== 0) {
1151
1192
  const name = componentName(fiber);
1152
1193
  renders[name] = (renders[name] ?? 0) + 1;
1153
1194
  if (mounted) {
1154
1195
  mounts++;
1155
1196
  }
1156
- if (profile || exported) {
1197
+ if ((profile && own !== null) || exported) {
1157
1198
  const cause = updateCause(fiber as ProfiledFiber);
1158
- if (profile) {
1159
- recordFiber(profile, name, fiber as ProfiledFiber, cause, fiberId(fiber));
1199
+ if (profile && own !== null) {
1200
+ recordFiber(profile, own, fiber as ProfiledFiber, cause, fiberId(fiber), parentOfOwner);
1160
1201
  }
1161
1202
  if (exported) {
1203
+ // Profiled exports assign the rendered id before fresh ancestor ids.
1204
+ if (profile && own === null) fiberId(fiber);
1162
1205
  exported.fibers.push(
1163
1206
  exportFiber(
1164
1207
  fiber,
@@ -1167,10 +1210,14 @@ const walkCommit = (
1167
1210
  totalMs(fiber as ProfiledFiber),
1168
1211
  selfMs(fiber as ProfiledFiber),
1169
1212
  (ancestors as (f: Fiber) => number[])(fiber),
1213
+ owner,
1170
1214
  ),
1171
1215
  );
1172
1216
  }
1173
1217
  }
1218
+ if (profile && own === null) {
1219
+ recordRolledUp(profile, inherited, inheritedParent, name, fiber as ProfiledFiber);
1220
+ }
1174
1221
  }
1175
1222
  // Children not cloned in this render are the previous commit's fibers and
1176
1223
  // carry stale flags, so a bailed-out subtree is skipped entirely.
@@ -1180,6 +1227,8 @@ const walkCommit = (
1180
1227
  }
1181
1228
  for (let child = fiber.child; child !== null; child = child.sibling) {
1182
1229
  stack.push(child);
1230
+ owners.push(owner);
1231
+ parents.push(parentOfOwner);
1183
1232
  }
1184
1233
  }
1185
1234
  return { renders, mounts, visited };
@@ -1217,7 +1266,7 @@ const onCommitFiberRoot = (rendererId: number, root: FiberRoot) => {
1217
1266
  // The figure `profile.renderMs` reads; null when the renderer does not time commits.
1218
1267
  renderMs = isProfilingFiber(profiled.current) ? totalMs(profiled.current) : null;
1219
1268
  if (settings.profile) {
1220
- profile = createCommitProfile(profiled, seq);
1269
+ profile = createCommitProfile(profiled, seq, t0);
1221
1270
  }
1222
1271
  // Only commits a window can take are exported (open now, or backfilled
1223
1272
  // into a window opened later with a backdated start).
@@ -1264,7 +1313,7 @@ const onCommitFiberRoot = (rendererId: number, root: FiberRoot) => {
1264
1313
  // Work no open window judges is the background of its source: pushes, and loops of earlier actions or of the launch.
1265
1314
  record.background = record.judged ? null : backgroundSource(causes);
1266
1315
  commitStream.noteCommit({ seq, t: t0, renderMs, causes, fibers: exported?.fibers ?? null, updaters: profile?.updaters ?? null });
1267
- if (commitStream.size() >= MAX_SEGMENT_COMMITS) {
1316
+ if (commitStream.size() >= MAX_SEGMENT_COMMITS && !(inRun() && openWindows.size > 0)) {
1268
1317
  cutStream();
1269
1318
  }
1270
1319
  recordCommit(record);
@@ -1477,7 +1526,7 @@ const netForWindow = (window: MarkerWindow): NetWindow | null =>
1477
1526
  window.net ? net.windowClose(window.id) : null;
1478
1527
 
1479
1528
  const reactExportName = (window: MarkerWindow) =>
1480
- `react-${PROCESS_ID}-${window.id}-${window.name.replace(/[^\w.:+-]/g, '_')}.json`;
1529
+ `react-${PROCESS_ID}-${window.id}-${fileNamePart(window.name)}.json`;
1481
1530
 
1482
1531
  interface PendingExport {
1483
1532
  name: string;
@@ -1488,7 +1537,12 @@ interface PendingExport {
1488
1537
  }
1489
1538
 
1490
1539
  // React exports held until flush (inside a run), capped like a window's.
1491
- const MAX_PENDING_EXPORT_FIBERS = 400000;
1540
+ let maxPendingExportFibers = 400000;
1541
+ export const setPendingExportCapForTest = (fibers: number) => {
1542
+ const previous = maxPendingExportFibers;
1543
+ maxPendingExportFibers = fibers;
1544
+ return previous;
1545
+ };
1492
1546
  const pendingExports: PendingExport[] = [];
1493
1547
  let pendingExportFibers = 0;
1494
1548
 
@@ -1509,7 +1563,8 @@ const writeReactExport = (window: MarkerWindow) => {
1509
1563
  truncated: window.exportTruncated,
1510
1564
  };
1511
1565
  if (inRun()) {
1512
- if (pendingExportFibers + window.exportFibers > MAX_PENDING_EXPORT_FIBERS) {
1566
+ if (pendingExportFibers + window.exportFibers > maxPendingExportFibers) {
1567
+ if (settings.diag || budgetBreaks.has(window.id)) runLosses.exportsCapped++;
1513
1568
  return { ...base, deferred: true, dropped: true };
1514
1569
  }
1515
1570
  pendingExports.push({
@@ -1586,6 +1641,7 @@ const writeDeferredFiles = (): ArtifactEntry[] => {
1586
1641
  } catch (error) {
1587
1642
  result = { error: String(error) };
1588
1643
  }
1644
+ if (!result.path && (settings.diag || budgetBreaks.has(pending.window))) runLosses.exportsUnwritten++;
1589
1645
  perfLog(
1590
1646
  `rexport ${JSON.stringify({ name: pending.name, commits: pending.commits.length, fibers: pending.fibers, ...result })}`,
1591
1647
  );
@@ -1720,7 +1776,7 @@ const finalizeWindow = (window: MarkerWindow) => {
1720
1776
  // Sampler first: its records carry the display rate the frame budget needs.
1721
1777
  const sampler = samplerForWindow(window);
1722
1778
  const profile = window.profile
1723
- ? summarizeWindowProfile(window.profile, { budget: frameBudget })
1779
+ ? summarizeWindowProfile(window.profile, { budget: frameBudget, t0: window.t0 })
1724
1780
  : null;
1725
1781
  const perf = settings.po ? summarizeEntries(window) : null;
1726
1782
  noteBudgetBreak(
@@ -1734,6 +1790,7 @@ const finalizeWindow = (window: MarkerWindow) => {
1734
1790
  windowModel: WINDOW_MODEL,
1735
1791
  id: window.id,
1736
1792
  name: window.name,
1793
+ ...(window.legacyName !== null && window.legacyName !== window.name ? { legacyName: window.legacyName } : {}),
1737
1794
  kind: window.kind,
1738
1795
  t0: window.t0,
1739
1796
  t1: window.t1,
@@ -1799,7 +1856,7 @@ const writeWindowRecord = (window: MarkerWindow, summary: Record<string, unknown
1799
1856
  windowListener?.(`${Date.now()} KEEPSNAPPY window ${json}`);
1800
1857
  if (settings.file) {
1801
1858
  const path = getNative()?.writeText(
1802
- `window-${window.id}-${window.name.replace(/[^\w.:+-]/g, '_')}.json`,
1859
+ `window-${window.id}-${fileNamePart(window.name)}.json`,
1803
1860
  json,
1804
1861
  );
1805
1862
  perfLog(`window file id=${window.id} path=${path ?? 'unavailable'}`);
@@ -1839,13 +1896,14 @@ const externalSummary = (window: MarkerWindow, timed: boolean) => {
1839
1896
 
1840
1897
  const finalizeBackground = (window: MarkerWindow) => {
1841
1898
  const profile = window.profile
1842
- ? summarizeWindowProfile(window.profile, { budget: frameBudget })
1899
+ ? summarizeWindowProfile(window.profile, { budget: frameBudget, t0: window.t0 })
1843
1900
  : null;
1844
1901
  writeWindowRecord(window, {
1845
1902
  v: 1,
1846
1903
  windowModel: WINDOW_MODEL,
1847
1904
  id: window.id,
1848
1905
  name: window.name,
1906
+ ...(window.legacyName !== null && window.legacyName !== window.name ? { legacyName: window.legacyName } : {}),
1849
1907
  kind: window.kind,
1850
1908
  t0: window.t0,
1851
1909
  t1: window.t1,
@@ -1903,11 +1961,13 @@ const leakJsHeap = (mb: number) => {
1903
1961
  leakState.retainedMB += mb;
1904
1962
  };
1905
1963
 
1906
- export const isFaultTarget = (windowName: string) =>
1907
- settings.faultTarget
1964
+ export const isFaultTarget = (press: { name: string; legacyName: string; target: { testID: string | null; accessibilityLabel: string | null } }) => {
1965
+ const names = [press.name, press.legacyName, press.target.testID, press.target.accessibilityLabel];
1966
+ return settings.faultTarget
1908
1967
  .split(',')
1909
1968
  .map(part => part.trim())
1910
- .some(part => part !== '' && windowName.includes(part));
1969
+ .some(part => part !== '' && names.some(name => name?.includes(part)));
1970
+ };
1911
1971
 
1912
1972
  export const runOpenFaults = (where: string) => {
1913
1973
  const faults = settings.faults;
@@ -2086,6 +2146,16 @@ const flushOnce = async (ack: string | undefined) => {
2086
2146
  } catch (error) {
2087
2147
  perfLog(`artifacts failed ${String(error)}`);
2088
2148
  }
2149
+ if (runToken !== null) {
2150
+ if (runLosses.stream > 0) {
2151
+ if (status === 'ok') status = 'error';
2152
+ detail += ` lost stream=${runLosses.stream}`;
2153
+ }
2154
+ if (runLosses.exportsCapped > 0) detail += ` exports capped=${runLosses.exportsCapped}`;
2155
+ if (runLosses.exportsUnwritten > 0) detail += ` exports unwritten=${runLosses.exportsUnwritten}`;
2156
+ if (runLosses.streamFibersDropped > 0) detail += ` stream fibersDropped=${runLosses.streamFibersDropped}`;
2157
+ }
2158
+ Object.assign(runLosses, { stream: 0, exportsCapped: 0, exportsUnwritten: 0, streamFibersDropped: 0 });
2089
2159
  try {
2090
2160
  await sendEarlierArtifacts(deadline);
2091
2161
  } catch (error) {
@@ -0,0 +1,10 @@
1
+ // The model a device reports to the API: what the OS calls the hardware, never the marketing name. expo-device's `modelId` is an
2
+ // iPhone's machine id ("iPhone17,1") and null on Android, where `modelName` is already Build.MODEL ("SM-S911B"); `modelName` on iOS
3
+ // is the marketing name, so the id comes first. A simulator keeps its name: there `modelId` is the host's `uname` machine ("arm64")
4
+ // and `modelName` is "Simulator iOS" (an emulator's Build.MODEL carries the word the API reads the device kind from).
5
+ export const deviceModelOf = (device: { isDevice?: boolean | null; modelId?: string | null; modelName?: string | null }): string | null => {
6
+ const modelId = /^(arm64|x86_64|i386)$/i.test(device.modelId ?? '') && device.isDevice === false ? null : device.modelId;
7
+ if (device.isDevice !== false) return modelId || device.modelName || null;
8
+ const model = device.modelName || modelId;
9
+ return model ? /(simulator|emulator|sdk_gphone|android sdk built for)/i.test(model) ? model : `${model} Simulator` : null;
10
+ };