@keepsnappy/react-native 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +35 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/{perflab/PerfLabModule.kt → keepsnappy/KeepSnappyModule.kt} +69 -28
- package/android/src/main/java/expo/modules/{perflab/PerfLabSampler.kt → keepsnappy/KeepSnappySampler.kt} +17 -17
- package/app.plugin.js +7 -6
- package/expo-module.config.json +2 -2
- package/ios/{PerfLab.podspec → KeepSnappy.podspec} +2 -2
- package/ios/{PerfLabHermes.h → KeepSnappyHermes.h} +1 -1
- package/ios/{PerfLabHermes.mm → KeepSnappyHermes.mm} +2 -2
- package/ios/{PerfLabModule.swift → KeepSnappyModule.swift} +65 -26
- package/ios/{PerfLabSampler.swift → KeepSnappySampler.swift} +10 -10
- package/js/babel-transformer.js +8 -4
- package/js/build-version.js +23 -0
- package/js/hook.polyfill.js +11 -11
- package/js/renderer-switch.js +1 -1
- package/js/sdk/activation.ts +15 -15
- package/js/sdk/active.ts +3 -3
- package/js/sdk/artifact-pending.ts +167 -0
- package/js/sdk/artifacts.ts +5 -4
- package/js/sdk/boot.ts +4 -4
- package/js/sdk/causes.ts +2 -2
- package/js/sdk/core.ts +143 -53
- package/js/sdk/cpuprof.ts +1 -1
- package/js/sdk/dormant.ts +7 -7
- package/js/sdk/links.ts +13 -13
- package/js/sdk/net.ts +2 -2
- package/js/sdk/profile-export.ts +7 -7
- package/js/sdk/push-install.ts +3 -3
- package/js/sdk/push.ts +52 -17
- package/js/sdk/sampler.ts +1 -1
- package/js/zustand-vanilla.js +2 -2
- package/metro.js +2 -0
- package/package.json +1 -1
package/js/sdk/core.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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
4
|
import {
|
|
5
5
|
type ActivationStore,
|
|
6
6
|
type Decision,
|
|
7
7
|
MODE,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
KEEPSNAPPY_DEEP_LINK_PATH,
|
|
9
|
+
KEEPSNAPPY_ENABLED,
|
|
10
10
|
parseQuery,
|
|
11
11
|
type PolyfillActivation,
|
|
12
12
|
} from './activation';
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
summarizeArtifacts,
|
|
33
33
|
type WrittenFile,
|
|
34
34
|
} from './artifacts';
|
|
35
|
+
import { createPendingArtifacts, type EarlierArtifacts } from './artifact-pending';
|
|
35
36
|
import {
|
|
36
37
|
backgroundSource,
|
|
37
38
|
type Carrier,
|
|
@@ -90,7 +91,7 @@ import {
|
|
|
90
91
|
windowAggregate,
|
|
91
92
|
} from './sampler';
|
|
92
93
|
|
|
93
|
-
export {
|
|
94
|
+
export { KEEPSNAPPY_DEEP_LINK_PATH, KEEPSNAPPY_ENABLED, parseQuery };
|
|
94
95
|
|
|
95
96
|
type LogSink = 'console' | 'native' | 'both';
|
|
96
97
|
|
|
@@ -126,7 +127,7 @@ interface Settings extends SamplerSettings {
|
|
|
126
127
|
diag: boolean;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
interface
|
|
130
|
+
interface KeepSnappyNative extends Partial<CpuProfileNative> {
|
|
130
131
|
beginAsyncSection(name: string, cookie: number): void;
|
|
131
132
|
endAsyncSection(name: string, cookie: number): void;
|
|
132
133
|
setCounter(name: string, value: number): void;
|
|
@@ -143,13 +144,16 @@ interface PerfLabNative extends Partial<CpuProfileNative> {
|
|
|
143
144
|
samplerRecent(sinceT: number): SamplerRecord[];
|
|
144
145
|
samplerNow(): number;
|
|
145
146
|
flushSignal?(status: string, detail: string): void;
|
|
146
|
-
//
|
|
147
|
+
// The list of profile files not uploaded yet, with run tokens: internal storage, read by a later process.
|
|
148
|
+
artifactsPendingRead?(): string | null;
|
|
149
|
+
artifactsPendingWrite?(content: string): boolean;
|
|
150
|
+
// Command answer (`gc:<n> ok`, `memread:<n> ok`): Android finishes the pending command broadcast with this result; iOS shows it on `keepsnappy.ack`.
|
|
147
151
|
commandDone?(n: number, data: string): void;
|
|
148
152
|
/** Android: whether the broadcast of command n still waits for its answer (native finishes it at a deadline). */
|
|
149
153
|
commandPending?(n: number): boolean;
|
|
150
154
|
ackSignal?(text: string): void;
|
|
151
155
|
commandListen?(on: boolean): void;
|
|
152
|
-
addListener?(event: '
|
|
156
|
+
addListener?(event: 'keepsnappyCommand', listener: (e: { cmd: string; n: number; tNative: number }) => void): { remove(): void };
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
interface RendererInternals {
|
|
@@ -232,7 +236,7 @@ interface PolyfillHook {
|
|
|
232
236
|
attach: (listener: HookListener | null) => void;
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
interface
|
|
239
|
+
interface KeepSnappyGlobal {
|
|
236
240
|
HermesInternal?: HermesInternalApi;
|
|
237
241
|
PerformanceObserver?: PerfObserverCtor & { supportedEntryTypes?: string[] };
|
|
238
242
|
performance?: PerformanceLike;
|
|
@@ -240,12 +244,12 @@ interface PerfLabGlobal {
|
|
|
240
244
|
console: { log: (...args: unknown[]) => void; timeStamp?: unknown };
|
|
241
245
|
expo?: { modules?: Record<string, unknown> };
|
|
242
246
|
__REACT_DEVTOOLS_GLOBAL_HOOK__?: Record<string, unknown>;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
__KEEPSNAPPY_HOOK__?: PolyfillHook;
|
|
248
|
+
__KEEPSNAPPY_ACTIVATION__?: PolyfillActivation;
|
|
249
|
+
__KEEPSNAPPY__?: unknown;
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
const g = globalThis as unknown as
|
|
252
|
+
const g = globalThis as unknown as KeepSnappyGlobal;
|
|
249
253
|
|
|
250
254
|
const NO_FAULTS: Faults = {
|
|
251
255
|
cpu: 0,
|
|
@@ -296,14 +300,14 @@ export const now = () => (g.performance ? g.performance.now() : Date.now());
|
|
|
296
300
|
export const fmt = (ms: number) => ms.toFixed(3);
|
|
297
301
|
const noop = () => undefined;
|
|
298
302
|
|
|
299
|
-
let nativeModule:
|
|
303
|
+
let nativeModule: KeepSnappyNative | null | undefined;
|
|
300
304
|
let nativeMissingLogged = false;
|
|
301
305
|
|
|
302
|
-
const getNative = ():
|
|
306
|
+
const getNative = (): KeepSnappyNative | null => {
|
|
303
307
|
if (nativeModule) {
|
|
304
308
|
return nativeModule;
|
|
305
309
|
}
|
|
306
|
-
const found = g.expo?.modules?.
|
|
310
|
+
const found = g.expo?.modules?.KeepSnappy as KeepSnappyNative | undefined;
|
|
307
311
|
if (found) {
|
|
308
312
|
nativeModule = found;
|
|
309
313
|
return found;
|
|
@@ -311,7 +315,7 @@ const getNative = (): PerfLabNative | null => {
|
|
|
311
315
|
if (!nativeMissingLogged) {
|
|
312
316
|
nativeMissingLogged = true;
|
|
313
317
|
g.console.log(
|
|
314
|
-
'
|
|
318
|
+
'KEEPSNAPPY native module KeepSnappy missing (expo.modules.KeepSnappy)',
|
|
315
319
|
);
|
|
316
320
|
}
|
|
317
321
|
return null;
|
|
@@ -344,10 +348,10 @@ const cpu = createCpuProfiler({
|
|
|
344
348
|
});
|
|
345
349
|
|
|
346
350
|
export const perfLog = (message: string, sink: LogSink = settings.log) => {
|
|
347
|
-
if (!
|
|
351
|
+
if (!KEEPSNAPPY_ENABLED) {
|
|
348
352
|
return;
|
|
349
353
|
}
|
|
350
|
-
const line = `
|
|
354
|
+
const line = `KEEPSNAPPY ${message}`;
|
|
351
355
|
if (sink === 'console' || sink === 'both') {
|
|
352
356
|
g.console.log(line);
|
|
353
357
|
}
|
|
@@ -412,7 +416,7 @@ const nativeSamplerNow = () => {
|
|
|
412
416
|
|
|
413
417
|
// On-demand sample: logged natively like the automatic ones and returned. Null when the sampler is off.
|
|
414
418
|
export const sample = (reason = 'manual'): SamplerRecord | null => {
|
|
415
|
-
if (!
|
|
419
|
+
if (!KEEPSNAPPY_ENABLED || samplerState.mode === 'off') {
|
|
416
420
|
return null;
|
|
417
421
|
}
|
|
418
422
|
try {
|
|
@@ -816,7 +820,7 @@ const clearIdleGc = () => {
|
|
|
816
820
|
|
|
817
821
|
const scheduleIdleGc = () => {
|
|
818
822
|
clearIdleGc();
|
|
819
|
-
if (!
|
|
823
|
+
if (!KEEPSNAPPY_ENABLED || !settings.gcwin || openWindows.size > 0) {
|
|
820
824
|
return;
|
|
821
825
|
}
|
|
822
826
|
idleGcTimer = setTimeout(() => {
|
|
@@ -835,7 +839,7 @@ export const lastCommitAt = (key: string) =>
|
|
|
835
839
|
export const isWindowOpen = (key?: string) =>
|
|
836
840
|
key ? openWindows.has(key) : openWindows.size > 0;
|
|
837
841
|
|
|
838
|
-
const traceName = (name: string) => `
|
|
842
|
+
const traceName = (name: string) => `keepsnappy:${name}`;
|
|
839
843
|
|
|
840
844
|
interface StartOptions {
|
|
841
845
|
key?: string;
|
|
@@ -962,7 +966,7 @@ const endBackground = (reason: string) => {
|
|
|
962
966
|
};
|
|
963
967
|
|
|
964
968
|
const start = (name: string, options: StartOptions = {}) => {
|
|
965
|
-
if (!
|
|
969
|
+
if (!KEEPSNAPPY_ENABLED || !settings.marks) {
|
|
966
970
|
return null;
|
|
967
971
|
}
|
|
968
972
|
const key = options.key ?? name;
|
|
@@ -1091,7 +1095,7 @@ const rename = (key: string, name: string) => {
|
|
|
1091
1095
|
};
|
|
1092
1096
|
|
|
1093
1097
|
const point = (name: string) => {
|
|
1094
|
-
if (!
|
|
1098
|
+
if (!KEEPSNAPPY_ENABLED || !settings.marks) {
|
|
1095
1099
|
return;
|
|
1096
1100
|
}
|
|
1097
1101
|
const t = now();
|
|
@@ -1277,15 +1281,15 @@ const onCommitFiberUnmount = (_rendererId: number, fiber: Fiber) => {
|
|
|
1277
1281
|
};
|
|
1278
1282
|
|
|
1279
1283
|
export const lastRoot = (rendererId = 1): FiberRoot | null =>
|
|
1280
|
-
g.
|
|
1284
|
+
g.__KEEPSNAPPY_HOOK__?.state.lastRootByRenderer[rendererId] ?? null;
|
|
1281
1285
|
|
|
1282
1286
|
// The polyfill (withKeepSnappy) installed the hook before React loaded; attach to
|
|
1283
1287
|
// it. Without the polyfill, fall back to installing it now, as the prototype did.
|
|
1284
1288
|
export const attachHook = () => {
|
|
1285
|
-
if (!
|
|
1289
|
+
if (!KEEPSNAPPY_ENABLED || hookState.installed) {
|
|
1286
1290
|
return;
|
|
1287
1291
|
}
|
|
1288
|
-
const polyfill = g.
|
|
1292
|
+
const polyfill = g.__KEEPSNAPPY_HOOK__;
|
|
1289
1293
|
if (polyfill) {
|
|
1290
1294
|
polyfill.ensure();
|
|
1291
1295
|
polyfill.attach({
|
|
@@ -1310,7 +1314,7 @@ export const attachHook = () => {
|
|
|
1310
1314
|
}
|
|
1311
1315
|
if (g.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
|
|
1312
1316
|
hookState.preexisting = true;
|
|
1313
|
-
g.console.log('
|
|
1317
|
+
g.console.log('KEEPSNAPPY hook already present, not replacing it');
|
|
1314
1318
|
return;
|
|
1315
1319
|
}
|
|
1316
1320
|
let nextRendererId = 1;
|
|
@@ -1327,7 +1331,7 @@ export const attachHook = () => {
|
|
|
1327
1331
|
bundleType: internals.bundleType,
|
|
1328
1332
|
});
|
|
1329
1333
|
g.console.log(
|
|
1330
|
-
`
|
|
1334
|
+
`KEEPSNAPPY hook inject id=${id} renderer=${internals.rendererPackageName} version=${internals.version} bundleType=${internals.bundleType}`,
|
|
1331
1335
|
);
|
|
1332
1336
|
return id;
|
|
1333
1337
|
},
|
|
@@ -1617,6 +1621,17 @@ export const setArtifactHandler = (handler: ArtifactHandler | null) => {
|
|
|
1617
1621
|
// Entries a window interrupted wait for the next flush of this process.
|
|
1618
1622
|
let artifactBacklog: ArtifactEntry[] = [];
|
|
1619
1623
|
|
|
1624
|
+
const pendingArtifacts = createPendingArtifacts(() => {
|
|
1625
|
+
const native = getNative();
|
|
1626
|
+
if (typeof native?.artifactsPendingRead !== 'function' || typeof native.artifactsPendingWrite !== 'function') {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
return {
|
|
1630
|
+
read: () => native.artifactsPendingRead!(),
|
|
1631
|
+
write: content => native.artifactsPendingWrite!(content),
|
|
1632
|
+
};
|
|
1633
|
+
}, perfLog);
|
|
1634
|
+
|
|
1620
1635
|
const writeManifest = (entries: ArtifactEntry[], upload: ArtifactUploadSummary | null) => {
|
|
1621
1636
|
try {
|
|
1622
1637
|
const manifest = buildManifest({
|
|
@@ -1662,11 +1677,39 @@ const sendArtifacts = async (fresh: ArtifactEntry[], deadline: number) => {
|
|
|
1662
1677
|
summary = summarizeArtifacts(entries, 0, 'push not configured');
|
|
1663
1678
|
}
|
|
1664
1679
|
artifactBacklog = entries.filter(entry => entry.state === 'pending' || entry.state === 'failed');
|
|
1680
|
+
pendingArtifacts.note(PROCESS_ID, entries, runToken);
|
|
1665
1681
|
writeManifest(entries, summary);
|
|
1666
1682
|
perfLog(`artifacts ${JSON.stringify(summary)}`);
|
|
1667
1683
|
return summary;
|
|
1668
1684
|
};
|
|
1669
1685
|
|
|
1686
|
+
// After this process's own files: what earlier processes left (a flush that never finished, a kill during the upload), each
|
|
1687
|
+
// under its own run token and process id, within the same deadline.
|
|
1688
|
+
const sendEarlierArtifacts = async (deadline: number) => {
|
|
1689
|
+
if (!artifactHandler) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const groups: EarlierArtifacts[] = pendingArtifacts.earlier(PROCESS_ID);
|
|
1693
|
+
if (groups.length === 0) {
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const tried: EarlierArtifacts[] = [];
|
|
1697
|
+
for (const group of groups) {
|
|
1698
|
+
// What does not fit in this flush waits for the next one, with no try counted.
|
|
1699
|
+
if (Date.now() >= deadline) {
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
tried.push(group);
|
|
1703
|
+
try {
|
|
1704
|
+
const summary = await artifactHandler(group.entries, null, group.processId, deadline);
|
|
1705
|
+
perfLog(`artifacts earlier ${JSON.stringify({ processId: group.processId, ...summary })}`);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
perfLog(`artifacts earlier failed ${String(error)}`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
pendingArtifacts.settleEarlier(tried);
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1670
1713
|
// Declared on every window line so the backend can tell windows of this model from older ones (REQ-VER-34).
|
|
1671
1714
|
// 2: a tap or scroll window runs from its touch to the next touch, the background or the flush; no quiet rule, no `+3s` twin.
|
|
1672
1715
|
// 3: model 2, and a screen window also ends at the next action, the next screen, the background or the flush; no 3 s cap.
|
|
@@ -1753,7 +1796,7 @@ const finalizeWindow = (window: MarkerWindow) => {
|
|
|
1753
1796
|
const writeWindowRecord = (window: MarkerWindow, summary: Record<string, unknown>) => {
|
|
1754
1797
|
const json = JSON.stringify(summary);
|
|
1755
1798
|
perfLog(`window ${json}`);
|
|
1756
|
-
windowListener?.(`${Date.now()}
|
|
1799
|
+
windowListener?.(`${Date.now()} KEEPSNAPPY window ${json}`);
|
|
1757
1800
|
if (settings.file) {
|
|
1758
1801
|
const path = getNative()?.writeText(
|
|
1759
1802
|
`window-${window.id}-${window.name.replace(/[^\w.:+-]/g, '_')}.json`,
|
|
@@ -1948,7 +1991,7 @@ const forwardSamples = () => {
|
|
|
1948
1991
|
if (record.t <= samplesSentT) {
|
|
1949
1992
|
continue;
|
|
1950
1993
|
}
|
|
1951
|
-
windowListener(`${record.wallMs}
|
|
1994
|
+
windowListener(`${record.wallMs} KEEPSNAPPY sample ${JSON.stringify(record)}`);
|
|
1952
1995
|
samplesSentT = record.t;
|
|
1953
1996
|
}
|
|
1954
1997
|
} catch (error) {
|
|
@@ -1973,7 +2016,7 @@ export const writeRunEvent = (kind: 'gc' | 'appstate', record: Record<string, un
|
|
|
1973
2016
|
const json = JSON.stringify({ ...record, t: now(), wallMs: Date.now() });
|
|
1974
2017
|
perfLog(`${kind} ${json}`);
|
|
1975
2018
|
if (inRun() && (kind === 'gc' || gcRanSinceFlush)) {
|
|
1976
|
-
windowListener?.(`${Date.now()}
|
|
2019
|
+
windowListener?.(`${Date.now()} KEEPSNAPPY ${kind} ${json}`);
|
|
1977
2020
|
}
|
|
1978
2021
|
};
|
|
1979
2022
|
|
|
@@ -1995,9 +2038,13 @@ export const setFlushHandler = (handler: FlushHandler | null) => {
|
|
|
1995
2038
|
};
|
|
1996
2039
|
|
|
1997
2040
|
// `flush=1`: end the open windows, send every unsent line, then show the result
|
|
1998
|
-
// to the test driver as an accessibility element (iOS: id `
|
|
2041
|
+
// to the test driver as an accessibility element (iOS: id `keepsnappy.flushed`,
|
|
1999
2042
|
// label `ok` / `error` / `off`), so a flow can wait for it without network access.
|
|
2000
|
-
|
|
2043
|
+
// The driver's name for one flush link (`ack=<n>`), echoed at the end of the answer's detail: the answer element stays up
|
|
2044
|
+
// until the next one replaces it, so without it a later flush in the same process could read an earlier flush's answer.
|
|
2045
|
+
const ACK_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
|
|
2046
|
+
|
|
2047
|
+
const flushOnce = async (ack: string | undefined) => {
|
|
2001
2048
|
// One deadline for the whole flush (lines, clock, artifacts); the end report of the upload comes after it.
|
|
2002
2049
|
const deadline = Date.now() + ARTIFACT_UPLOAD_DEADLINE_MS;
|
|
2003
2050
|
endAll('flush');
|
|
@@ -2008,6 +2055,8 @@ const flushOnce = async () => {
|
|
|
2008
2055
|
} catch (error) {
|
|
2009
2056
|
perfLog(`flush files failed ${String(error)}`);
|
|
2010
2057
|
}
|
|
2058
|
+
// Before anything that waits on the network: a flush that never ends, or a process killed now, leaves the list to a later one.
|
|
2059
|
+
pendingArtifacts.note(PROCESS_ID, [...artifactBacklog, ...artifacts], runToken);
|
|
2011
2060
|
let status = 'off';
|
|
2012
2061
|
let detail = 'push not configured';
|
|
2013
2062
|
if (flushHandler) {
|
|
@@ -2026,10 +2075,25 @@ const flushOnce = async () => {
|
|
|
2026
2075
|
const sent = await sendArtifacts(artifacts, deadline);
|
|
2027
2076
|
if (sent) {
|
|
2028
2077
|
detail += ` artifacts=${sent.uploaded}/${sent.total}`;
|
|
2078
|
+
// A profile file of a run that did not reach the API is missing data, whatever kept it back: failed after its tries, left
|
|
2079
|
+
// unsent by the deadline, kept on the device (token refused, no endpoint, no native upload) or refused by the API.
|
|
2080
|
+
const missing = sent.failed + sent.pending + (runToken !== null ? sent.local + sent.rejected : 0);
|
|
2081
|
+
if (status === 'ok' && missing > 0) {
|
|
2082
|
+
status = 'error';
|
|
2083
|
+
detail += ` failed=${sent.failed} pending=${sent.pending} local=${sent.local} rejected=${sent.rejected}`;
|
|
2084
|
+
}
|
|
2029
2085
|
}
|
|
2030
2086
|
} catch (error) {
|
|
2031
2087
|
perfLog(`artifacts failed ${String(error)}`);
|
|
2032
2088
|
}
|
|
2089
|
+
try {
|
|
2090
|
+
await sendEarlierArtifacts(deadline);
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
perfLog(`artifacts earlier failed ${String(error)}`);
|
|
2093
|
+
}
|
|
2094
|
+
if (ack !== undefined && ACK_PATTERN.test(ack)) {
|
|
2095
|
+
detail += ` ack=${ack}`;
|
|
2096
|
+
}
|
|
2033
2097
|
perfLog(`flush ${status} ${detail}`);
|
|
2034
2098
|
try {
|
|
2035
2099
|
getNative()?.flushSignal?.(status, detail);
|
|
@@ -2041,11 +2105,11 @@ const flushOnce = async () => {
|
|
|
2041
2105
|
// A second flush link while one runs (the driver's final flush) waits for it: the flush signal means the
|
|
2042
2106
|
// uploads of the first are done, so it must not be raised by a flush that has nothing left to send.
|
|
2043
2107
|
let flushing: Promise<void> | null = null;
|
|
2044
|
-
export const flushNow = (): Promise<void> => {
|
|
2108
|
+
export const flushNow = (ack?: string): Promise<void> => {
|
|
2045
2109
|
commandAnswers.clear();
|
|
2046
2110
|
gcRanSinceFlush = false;
|
|
2047
2111
|
// The first flush starts at once (it ends the open windows in the same tick as the link).
|
|
2048
|
-
const run = flushing === null ? flushOnce() : flushing.then(flushOnce);
|
|
2112
|
+
const run = flushing === null ? flushOnce(ack) : flushing.then(() => flushOnce(ack));
|
|
2049
2113
|
const tail: Promise<void> = run
|
|
2050
2114
|
.catch(() => undefined)
|
|
2051
2115
|
.then(() => {
|
|
@@ -2059,7 +2123,7 @@ export const flushNow = (): Promise<void> => {
|
|
|
2059
2123
|
|
|
2060
2124
|
// The activation's profile/cpuprof turn those on for the whole process; off
|
|
2061
2125
|
// leaves the build's env defaults.
|
|
2062
|
-
// A
|
|
2126
|
+
// A KeepSnappy run (one with a run token) forces an idle GC between windows so heap-after-GC
|
|
2063
2127
|
// readings exist for the leak slope (plan 41 Q7); `gcwin=0` in a link still turns it off.
|
|
2064
2128
|
const applyDecision = (decision: Decision) => {
|
|
2065
2129
|
settings.diag = decision.diag;
|
|
@@ -2093,13 +2157,13 @@ const activationReport = () => {
|
|
|
2093
2157
|
mode: 'active',
|
|
2094
2158
|
buildMode: decision.mode,
|
|
2095
2159
|
read: decision.read,
|
|
2096
|
-
installMs: g.
|
|
2160
|
+
installMs: g.__KEEPSNAPPY_ACTIVATION__?.installMs,
|
|
2097
2161
|
runToken: runToken !== null,
|
|
2098
2162
|
expiresAt: decision.expiresAt,
|
|
2099
2163
|
diag: decision.diag,
|
|
2100
2164
|
profile: decision.profile,
|
|
2101
2165
|
cpuprof: decision.cpuprof,
|
|
2102
|
-
renderer: g.
|
|
2166
|
+
renderer: g.__KEEPSNAPPY_ACTIVATION__?.renderer ?? null,
|
|
2103
2167
|
next: next
|
|
2104
2168
|
? {
|
|
2105
2169
|
active: next.active,
|
|
@@ -2242,10 +2306,32 @@ export const actionGc = (n: number, tNative?: number): string => {
|
|
|
2242
2306
|
}
|
|
2243
2307
|
// The gc-pre / gc-post records go out now: the backend pairs them with this command by their seq.
|
|
2244
2308
|
forwardSamples();
|
|
2309
|
+
rememberAnswer(key, answer);
|
|
2310
|
+
answerCommand(n, answer);
|
|
2311
|
+
return answer;
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
const rememberAnswer = (key: string, answer: string) => {
|
|
2245
2315
|
commandAnswers.set(key, answer);
|
|
2246
2316
|
if (commandAnswers.size > COMMAND_ANSWERS_KEPT) {
|
|
2247
2317
|
commandAnswers.delete(commandAnswers.keys().next().value as string);
|
|
2248
2318
|
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// The end GC's manual memory read: the driver's `memread` command, sent with the GC's own n after the GC's answer. One sample, at
|
|
2322
|
+
// once (no timer), reason `endgc-read:<n>`, sent with the run's lines before the answer; the answer is the driver's signal that the
|
|
2323
|
+
// app may go to the background. Derive reads kept from this record (derivation 23).
|
|
2324
|
+
export const endGcRead = (n: number): string => {
|
|
2325
|
+
const key = `${runToken ?? '-'}#read${n}`;
|
|
2326
|
+
const known = commandAnswers.get(key);
|
|
2327
|
+
if (known !== undefined) {
|
|
2328
|
+
answerCommand(n, known);
|
|
2329
|
+
return known;
|
|
2330
|
+
}
|
|
2331
|
+
const record = sample(`endgc-read:${n}`);
|
|
2332
|
+
const answer = `memread:${n} ${record ? 'ok' : 'unavailable'}`;
|
|
2333
|
+
forwardSamples();
|
|
2334
|
+
rememberAnswer(key, answer);
|
|
2249
2335
|
answerCommand(n, answer);
|
|
2250
2336
|
return answer;
|
|
2251
2337
|
};
|
|
@@ -2258,44 +2344,48 @@ const syncCommandListener = () => {
|
|
|
2258
2344
|
if (!native?.commandListen) {
|
|
2259
2345
|
return;
|
|
2260
2346
|
}
|
|
2261
|
-
const on =
|
|
2347
|
+
const on = KEEPSNAPPY_ENABLED && runToken !== null;
|
|
2262
2348
|
if (on && commandSubscription === null) {
|
|
2263
2349
|
commandSubscription =
|
|
2264
|
-
native.addListener?.('
|
|
2350
|
+
native.addListener?.('keepsnappyCommand', e => {
|
|
2265
2351
|
// A command native already timed out on is not run: its GC would land in the next step's action.
|
|
2266
2352
|
if (native.commandPending?.(e.n) === false) {
|
|
2267
|
-
perfLog(`command
|
|
2353
|
+
perfLog(`command ${e.cmd}:${e.n} late, skipped`);
|
|
2268
2354
|
return;
|
|
2269
2355
|
}
|
|
2270
|
-
|
|
2356
|
+
handleKeepSnappyCommand(e.cmd, e.n, e.tNative);
|
|
2271
2357
|
}) ?? null;
|
|
2272
2358
|
}
|
|
2273
2359
|
native.commandListen(on);
|
|
2274
2360
|
};
|
|
2275
2361
|
|
|
2276
2362
|
/** A command (`cmd=<name>&n=<n>`), from a link (iOS) or the native receiver (Android); unknown commands are ignored. */
|
|
2277
|
-
export const
|
|
2278
|
-
if (!
|
|
2363
|
+
export const handleKeepSnappyCommand = (cmd: string | undefined, n: number, tNative?: number): boolean => {
|
|
2364
|
+
if (!KEEPSNAPPY_ENABLED || (cmd !== 'gc' && cmd !== 'memread') || !Number.isInteger(n) || n < 1) {
|
|
2279
2365
|
return false;
|
|
2280
2366
|
}
|
|
2281
2367
|
// Every received command is answered, so the Android receiver is never left pending.
|
|
2282
2368
|
try {
|
|
2283
|
-
|
|
2369
|
+
if (cmd === 'gc') {
|
|
2370
|
+
actionGc(n, tNative);
|
|
2371
|
+
} else {
|
|
2372
|
+
endGcRead(n);
|
|
2373
|
+
}
|
|
2284
2374
|
} catch (error) {
|
|
2285
|
-
perfLog(`command
|
|
2286
|
-
answerCommand(n,
|
|
2375
|
+
perfLog(`command ${cmd}:${n} failed ${String(error)}`);
|
|
2376
|
+
answerCommand(n, `${cmd}:${n} error`);
|
|
2287
2377
|
}
|
|
2288
2378
|
return true;
|
|
2289
2379
|
};
|
|
2290
2380
|
|
|
2291
|
-
export const
|
|
2292
|
-
if (!
|
|
2381
|
+
export const handleKeepSnappyDeepLink = (url: string) => {
|
|
2382
|
+
if (!KEEPSNAPPY_ENABLED) {
|
|
2293
2383
|
return;
|
|
2294
2384
|
}
|
|
2295
2385
|
const params = parseQuery(url.split('?')[1]);
|
|
2296
2386
|
// A command changes no settings and writes no activation record.
|
|
2297
2387
|
if (params.cmd !== undefined) {
|
|
2298
|
-
|
|
2388
|
+
handleKeepSnappyCommand(params.cmd, Number(params.n));
|
|
2299
2389
|
return;
|
|
2300
2390
|
}
|
|
2301
2391
|
const written = activation?.store.apply(params) ?? null;
|
|
@@ -2306,7 +2396,7 @@ export const handlePerfLabDeepLink = (url: string) => {
|
|
|
2306
2396
|
}
|
|
2307
2397
|
applySettings(params);
|
|
2308
2398
|
if (params.flush === '1') {
|
|
2309
|
-
void flushNow();
|
|
2399
|
+
void flushNow(params.ack);
|
|
2310
2400
|
}
|
|
2311
2401
|
};
|
|
2312
2402
|
|
|
@@ -2326,7 +2416,7 @@ export const addStartupSection = (name: string, read: () => unknown) => {
|
|
|
2326
2416
|
|
|
2327
2417
|
export const logStartup = (phase: string) => {
|
|
2328
2418
|
const native = getNative();
|
|
2329
|
-
const polyfill = g.
|
|
2419
|
+
const polyfill = g.__KEEPSNAPPY_HOOK__?.state;
|
|
2330
2420
|
const extra: Record<string, unknown> = {};
|
|
2331
2421
|
for (const [name, read] of extraStartupSections) {
|
|
2332
2422
|
extra[name] = safe(read);
|
|
@@ -2391,7 +2481,7 @@ export const logStartup = (phase: string) => {
|
|
|
2391
2481
|
};
|
|
2392
2482
|
|
|
2393
2483
|
export const exposeGlobal = () => {
|
|
2394
|
-
g.
|
|
2484
|
+
g.__KEEPSNAPPY__ = {
|
|
2395
2485
|
settings,
|
|
2396
2486
|
set: applySettings,
|
|
2397
2487
|
markers,
|
package/js/sdk/cpuprof.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// overlapping windows (a press and the screen it opens) share one span and one
|
|
4
4
|
// file. Inside a run it samples on until `flush=1` and writes one file there.
|
|
5
5
|
// The file is a DevTools .cpuprofile (nodes/samples/timeDeltas) written by the
|
|
6
|
-
// native module into Documents/
|
|
6
|
+
// native module into Documents/keepsnappy.
|
|
7
7
|
|
|
8
8
|
export interface CpuProfileNative {
|
|
9
9
|
cpuProfileStart(hz: number): boolean;
|
package/js/sdk/dormant.ts
CHANGED
|
@@ -16,14 +16,14 @@ interface DormantNative {
|
|
|
16
16
|
|
|
17
17
|
interface DormantGlobal {
|
|
18
18
|
__REACT_DEVTOOLS_GLOBAL_HOOK__?: unknown;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
__KEEPSNAPPY_HOOK__?: { state?: { mode?: string; listener?: unknown } };
|
|
20
|
+
__KEEPSNAPPY__?: unknown;
|
|
21
21
|
expo?: { modules?: Record<string, unknown> };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const g = globalThis as unknown as DormantGlobal;
|
|
25
25
|
|
|
26
|
-
const NET_MARK = '
|
|
26
|
+
const NET_MARK = '__keepsnappyNet';
|
|
27
27
|
|
|
28
28
|
const marked = (value: unknown) =>
|
|
29
29
|
(typeof value === 'function' || (typeof value === 'object' && value !== null)) &&
|
|
@@ -54,8 +54,8 @@ export const dormantReport = (decision: Decision) => ({
|
|
|
54
54
|
buildMode: decision.mode,
|
|
55
55
|
read: decision.read,
|
|
56
56
|
reactHook: typeof g.__REACT_DEVTOOLS_GLOBAL_HOOK__,
|
|
57
|
-
|
|
58
|
-
sdkGlobal: g.
|
|
57
|
+
keepsnappyHook: g.__KEEPSNAPPY_HOOK__ ? (g.__KEEPSNAPPY_HOOK__.state?.mode ?? 'present') : 'absent',
|
|
58
|
+
sdkGlobal: g.__KEEPSNAPPY__ !== undefined,
|
|
59
59
|
fetchWrapped: globalWrapped('fetch'),
|
|
60
60
|
xhrGetterWrapped: globalWrapped('XMLHttpRequest'),
|
|
61
61
|
xhrPatched: xhrPatched(),
|
|
@@ -69,9 +69,9 @@ export const startDormant = (decision: Decision) => {
|
|
|
69
69
|
store.apply(params);
|
|
70
70
|
if (params.dump === '1') {
|
|
71
71
|
// Asked for: one os_log line (iOS) / logcat line (Android), no log file.
|
|
72
|
-
const native = g.expo?.modules?.
|
|
72
|
+
const native = g.expo?.modules?.KeepSnappy as DormantNative | undefined;
|
|
73
73
|
const report = { ...dormantReport(decision), activation: store.stats() };
|
|
74
|
-
native?.osLog?.(`
|
|
74
|
+
native?.osLog?.(`KEEPSNAPPY dormant ${JSON.stringify(report)}`);
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
};
|
package/js/sdk/links.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
// Settings links (`<scheme>://devtools/
|
|
1
|
+
// Settings links (`<scheme>://devtools/keepsnappy?...`) without app code: the SDK
|
|
2
2
|
// runs before the router subscribes to Linking, so it wraps the link sources.
|
|
3
|
-
// A
|
|
3
|
+
// A keepsnappy link goes to the handler here and never reaches the app's
|
|
4
4
|
// listeners, so the router does not navigate to an unmatched route. Only a URL
|
|
5
|
-
// whose host and path are exactly `devtools/
|
|
5
|
+
// whose host and path are exactly `devtools/keepsnappy` on a custom (non-web)
|
|
6
6
|
// scheme is one; every other URL passes through unchanged after one substring
|
|
7
7
|
// check: in a dormant build this is the only work the SDK does after startup.
|
|
8
8
|
|
|
9
9
|
import { Linking } from 'react-native';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { KEEPSNAPPY_DEEP_LINK_PATH } from './activation';
|
|
12
12
|
|
|
13
13
|
export type LinkHandler = (url: string, via: string) => void;
|
|
14
|
-
// Told when an app URL (not a
|
|
14
|
+
// Told when an app URL (not a keepsnappy one) is handed to the app, before the
|
|
15
15
|
// app sees it: the start of a link-driven navigation.
|
|
16
16
|
export type AppLinkHandler = (url: string, via: string) => void;
|
|
17
17
|
|
|
18
18
|
type UrlListener = (event: { url: string }) => void;
|
|
19
19
|
|
|
20
|
-
// `<scheme>://devtools/
|
|
21
|
-
// (`<scheme>:///devtools/
|
|
20
|
+
// `<scheme>://devtools/keepsnappy[/][?query][#fragment]`, also with an empty host
|
|
21
|
+
// (`<scheme>:///devtools/keepsnappy`). Universal links (http/https) never match,
|
|
22
22
|
// so a web page cannot route a link here.
|
|
23
|
-
// Spells out
|
|
24
|
-
const SETTINGS_LINK = /^([a-z][a-z0-9+.-]*):\/\/\/?devtools\/
|
|
23
|
+
// Spells out KEEPSNAPPY_DEEP_LINK_PATH.
|
|
24
|
+
const SETTINGS_LINK = /^([a-z][a-z0-9+.-]*):\/\/\/?devtools\/keepsnappy\/?(?:[?#]|$)/i;
|
|
25
25
|
const WEB_SCHEMES = new Set(['http', 'https']);
|
|
26
26
|
|
|
27
|
-
export const
|
|
28
|
-
if (typeof url !== 'string' || !url.includes(
|
|
27
|
+
export const isKeepSnappyUrl = (url: unknown): url is string => {
|
|
28
|
+
if (typeof url !== 'string' || !url.includes(KEEPSNAPPY_DEEP_LINK_PATH)) {
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
31
|
const match = SETTINGS_LINK.exec(url);
|
|
@@ -126,7 +126,7 @@ const patch = (
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
const filterInitial = (url: unknown, via: string) => {
|
|
129
|
-
if (
|
|
129
|
+
if (isKeepSnappyUrl(url)) {
|
|
130
130
|
apply(url, via);
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
@@ -157,7 +157,7 @@ export const installLinks = (
|
|
|
157
157
|
return original(type, listener);
|
|
158
158
|
}
|
|
159
159
|
const filtered: UrlListener = event => {
|
|
160
|
-
if (
|
|
160
|
+
if (isKeepSnappyUrl(event?.url)) {
|
|
161
161
|
apply(event.url, 'event');
|
|
162
162
|
return;
|
|
163
163
|
}
|
package/js/sdk/net.ts
CHANGED
|
@@ -403,8 +403,8 @@ export type NetCounter = ReturnType<typeof createNetCounter>;
|
|
|
403
403
|
// images loaded natively (Image, expo-image), WebSockets, and native SDKs
|
|
404
404
|
// (Stream, LiveKit, Firebase).
|
|
405
405
|
|
|
406
|
-
const WRAPPED = '
|
|
407
|
-
const XHR_STATE = '
|
|
406
|
+
const WRAPPED = '__keepsnappyNet';
|
|
407
|
+
const XHR_STATE = '__keepsnappyNetXhr';
|
|
408
408
|
const EXPO_BUILTIN = Symbol.for('expo.builtin');
|
|
409
409
|
|
|
410
410
|
type AnyFn = (...args: unknown[]) => unknown;
|
package/js/sdk/profile-export.ts
CHANGED
|
@@ -178,7 +178,7 @@ const changeDescription = (cause: UpdateCause) => ({
|
|
|
178
178
|
|
|
179
179
|
export interface ReactExport {
|
|
180
180
|
version: 5;
|
|
181
|
-
source: '
|
|
181
|
+
source: 'keepsnappy';
|
|
182
182
|
captureStart: { clock: 'perf-now-ms'; t0: number };
|
|
183
183
|
dataForRoots: {
|
|
184
184
|
rootID: number;
|
|
@@ -225,11 +225,11 @@ export const buildReactExport = (t0: number, commits: ExportCommit[]): ReactExpo
|
|
|
225
225
|
passiveEffectDuration: null,
|
|
226
226
|
priorityLevel: null,
|
|
227
227
|
timestamp: Math.max(0, commit.t - t0),
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
...(commit.facts ? {
|
|
228
|
+
keepsnappyCauses: commit.causes,
|
|
229
|
+
keepsnappyCarrier: commit.carrier,
|
|
230
|
+
keepsnappyStore: commit.store,
|
|
231
|
+
keepsnappyTask: commit.task,
|
|
232
|
+
...(commit.facts ? { keepsnappyFacts: commit.facts } : {}),
|
|
233
233
|
updaters: (commit.updaters ?? []).map(displayName => ({
|
|
234
234
|
displayName,
|
|
235
235
|
hocDisplayNames: null,
|
|
@@ -241,7 +241,7 @@ export const buildReactExport = (t0: number, commits: ExportCommit[]): ReactExpo
|
|
|
241
241
|
});
|
|
242
242
|
return {
|
|
243
243
|
version: 5,
|
|
244
|
-
source: '
|
|
244
|
+
source: 'keepsnappy',
|
|
245
245
|
captureStart: { clock: 'perf-now-ms', t0 },
|
|
246
246
|
dataForRoots: [
|
|
247
247
|
{
|