@keepsnappy/react-native 0.0.0-stage → 0.1.0-alpha.11
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 +260 -2
- package/android/build.gradle +35 -0
- package/android/src/main/java/expo/modules/perflab/PerfLabModule.kt +445 -0
- package/android/src/main/java/expo/modules/perflab/PerfLabSampler.kt +515 -0
- package/app.plugin.js +105 -0
- package/expo-module.config.json +9 -0
- package/ios/PerfLab.podspec +29 -0
- package/ios/PerfLabHermes.h +18 -0
- package/ios/PerfLabHermes.mm +64 -0
- package/ios/PerfLabModule.swift +371 -0
- package/ios/PerfLabSampler.swift +364 -0
- package/js/babel-transformer.js +71 -0
- package/js/hook.polyfill.js +333 -0
- package/js/renderer-switch.js +16 -0
- package/js/sdk/activation.ts +375 -0
- package/js/sdk/active.ts +62 -0
- package/js/sdk/artifacts.ts +632 -0
- package/js/sdk/boot.ts +21 -0
- package/js/sdk/causes.ts +932 -0
- package/js/sdk/core.ts +2403 -0
- package/js/sdk/cpuprof.ts +133 -0
- package/js/sdk/dormant.ts +77 -0
- package/js/sdk/env.d.ts +2 -0
- package/js/sdk/fiber.ts +591 -0
- package/js/sdk/interactions.ts +531 -0
- package/js/sdk/links.ts +201 -0
- package/js/sdk/nav.ts +383 -0
- package/js/sdk/net-install.ts +49 -0
- package/js/sdk/net.ts +684 -0
- package/js/sdk/profile-export.ts +257 -0
- package/js/sdk/profile.ts +539 -0
- package/js/sdk/push-install.ts +158 -0
- package/js/sdk/push.ts +819 -0
- package/js/sdk/retain-floor.ts +11 -0
- package/js/sdk/sampler.ts +236 -0
- package/js/sdk/screens.ts +586 -0
- package/js/sdk/stream.ts +125 -0
- package/js/zustand-vanilla.js +31 -0
- package/metro.js +238 -0
- package/package.json +26 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The oldest commit a later window can still need (REQ-MEAS-28, bounded by events): no visit starts before the current
|
|
3
|
+
* one opened; until the launch window opens (and before the first flush or trip to the background) the launch window can
|
|
4
|
+
* still start at the app's start; otherwise the newest action.
|
|
5
|
+
*/
|
|
6
|
+
export const retainFloorOf = (
|
|
7
|
+
openedAt: number | null,
|
|
8
|
+
launchPending: boolean,
|
|
9
|
+
launchT: number | null,
|
|
10
|
+
newestActionT: number,
|
|
11
|
+
): number => openedAt ?? (launchPending && launchT !== null ? launchT : newestActionT);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Native resource sampler: settings parsing and per-window aggregation.
|
|
2
|
+
// Pure (no React Native imports) so it runs under `bun test`; core.ts wires it to the native module.
|
|
3
|
+
|
|
4
|
+
export type SamplerMode = 'auto' | 'manual' | 'off';
|
|
5
|
+
|
|
6
|
+
export interface SamplerSettings {
|
|
7
|
+
sampler: SamplerMode;
|
|
8
|
+
samplerMs: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// One record as the native module returns and logs it. Per-interval fields cover the time since the
|
|
12
|
+
// previous sample of any mode; *Total / *Ms fields are cumulative, so any two samples can be differenced.
|
|
13
|
+
export interface SamplerRecord {
|
|
14
|
+
v: number;
|
|
15
|
+
seq: number;
|
|
16
|
+
reason: string;
|
|
17
|
+
t: number;
|
|
18
|
+
wallMs: number;
|
|
19
|
+
dtMs?: number;
|
|
20
|
+
collectMs: number;
|
|
21
|
+
thermal?: number;
|
|
22
|
+
mem: { footprintMb?: number; residentMb?: number };
|
|
23
|
+
cpu: {
|
|
24
|
+
processMs?: number;
|
|
25
|
+
mainMs?: number;
|
|
26
|
+
jsMs?: number;
|
|
27
|
+
jsThread?: string;
|
|
28
|
+
processCores?: number;
|
|
29
|
+
mainCores?: number;
|
|
30
|
+
jsCores?: number;
|
|
31
|
+
};
|
|
32
|
+
frames: {
|
|
33
|
+
presentedTotal: number;
|
|
34
|
+
missedTotal: number;
|
|
35
|
+
presented?: number;
|
|
36
|
+
missed?: number;
|
|
37
|
+
longestGapMs?: number;
|
|
38
|
+
budgetMs?: number;
|
|
39
|
+
// iOS: UIScreen.maximumFramesPerSecond (120 on ProMotion).
|
|
40
|
+
maxHz?: number;
|
|
41
|
+
// Android FrameMetrics (frames the activity window drew, and those past their deadline); evidence only. Absent on iOS
|
|
42
|
+
// and on SDKs before alpha.10. A frame is counted at the first record taken after it ended. `longestMs` is the longest
|
|
43
|
+
// frame since the previous record, absent when none was drawn. `droppedTotal`: frames the framework dropped before
|
|
44
|
+
// reporting them (not in `drawnTotal`). `windowCovered`: another window (RN Modal, popup, system dialog) held focus
|
|
45
|
+
// during the interval, so its counts are unknown (not zero); SurfaceView/TextureView content is never in them.
|
|
46
|
+
drawnTotal?: number;
|
|
47
|
+
jankTotal?: number;
|
|
48
|
+
droppedTotal?: number;
|
|
49
|
+
drawn?: number;
|
|
50
|
+
jank?: number;
|
|
51
|
+
longestMs?: number;
|
|
52
|
+
windowCovered?: boolean;
|
|
53
|
+
source?: 'framemetrics';
|
|
54
|
+
// 'deadline': API 31+ FrameMetrics.DEADLINE. 'display-rate': API below 31, the current display frame interval.
|
|
55
|
+
jankBasis?: 'deadline' | 'display-rate';
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WindowSampler {
|
|
60
|
+
samples: number;
|
|
61
|
+
spanMs: number;
|
|
62
|
+
leadMs: number;
|
|
63
|
+
lagMs: number;
|
|
64
|
+
footprintStartMb: number | null;
|
|
65
|
+
footprintEndMb: number | null;
|
|
66
|
+
footprintDeltaMb: number | null;
|
|
67
|
+
footprintMaxMb: number | null;
|
|
68
|
+
cpuCores: number | null;
|
|
69
|
+
mainCores: number | null;
|
|
70
|
+
jsCores: number | null;
|
|
71
|
+
framesPresented: number;
|
|
72
|
+
framesMissed: number;
|
|
73
|
+
longestGapMs: number | null;
|
|
74
|
+
framesDrawn: number | null;
|
|
75
|
+
framesJank: number | null;
|
|
76
|
+
longestFrameMs: number | null;
|
|
77
|
+
// True: another window held focus inside the window, so framesDrawn, framesJank and longestFrameMs are null (unknown).
|
|
78
|
+
framesCovered: boolean | null;
|
|
79
|
+
framesJankBasis: 'deadline' | 'display-rate' | null;
|
|
80
|
+
collectMsMax: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const SAMPLER_MS_MIN = 250;
|
|
84
|
+
export const SAMPLER_MS_MAX = 60_000;
|
|
85
|
+
// Background interval between native samples. 4 s: Arek 2026-10-16 (followups, RULING ~09:00); windows get their own
|
|
86
|
+
// samples at open and close, so the background ones only serve peak memory in long windows and the heat trend.
|
|
87
|
+
// The native samplers keep the same number as their unconfigured fallback (PerfLabSampler.swift / .kt).
|
|
88
|
+
export const SAMPLER_MS_DEFAULT = 4000;
|
|
89
|
+
|
|
90
|
+
export const parseSamplerMode = (
|
|
91
|
+
value: string | undefined,
|
|
92
|
+
): SamplerMode | undefined =>
|
|
93
|
+
value === 'auto' || value === 'manual' || value === 'off' ? value : undefined;
|
|
94
|
+
|
|
95
|
+
export const parseSamplerMs = (value: string | undefined): number | undefined => {
|
|
96
|
+
if (value === undefined || value === '') {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const parsed = Number(value);
|
|
100
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return Math.min(SAMPLER_MS_MAX, Math.max(SAMPLER_MS_MIN, Math.round(parsed)));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface SamplerEnv {
|
|
107
|
+
auto: boolean;
|
|
108
|
+
sampler?: string;
|
|
109
|
+
samplerMs?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Off unless enabled: the build env names the mode, measure-only (AUTO) builds default to auto.
|
|
113
|
+
export const samplerDefaults = (env: SamplerEnv): SamplerSettings => ({
|
|
114
|
+
sampler: parseSamplerMode(env.sampler) ?? (env.auto ? 'auto' : 'off'),
|
|
115
|
+
samplerMs: parseSamplerMs(env.samplerMs) ?? SAMPLER_MS_DEFAULT,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const applySamplerParams = (
|
|
119
|
+
current: SamplerSettings,
|
|
120
|
+
params: Record<string, string>,
|
|
121
|
+
): SamplerSettings => ({
|
|
122
|
+
sampler: parseSamplerMode(params.sampler) ?? current.sampler,
|
|
123
|
+
samplerMs: parseSamplerMs(params.samplerMs) ?? current.samplerMs,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Native clock and performance.now() are the same clock on both platforms today; the offset
|
|
127
|
+
// is measured anyway so a mismatch shows up as a number instead of misaligned windows.
|
|
128
|
+
export const estimateOffset = (before: number, native: number, after: number) =>
|
|
129
|
+
(before + after) / 2 - native;
|
|
130
|
+
|
|
131
|
+
// How far past t0 a sample can be and still be the start bound of a window that has no open sample of its own, and how
|
|
132
|
+
// late after its t0 a window can start and still take one. It gates only screen windows (a screen window opened by a
|
|
133
|
+
// press is detected after the new screen rendered and takes the press's open sample, taken the touch delivery delay
|
|
134
|
+
// past the press's t0) and the reuse of a close sample for a restarted key. Press windows do not depend on it: they
|
|
135
|
+
// take the previous window's close sample by contiguity. Evidence for 50: idle lab event timing of 4.9-19.1 ms
|
|
136
|
+
// (packages/derive/test/fixtures/p37-*.log); no loaded-device data. Measure the touchstart delivery delay p99 per
|
|
137
|
+
// device from farm data (docs/log/followups.md).
|
|
138
|
+
export const OPEN_SAMPLE_TOLERANCE_MS = 50;
|
|
139
|
+
// A sample at or after t1 (the close sample) can also land a hair past it.
|
|
140
|
+
const EDGE_TOLERANCE_MS = 1;
|
|
141
|
+
|
|
142
|
+
const delta = (a: number | undefined, b: number | undefined) =>
|
|
143
|
+
a === undefined || b === undefined ? null : b - a;
|
|
144
|
+
|
|
145
|
+
const rate = (a: number | undefined, b: number | undefined, ms: number) => {
|
|
146
|
+
const d = delta(a, b);
|
|
147
|
+
return d === null || d < 0 || ms <= 0 ? null : d / ms;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Aggregate of the samples that bracket [t0, t1] (native clock ms): the window's own open sample when it has one, else
|
|
152
|
+
* the last sample at or before t0 plus OPEN_SAMPLE_TOLERANCE_MS (a screen window's bound is the press's open sample,
|
|
153
|
+
* taken after the press's t0 by the touch delivery delay), and the first
|
|
154
|
+
* sample at/after t1 (the close sample). `leadMs` / `lagMs` say how far the bracket reaches outside the window;
|
|
155
|
+
* `leadMs` is slightly negative when the open sample is the touch delivery delay past t0.
|
|
156
|
+
*/
|
|
157
|
+
export const windowAggregate = (
|
|
158
|
+
records: readonly SamplerRecord[],
|
|
159
|
+
t0: number,
|
|
160
|
+
t1: number,
|
|
161
|
+
open?: SamplerRecord,
|
|
162
|
+
): WindowSampler | null => {
|
|
163
|
+
const sorted = [...records].sort((x, y) => x.t - y.t);
|
|
164
|
+
let a: SamplerRecord | undefined = open && open.t < t1 ? open : undefined;
|
|
165
|
+
if (!a) {
|
|
166
|
+
for (const r of sorted) {
|
|
167
|
+
if (r.t <= t0 + OPEN_SAMPLE_TOLERANCE_MS && r.t <= t1) {
|
|
168
|
+
a = r;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const b = sorted.find(r => r.t >= t1 - EDGE_TOLERANCE_MS && (a === undefined || r.t > a.t));
|
|
173
|
+
if (!a || !b) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const first = a;
|
|
177
|
+
const inside = sorted.filter(r => r.t > first.t && r.t <= b.t);
|
|
178
|
+
const span = b.t - a.t;
|
|
179
|
+
const sameJsThread = a.cpu.jsThread === b.cpu.jsThread;
|
|
180
|
+
const footprints = [a, ...inside]
|
|
181
|
+
.map(r => r.mem.footprintMb)
|
|
182
|
+
.filter((x): x is number => x !== undefined);
|
|
183
|
+
const gaps = inside
|
|
184
|
+
.map(r => r.frames.longestGapMs)
|
|
185
|
+
.filter((x): x is number => x !== undefined);
|
|
186
|
+
const frameMs = inside
|
|
187
|
+
.map(r => r.frames.longestMs)
|
|
188
|
+
.filter((x): x is number => x !== undefined);
|
|
189
|
+
const covered = [...inside].some(r => r.frames.windowCovered === true);
|
|
190
|
+
const counted = !covered && a.frames.drawnTotal !== undefined && b.frames.drawnTotal !== undefined;
|
|
191
|
+
return {
|
|
192
|
+
samples: inside.length + 1,
|
|
193
|
+
spanMs: span,
|
|
194
|
+
leadMs: t0 - a.t,
|
|
195
|
+
lagMs: b.t - t1,
|
|
196
|
+
footprintStartMb: a.mem.footprintMb ?? null,
|
|
197
|
+
footprintEndMb: b.mem.footprintMb ?? null,
|
|
198
|
+
footprintDeltaMb: delta(a.mem.footprintMb, b.mem.footprintMb),
|
|
199
|
+
footprintMaxMb: footprints.length ? Math.max(...footprints) : null,
|
|
200
|
+
cpuCores: rate(a.cpu.processMs, b.cpu.processMs, span),
|
|
201
|
+
mainCores: rate(a.cpu.mainMs, b.cpu.mainMs, span),
|
|
202
|
+
jsCores: sameJsThread ? rate(a.cpu.jsMs, b.cpu.jsMs, span) : null,
|
|
203
|
+
framesPresented: Math.max(0, b.frames.presentedTotal - a.frames.presentedTotal),
|
|
204
|
+
framesMissed: Math.max(0, b.frames.missedTotal - a.frames.missedTotal),
|
|
205
|
+
longestGapMs: gaps.length ? Math.max(...gaps) : null,
|
|
206
|
+
framesDrawn: counted ? Math.max(0, b.frames.drawnTotal! - a.frames.drawnTotal!) : null,
|
|
207
|
+
framesJank:
|
|
208
|
+
counted && a.frames.jankTotal !== undefined && b.frames.jankTotal !== undefined
|
|
209
|
+
? Math.max(0, b.frames.jankTotal - a.frames.jankTotal)
|
|
210
|
+
: null,
|
|
211
|
+
longestFrameMs: counted && frameMs.length ? Math.max(...frameMs) : null,
|
|
212
|
+
framesCovered: b.frames.windowCovered === undefined ? null : covered,
|
|
213
|
+
framesJankBasis: b.frames.jankBasis ?? null,
|
|
214
|
+
collectMsMax: Math.max(...[a, ...inside].map(r => r.collectMs)),
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// The device frame budget from a sampler record: the display's top rate when
|
|
219
|
+
// the record has it (iOS lowers the current rate on idle ProMotion screens),
|
|
220
|
+
// else the current frame interval (Android: 1000 / refresh rate). Null when
|
|
221
|
+
// the frame counter has not run.
|
|
222
|
+
export const frameBudgetFromRecord = (
|
|
223
|
+
record: Pick<SamplerRecord, 'frames'> | null | undefined,
|
|
224
|
+
): { ms: number; hz: number } | null => {
|
|
225
|
+
const frames = record?.frames;
|
|
226
|
+
if (!frames) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (typeof frames.maxHz === 'number' && frames.maxHz > 1) {
|
|
230
|
+
return { ms: Math.round(10000 / frames.maxHz) / 10, hz: frames.maxHz };
|
|
231
|
+
}
|
|
232
|
+
if (typeof frames.budgetMs === 'number' && frames.budgetMs > 1) {
|
|
233
|
+
return { ms: Math.round(frames.budgetMs * 10) / 10, hz: Math.round(1000 / frames.budgetMs) };
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
};
|