@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
package/js/sdk/links.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Settings links (`<scheme>://devtools/perf-lab?...`) without app code: the SDK
|
|
2
|
+
// runs before the router subscribes to Linking, so it wraps the link sources.
|
|
3
|
+
// A perf-lab link goes to the handler here and never reaches the app's
|
|
4
|
+
// listeners, so the router does not navigate to an unmatched route. Only a URL
|
|
5
|
+
// whose host and path are exactly `devtools/perf-lab` on a custom (non-web)
|
|
6
|
+
// scheme is one; every other URL passes through unchanged after one substring
|
|
7
|
+
// check: in a dormant build this is the only work the SDK does after startup.
|
|
8
|
+
|
|
9
|
+
import { Linking } from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { PERF_LAB_DEEP_LINK_PATH } from './activation';
|
|
12
|
+
|
|
13
|
+
export type LinkHandler = (url: string, via: string) => void;
|
|
14
|
+
// Told when an app URL (not a perf-lab one) is handed to the app, before the
|
|
15
|
+
// app sees it: the start of a link-driven navigation.
|
|
16
|
+
export type AppLinkHandler = (url: string, via: string) => void;
|
|
17
|
+
|
|
18
|
+
type UrlListener = (event: { url: string }) => void;
|
|
19
|
+
|
|
20
|
+
// `<scheme>://devtools/perf-lab[/][?query][#fragment]`, also with an empty host
|
|
21
|
+
// (`<scheme>:///devtools/perf-lab`). Universal links (http/https) never match,
|
|
22
|
+
// so a web page cannot route a link here.
|
|
23
|
+
// Spells out PERF_LAB_DEEP_LINK_PATH.
|
|
24
|
+
const SETTINGS_LINK = /^([a-z][a-z0-9+.-]*):\/\/\/?devtools\/perf-lab\/?(?:[?#]|$)/i;
|
|
25
|
+
const WEB_SCHEMES = new Set(['http', 'https']);
|
|
26
|
+
|
|
27
|
+
export const isPerfLabUrl = (url: unknown): url is string => {
|
|
28
|
+
if (typeof url !== 'string' || !url.includes(PERF_LAB_DEEP_LINK_PATH)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const match = SETTINGS_LINK.exec(url);
|
|
32
|
+
return match !== null && !WEB_SCHEMES.has((match[1] ?? '').toLowerCase());
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// The same link can arrive twice (cold-start URL and a `url` event).
|
|
36
|
+
const DUPLICATE_MS = 2000;
|
|
37
|
+
let lastApplied: { url: string; t: number } | null = null;
|
|
38
|
+
|
|
39
|
+
let handler: LinkHandler = () => undefined;
|
|
40
|
+
let appLinkHandler: AppLinkHandler | null = null;
|
|
41
|
+
|
|
42
|
+
const noteAppLink = (url: unknown, via: string) => {
|
|
43
|
+
if (appLinkHandler && typeof url === 'string' && url !== '') {
|
|
44
|
+
try {
|
|
45
|
+
appLinkHandler(url, via);
|
|
46
|
+
} catch {
|
|
47
|
+
// never breaks the app's link handling
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const apply = (url: string, via: string) => {
|
|
53
|
+
const t = Date.now();
|
|
54
|
+
if (
|
|
55
|
+
lastApplied &&
|
|
56
|
+
lastApplied.url === url &&
|
|
57
|
+
t - lastApplied.t < DUPLICATE_MS
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
lastApplied = { url, t };
|
|
62
|
+
handler(url, via);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Every `runToken` value, however often and however its name is encoded.
|
|
66
|
+
export const redactUrl = (url: string) => {
|
|
67
|
+
const q = url.indexOf('?');
|
|
68
|
+
if (q < 0) {
|
|
69
|
+
return url;
|
|
70
|
+
}
|
|
71
|
+
const pairs = url
|
|
72
|
+
.slice(q + 1)
|
|
73
|
+
.split('&')
|
|
74
|
+
.map(pair => {
|
|
75
|
+
const raw = pair.split('=')[0] ?? '';
|
|
76
|
+
let name = raw;
|
|
77
|
+
try {
|
|
78
|
+
name = decodeURIComponent(raw);
|
|
79
|
+
} catch {
|
|
80
|
+
// keeps the raw name
|
|
81
|
+
}
|
|
82
|
+
return name === 'runToken' ? `${raw}=<redacted>` : pair;
|
|
83
|
+
});
|
|
84
|
+
return `${url.slice(0, q + 1)}${pairs.join('&')}`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const linkStats = { patched: [] as string[], failed: [] as string[] };
|
|
88
|
+
|
|
89
|
+
export const linkPatchStatus = () => linkStats;
|
|
90
|
+
|
|
91
|
+
const patch = (
|
|
92
|
+
target: Record<string, unknown>,
|
|
93
|
+
method: string,
|
|
94
|
+
label: string,
|
|
95
|
+
wrap: (
|
|
96
|
+
original: (...args: unknown[]) => unknown,
|
|
97
|
+
) => (...args: unknown[]) => unknown,
|
|
98
|
+
) => {
|
|
99
|
+
const original = target[method];
|
|
100
|
+
if (typeof original !== 'function') {
|
|
101
|
+
linkStats.failed.push(`${label}: not a function`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const wrapped = wrap((...args: unknown[]) =>
|
|
105
|
+
(original as (...a: unknown[]) => unknown).apply(target, args),
|
|
106
|
+
);
|
|
107
|
+
try {
|
|
108
|
+
target[method] = wrapped;
|
|
109
|
+
} catch {
|
|
110
|
+
// ESM namespace bindings can be getter-only.
|
|
111
|
+
}
|
|
112
|
+
if (target[method] !== wrapped) {
|
|
113
|
+
try {
|
|
114
|
+
Object.defineProperty(target, method, {
|
|
115
|
+
configurable: true,
|
|
116
|
+
enumerable: true,
|
|
117
|
+
writable: true,
|
|
118
|
+
value: wrapped,
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
linkStats.failed.push(`${label}: ${String(error)}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
linkStats.patched.push(label);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const filterInitial = (url: unknown, via: string) => {
|
|
129
|
+
if (isPerfLabUrl(url)) {
|
|
130
|
+
apply(url, via);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
noteAppLink(url, via);
|
|
134
|
+
return url;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let installed = false;
|
|
138
|
+
|
|
139
|
+
export const installLinks = (
|
|
140
|
+
onLink: LinkHandler,
|
|
141
|
+
onAppLink: AppLinkHandler | null = null,
|
|
142
|
+
) => {
|
|
143
|
+
handler = onLink;
|
|
144
|
+
appLinkHandler = onAppLink;
|
|
145
|
+
if (installed) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
installed = true;
|
|
149
|
+
const rnLinking = Linking as unknown as Record<string, unknown>;
|
|
150
|
+
|
|
151
|
+
patch(
|
|
152
|
+
rnLinking,
|
|
153
|
+
'addEventListener',
|
|
154
|
+
'Linking.addEventListener',
|
|
155
|
+
original => (type: unknown, listener: unknown) => {
|
|
156
|
+
if (type !== 'url' || typeof listener !== 'function') {
|
|
157
|
+
return original(type, listener);
|
|
158
|
+
}
|
|
159
|
+
const filtered: UrlListener = event => {
|
|
160
|
+
if (isPerfLabUrl(event?.url)) {
|
|
161
|
+
apply(event.url, 'event');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
noteAppLink(event?.url, 'event');
|
|
165
|
+
(listener as UrlListener)(event);
|
|
166
|
+
};
|
|
167
|
+
return original(type, filtered);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
patch(
|
|
172
|
+
rnLinking,
|
|
173
|
+
'getInitialURL',
|
|
174
|
+
'Linking.getInitialURL',
|
|
175
|
+
original => () =>
|
|
176
|
+
Promise.resolve(original()).then(url => filterInitial(url, 'initial')),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const expoLinking = require('expo-linking') as Record<string, unknown>;
|
|
181
|
+
patch(
|
|
182
|
+
expoLinking,
|
|
183
|
+
'getLinkingURL',
|
|
184
|
+
'expo-linking.getLinkingURL',
|
|
185
|
+
original => () => filterInitial(original(), 'linkingURL'),
|
|
186
|
+
);
|
|
187
|
+
patch(
|
|
188
|
+
expoLinking,
|
|
189
|
+
'getInitialURL',
|
|
190
|
+
'expo-linking.getInitialURL',
|
|
191
|
+
original => () =>
|
|
192
|
+
Promise.resolve(original()).then(url => filterInitial(url, 'initial')),
|
|
193
|
+
);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
linkStats.failed.push(`expo-linking: ${String(error)}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Nothing subscribes to `url` events in an app that only reads the initial
|
|
199
|
+
// URL; this listener makes runtime links work there too.
|
|
200
|
+
Linking.addEventListener('url', () => undefined);
|
|
201
|
+
};
|
package/js/sdk/nav.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// React Navigation containers found in the commit walk, with no app code: the
|
|
2
|
+
// NavigationContainerRefContext provider value is the container's imperative
|
|
3
|
+
// handle. Each container's `state` event gives the focused route chain; the
|
|
4
|
+
// unstable `__unsafe_action__` event, when it fires, timestamps the dispatch.
|
|
5
|
+
|
|
6
|
+
import { type CauseRoot, currentCause } from './causes';
|
|
7
|
+
import {
|
|
8
|
+
fmt,
|
|
9
|
+
lastRoot,
|
|
10
|
+
now,
|
|
11
|
+
onAfterCommit,
|
|
12
|
+
percentile,
|
|
13
|
+
perfLog,
|
|
14
|
+
settings,
|
|
15
|
+
} from './core';
|
|
16
|
+
import {
|
|
17
|
+
CONTEXT_PROVIDER,
|
|
18
|
+
type Fiber,
|
|
19
|
+
findInTree,
|
|
20
|
+
type NavigationRef,
|
|
21
|
+
noteContainer,
|
|
22
|
+
onNavigationContainer,
|
|
23
|
+
registrySizes,
|
|
24
|
+
} from './fiber';
|
|
25
|
+
|
|
26
|
+
export interface RoutePart {
|
|
27
|
+
name: string;
|
|
28
|
+
key: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NavAction {
|
|
32
|
+
t: number;
|
|
33
|
+
type: string;
|
|
34
|
+
// The action (touch, link, typing) behind the code that dispatched it (causes.ts).
|
|
35
|
+
cause: CauseRoot | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NavSnapshot {
|
|
39
|
+
chain: RoutePart[];
|
|
40
|
+
stateT: number;
|
|
41
|
+
action: NavAction | null;
|
|
42
|
+
// The action behind the state change: the cause of the code that emitted the first `state` event.
|
|
43
|
+
cause: CauseRoot | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Container {
|
|
47
|
+
id: number;
|
|
48
|
+
fiber: Fiber;
|
|
49
|
+
ref: NavigationRef;
|
|
50
|
+
depth: number;
|
|
51
|
+
foundAt: number;
|
|
52
|
+
source: 'mount' | 'scan';
|
|
53
|
+
routesAtDiscovery: number;
|
|
54
|
+
unsubscribe: (() => void)[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const containers = new Map<Fiber, Container>();
|
|
58
|
+
let nextContainerId = 1;
|
|
59
|
+
|
|
60
|
+
interface IndexedRoute {
|
|
61
|
+
name: string;
|
|
62
|
+
path: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const MAX_INDEXED_KEYS = 1000;
|
|
66
|
+
const keyIndex = new Map<string, IndexedRoute>();
|
|
67
|
+
|
|
68
|
+
// Action types that don't move the user anywhere, so they never end a window.
|
|
69
|
+
const PASSIVE_ACTIONS = new Set(['SET_PARAMS', 'PRELOAD']);
|
|
70
|
+
|
|
71
|
+
const MAX_COST_SAMPLES = 1000;
|
|
72
|
+
const MAX_LISTED = 100;
|
|
73
|
+
|
|
74
|
+
const stats = {
|
|
75
|
+
firstContainerAt: null as number | null,
|
|
76
|
+
stateEvents: 0,
|
|
77
|
+
evaluations: 0,
|
|
78
|
+
actions: { n: 0, noop: 0, byType: {} as Record<string, number> },
|
|
79
|
+
costMs: [] as number[],
|
|
80
|
+
chains: new Set<string>(),
|
|
81
|
+
errors: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let pendingStateT: number | null = null;
|
|
85
|
+
let pendingStateCause: CauseRoot | null = null;
|
|
86
|
+
let pendingAction: NavAction | null = null;
|
|
87
|
+
let evaluateScheduled = false;
|
|
88
|
+
let scanned = false;
|
|
89
|
+
let scanning = false;
|
|
90
|
+
|
|
91
|
+
let snapshotListener: ((snapshot: NavSnapshot) => void) | null = null;
|
|
92
|
+
let actionListener: ((action: NavAction) => void) | null = null;
|
|
93
|
+
|
|
94
|
+
export const onNavSnapshot = (listener: (snapshot: NavSnapshot) => void) => {
|
|
95
|
+
snapshotListener = listener;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const onNavAction = (listener: (action: NavAction) => void) => {
|
|
99
|
+
actionListener = listener;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const containerCount = () => containers.size;
|
|
103
|
+
|
|
104
|
+
export const resolveRouteKey = (key: string) => keyIndex.get(key) ?? null;
|
|
105
|
+
|
|
106
|
+
export const recordNavCost = (ms: number) => {
|
|
107
|
+
if (stats.costMs.length < MAX_COST_SAMPLES) {
|
|
108
|
+
stats.costMs.push(ms);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const noteError = (where: string, error: unknown) => {
|
|
113
|
+
stats.errors++;
|
|
114
|
+
if (stats.errors <= 3) {
|
|
115
|
+
perfLog(`nav error ${where} ${String(error)}`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
interface StateLike {
|
|
120
|
+
index?: unknown;
|
|
121
|
+
routes?: unknown;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface RouteLike {
|
|
125
|
+
name?: unknown;
|
|
126
|
+
key?: unknown;
|
|
127
|
+
state?: unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const indexKeys = (state: StateLike | undefined, prefix: string) => {
|
|
131
|
+
if (!state || !Array.isArray(state.routes)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const route of state.routes as RouteLike[]) {
|
|
135
|
+
if (typeof route?.name !== 'string') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const path = prefix ? `${prefix}/${route.name}` : route.name;
|
|
139
|
+
if (typeof route.key === 'string') {
|
|
140
|
+
keyIndex.delete(route.key);
|
|
141
|
+
keyIndex.set(route.key, { name: route.name, path });
|
|
142
|
+
if (keyIndex.size > MAX_INDEXED_KEYS) {
|
|
143
|
+
const oldest = keyIndex.keys().next().value;
|
|
144
|
+
if (oldest !== undefined) {
|
|
145
|
+
keyIndex.delete(oldest);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
indexKeys(route.state as StateLike | undefined, path);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Same rule as React Navigation's findFocusedRoute: a partial state without
|
|
154
|
+
// `index` focuses its first route.
|
|
155
|
+
const focusedChainOf = (root: unknown): RoutePart[] => {
|
|
156
|
+
const chain: RoutePart[] = [];
|
|
157
|
+
let state = root as StateLike | undefined;
|
|
158
|
+
while (state && Array.isArray(state.routes)) {
|
|
159
|
+
const index = typeof state.index === 'number' ? state.index : 0;
|
|
160
|
+
const route = state.routes[index] as RouteLike | undefined;
|
|
161
|
+
if (!route || typeof route.name !== 'string') {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
chain.push({
|
|
165
|
+
name: route.name,
|
|
166
|
+
key: typeof route.key === 'string' ? route.key : null,
|
|
167
|
+
});
|
|
168
|
+
state = route.state as StateLike | undefined;
|
|
169
|
+
}
|
|
170
|
+
return chain;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Nested independent containers (e.g. a sheet with its own navigator) extend
|
|
174
|
+
// the chain of the containers above them, as the route-prop scan did.
|
|
175
|
+
const evaluate = () => {
|
|
176
|
+
evaluateScheduled = false;
|
|
177
|
+
if (!settings.hook) {
|
|
178
|
+
pendingStateT = null;
|
|
179
|
+
pendingStateCause = null;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const t0 = now();
|
|
183
|
+
stats.evaluations++;
|
|
184
|
+
const ordered = [...containers.values()].sort((a, b) => a.depth - b.depth);
|
|
185
|
+
const chain: RoutePart[] = [];
|
|
186
|
+
for (const container of ordered) {
|
|
187
|
+
try {
|
|
188
|
+
const root = container.ref.getRootState();
|
|
189
|
+
indexKeys(root as StateLike | undefined, '');
|
|
190
|
+
chain.push(...focusedChainOf(root));
|
|
191
|
+
} catch (error) {
|
|
192
|
+
noteError('getRootState', error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const snapshot: NavSnapshot = {
|
|
196
|
+
chain,
|
|
197
|
+
stateT: pendingStateT ?? t0,
|
|
198
|
+
action: pendingAction,
|
|
199
|
+
cause: pendingStateCause,
|
|
200
|
+
};
|
|
201
|
+
pendingStateT = null;
|
|
202
|
+
pendingStateCause = null;
|
|
203
|
+
if (chain.length > 0 && stats.chains.size < MAX_LISTED) {
|
|
204
|
+
stats.chains.add(chain.map(part => part.name).join('/'));
|
|
205
|
+
}
|
|
206
|
+
snapshotListener?.(snapshot);
|
|
207
|
+
recordNavCost(now() - t0);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const scheduleEvaluate = () => {
|
|
211
|
+
if (!evaluateScheduled) {
|
|
212
|
+
evaluateScheduled = true;
|
|
213
|
+
setTimeout(evaluate, 0);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Called by the visit logic once a snapshot started a visit, so an older
|
|
218
|
+
// action is not reused as the start of a later navigation.
|
|
219
|
+
export const consumeAction = () => {
|
|
220
|
+
pendingAction = null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const onState = () => {
|
|
224
|
+
stats.stateEvents++;
|
|
225
|
+
if (pendingStateT === null) {
|
|
226
|
+
pendingStateT = now();
|
|
227
|
+
pendingStateCause = currentCause();
|
|
228
|
+
}
|
|
229
|
+
scheduleEvaluate();
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const onUnsafeAction = (event: unknown) => {
|
|
233
|
+
const t = now();
|
|
234
|
+
const data = (
|
|
235
|
+
event as { data?: { action?: { type?: unknown }; noop?: unknown } }
|
|
236
|
+
)?.data;
|
|
237
|
+
const type =
|
|
238
|
+
typeof data?.action?.type === 'string' ? data.action.type : 'unknown';
|
|
239
|
+
stats.actions.n++;
|
|
240
|
+
stats.actions.byType[type] = (stats.actions.byType[type] ?? 0) + 1;
|
|
241
|
+
if (data?.noop === true) {
|
|
242
|
+
stats.actions.noop++;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
perfLog(`nav action type=${type} t=${fmt(t)}`);
|
|
246
|
+
if (PASSIVE_ACTIONS.has(type)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
pendingAction = { t, type, cause: currentCause() };
|
|
250
|
+
actionListener?.(pendingAction);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const subscribe = (
|
|
254
|
+
ref: NavigationRef,
|
|
255
|
+
type: string,
|
|
256
|
+
listener: (event: unknown) => void,
|
|
257
|
+
) => {
|
|
258
|
+
try {
|
|
259
|
+
const off = ref.addListener(type, listener);
|
|
260
|
+
return typeof off === 'function' ? (off as () => void) : null;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
noteError(`addListener ${type}`, error);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const added = (
|
|
268
|
+
fiber: Fiber,
|
|
269
|
+
ref: NavigationRef,
|
|
270
|
+
depth: number,
|
|
271
|
+
source: Container['source'],
|
|
272
|
+
) => {
|
|
273
|
+
const container: Container = {
|
|
274
|
+
id: nextContainerId++,
|
|
275
|
+
fiber,
|
|
276
|
+
ref,
|
|
277
|
+
depth,
|
|
278
|
+
foundAt: now(),
|
|
279
|
+
source,
|
|
280
|
+
routesAtDiscovery: registrySizes().routes,
|
|
281
|
+
unsubscribe: [],
|
|
282
|
+
};
|
|
283
|
+
stats.firstContainerAt ??= container.foundAt;
|
|
284
|
+
// Subscribed during the commit, before passive effects run, so the
|
|
285
|
+
// container's first `state` emit (on mount) is not missed.
|
|
286
|
+
for (const [type, listener] of [
|
|
287
|
+
['state', onState],
|
|
288
|
+
['__unsafe_action__', onUnsafeAction],
|
|
289
|
+
] as const) {
|
|
290
|
+
const off = subscribe(ref, type, listener);
|
|
291
|
+
if (off) {
|
|
292
|
+
container.unsubscribe.push(off);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
containers.set(fiber, container);
|
|
296
|
+
perfLog(
|
|
297
|
+
`nav container id=${container.id} depth=${depth} source=${source} t=${fmt(container.foundAt)} routesBefore=${container.routesAtDiscovery}`,
|
|
298
|
+
);
|
|
299
|
+
scheduleEvaluate();
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const removed = (fiber: Fiber) => {
|
|
303
|
+
const container = containers.get(fiber);
|
|
304
|
+
if (!container) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
for (const off of container.unsubscribe) {
|
|
308
|
+
try {
|
|
309
|
+
off();
|
|
310
|
+
} catch (error) {
|
|
311
|
+
noteError('unsubscribe', error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
containers.delete(fiber);
|
|
315
|
+
perfLog(`nav container removed id=${container.id} t=${fmt(now())}`);
|
|
316
|
+
scheduleEvaluate();
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Containers mounted before the hook was switched on (settings.hook off at
|
|
320
|
+
// boot) were never seen mounting; look for them once in the current tree.
|
|
321
|
+
const scanOnce = () => {
|
|
322
|
+
if (scanned || containers.size > 0 || !settings.hook) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
scanned = true;
|
|
326
|
+
const root = lastRoot(1);
|
|
327
|
+
if (!root) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
scanning = true;
|
|
331
|
+
try {
|
|
332
|
+
findInTree(root.current, fiber => {
|
|
333
|
+
if (fiber.tag === CONTEXT_PROVIDER) {
|
|
334
|
+
noteContainer(fiber);
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
});
|
|
338
|
+
} finally {
|
|
339
|
+
scanning = false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const navStats = () => ({
|
|
344
|
+
containers: [...containers.values()].map(c => ({
|
|
345
|
+
id: c.id,
|
|
346
|
+
depth: c.depth,
|
|
347
|
+
foundAt: c.foundAt,
|
|
348
|
+
source: c.source,
|
|
349
|
+
routesAtDiscovery: c.routesAtDiscovery,
|
|
350
|
+
subscriptions: c.unsubscribe.length,
|
|
351
|
+
})),
|
|
352
|
+
firstContainerAt: stats.firstContainerAt,
|
|
353
|
+
stateEvents: stats.stateEvents,
|
|
354
|
+
evaluations: stats.evaluations,
|
|
355
|
+
actions: { ...stats.actions, byType: { ...stats.actions.byType } },
|
|
356
|
+
indexedKeys: keyIndex.size,
|
|
357
|
+
chains: [...stats.chains],
|
|
358
|
+
costMs: {
|
|
359
|
+
n: stats.costMs.length,
|
|
360
|
+
mean:
|
|
361
|
+
stats.costMs.length === 0
|
|
362
|
+
? null
|
|
363
|
+
: stats.costMs.reduce((s, n) => s + n, 0) / stats.costMs.length,
|
|
364
|
+
p99: percentile(stats.costMs, 99),
|
|
365
|
+
max: percentile(stats.costMs, 100),
|
|
366
|
+
},
|
|
367
|
+
errors: stats.errors,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
let installed = false;
|
|
371
|
+
|
|
372
|
+
export const installNav = () => {
|
|
373
|
+
if (installed) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
installed = true;
|
|
377
|
+
onNavigationContainer({
|
|
378
|
+
added: (fiber, ref, depth) =>
|
|
379
|
+
added(fiber, ref, depth, scanning ? 'scan' : 'mount'),
|
|
380
|
+
removed,
|
|
381
|
+
});
|
|
382
|
+
onAfterCommit(scanOnce);
|
|
383
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Wires the request counter to the app's fetch and XMLHttpRequest. Expo installs
|
|
2
|
+
// its own fetch when the app imports `expo`, after this runs, so the hooks are
|
|
3
|
+
// re-applied once the entry module has loaded, at the first commit and at every
|
|
4
|
+
// window start (a descriptor check when nothing changed).
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
addStartupSection,
|
|
8
|
+
net,
|
|
9
|
+
onAfterCommit,
|
|
10
|
+
onWindowStart,
|
|
11
|
+
settings,
|
|
12
|
+
} from './core';
|
|
13
|
+
import { createNetHooks, type NetHooks } from './net';
|
|
14
|
+
|
|
15
|
+
let hooks: NetHooks | null = null;
|
|
16
|
+
|
|
17
|
+
export const installNet = () => {
|
|
18
|
+
if (hooks) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const installed = createNetHooks({
|
|
22
|
+
target: globalThis as unknown as Record<string, unknown>,
|
|
23
|
+
counter: net,
|
|
24
|
+
enabled: () => settings.net,
|
|
25
|
+
});
|
|
26
|
+
hooks = installed;
|
|
27
|
+
const first = installed.ensure();
|
|
28
|
+
setTimeout(() => installed.ensure(), 0);
|
|
29
|
+
let committed = false;
|
|
30
|
+
onAfterCommit(() => {
|
|
31
|
+
if (!committed) {
|
|
32
|
+
committed = true;
|
|
33
|
+
installed.ensure();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
onWindowStart(() => installed.ensure());
|
|
37
|
+
addStartupSection('net', () => ({
|
|
38
|
+
enabled: settings.net,
|
|
39
|
+
atBoot: first,
|
|
40
|
+
hooks: installed.stats(),
|
|
41
|
+
counter: net.stats(),
|
|
42
|
+
}));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// The SDK's own requests (push client) go through this and are never counted.
|
|
46
|
+
export const ownFetch = (url: string, init: Record<string, unknown>) => {
|
|
47
|
+
const run = () => fetch(url, init as RequestInit);
|
|
48
|
+
return hooks ? hooks.own(run) : run();
|
|
49
|
+
};
|