@keepsnappy/react-native 0.0.0-stage → 0.1.0-alpha.12
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 +237 -0
- package/package.json +26 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// zustand/vanilla in a PerfLab bundle. withKeepSnappy (metro.js) resolves every
|
|
2
|
+
// `zustand/vanilla` request to this module (zustand 5's index, react and
|
|
3
|
+
// traditional entries all load it by that name), and the name below to the real
|
|
4
|
+
// one. Each store's setState goes through the cause tracker's store writer
|
|
5
|
+
// (causes.ts storeWriter), also for the `set` its initializer and middleware
|
|
6
|
+
// get. In a process that does not measure, stores are left as they are.
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var real = require('@keepsnappy/react-native/zustand/vanilla');
|
|
10
|
+
|
|
11
|
+
var wrap = function (createState) {
|
|
12
|
+
return function (set, get, api) {
|
|
13
|
+
var stores = globalThis.__PERFLAB_STORES__;
|
|
14
|
+
if (!stores) {
|
|
15
|
+
return createState(set, get, api);
|
|
16
|
+
}
|
|
17
|
+
var write = stores.writer(set);
|
|
18
|
+
api.setState = write;
|
|
19
|
+
return createState(write, get, api);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
var createStore = function (createState) {
|
|
24
|
+
return createState
|
|
25
|
+
? real.createStore(wrap(createState))
|
|
26
|
+
: function (later) {
|
|
27
|
+
return real.createStore(wrap(later));
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = Object.assign({}, real, { createStore: createStore });
|
package/metro.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// withKeepSnappy(metroConfig): the only line an app adds to metro.config.js.
|
|
2
|
+
//
|
|
3
|
+
// With EXPO_PUBLIC_KEEPSNAPPY=1 it
|
|
4
|
+
// 1. appends the React DevTools hook as a Metro polyfill, so the hook exists
|
|
5
|
+
// before any module (React included) evaluates;
|
|
6
|
+
// 2. wraps the Babel transformer so the app's entry module imports the SDK boot
|
|
7
|
+
// module (Metro only runs pre-main modules that are already in the graph);
|
|
8
|
+
// 3. lists the boot module in getModulesRunBeforeMainModule, so it runs right
|
|
9
|
+
// after InitializeCore and before the entry's own imports.
|
|
10
|
+
// 4. resolves `zustand/vanilla` to js/zustand-vanilla.js, which routes every
|
|
11
|
+
// zustand store's setState through the cause tracker (REQ-MEAS-32 timers).
|
|
12
|
+
// Without the flag it returns the config object untouched.
|
|
13
|
+
//
|
|
14
|
+
// KEEPSNAPPY_REACT_PROFILING=1 (independent of the flag) resolves React Native's
|
|
15
|
+
// production renderer to its profiling build (fiber actualDuration /
|
|
16
|
+
// selfBaseDuration, root.memoizedUpdaters), the same rewrite as
|
|
17
|
+
// @callstack/inspector's withInspector. Nothing else in the bundle changes.
|
|
18
|
+
//
|
|
19
|
+
// KEEPSNAPPY_REACT_PROFILING=switch (flag on only) bundles both Fabric renderers
|
|
20
|
+
// behind js/renderer-switch.js, which loads the profiling one only in a process
|
|
21
|
+
// whose activation asked for it (plan 40 §2.1). Ignored with the flag off.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const POLYFILL_PATH = path.join(__dirname, 'js', 'hook.polyfill.js');
|
|
27
|
+
const BOOT_PATH = path.join(__dirname, 'js', 'sdk', 'boot.ts');
|
|
28
|
+
const TRANSFORMER_PATH = path.join(__dirname, 'js', 'babel-transformer.js');
|
|
29
|
+
|
|
30
|
+
const ENV_UPSTREAM = 'KEEPSNAPPY_UPSTREAM_BABEL_TRANSFORMER';
|
|
31
|
+
const ENV_ENTRY = 'KEEPSNAPPY_ENTRY_FILE';
|
|
32
|
+
const ENV_BOOT = 'KEEPSNAPPY_BOOT_FILE';
|
|
33
|
+
|
|
34
|
+
const isEnabled = () => (process.env.EXPO_PUBLIC_KEEPSNAPPY) === '1';
|
|
35
|
+
const isProfiling = () => (process.env.KEEPSNAPPY_REACT_PROFILING) === '1';
|
|
36
|
+
const isSwitch = () => (process.env.KEEPSNAPPY_REACT_PROFILING) === 'switch';
|
|
37
|
+
|
|
38
|
+
const SWITCH_PATH = path.join(__dirname, 'js', 'renderer-switch.js');
|
|
39
|
+
// The names renderer-switch.js requires; they resolve next to RN's shim.
|
|
40
|
+
const SDK_NAMES = ['@keepsnappy/react-native'];
|
|
41
|
+
const SWITCH_TARGET_PREFIXES = SDK_NAMES.map(name => `${name}/renderer/`);
|
|
42
|
+
const FABRIC_RENDERERS = new Set([
|
|
43
|
+
'../implementations/ReactFabric-prod',
|
|
44
|
+
'../implementations/ReactFabric-prod.js',
|
|
45
|
+
'../implementations/ReactFabric-profiling',
|
|
46
|
+
'../implementations/ReactFabric-profiling.js',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const PROD_RENDERERS = {
|
|
50
|
+
'../implementations/ReactFabric-prod': '../implementations/ReactFabric-profiling',
|
|
51
|
+
'../implementations/ReactFabric-prod.js': '../implementations/ReactFabric-profiling.js',
|
|
52
|
+
'../implementations/ReactNativeRenderer-prod': '../implementations/ReactNativeRenderer-profiling',
|
|
53
|
+
'../implementations/ReactNativeRenderer-prod.js': '../implementations/ReactNativeRenderer-profiling.js',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const profilingModuleName = moduleName => PROD_RENDERERS[moduleName] ?? moduleName;
|
|
57
|
+
|
|
58
|
+
function withReactProfiling(config) {
|
|
59
|
+
const resolver = config.resolver ?? {};
|
|
60
|
+
const upstream = resolver.resolveRequest;
|
|
61
|
+
return {
|
|
62
|
+
...config,
|
|
63
|
+
resolver: {
|
|
64
|
+
...resolver,
|
|
65
|
+
resolveRequest: (context, moduleName, platform) => {
|
|
66
|
+
const rewritten = profilingModuleName(moduleName);
|
|
67
|
+
return upstream
|
|
68
|
+
? upstream(context, rewritten, platform)
|
|
69
|
+
: context.resolveRequest(context, rewritten, platform);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function withRendererSwitch(config) {
|
|
76
|
+
const resolver = config.resolver ?? {};
|
|
77
|
+
const upstream = resolver.resolveRequest;
|
|
78
|
+
const resolve = (context, moduleName, platform) =>
|
|
79
|
+
upstream
|
|
80
|
+
? upstream(context, moduleName, platform)
|
|
81
|
+
: context.resolveRequest(context, moduleName, platform);
|
|
82
|
+
// RN's shim is always resolved before the switch module's own requires.
|
|
83
|
+
let shim = null;
|
|
84
|
+
return {
|
|
85
|
+
...config,
|
|
86
|
+
resolver: {
|
|
87
|
+
...resolver,
|
|
88
|
+
resolveRequest: (context, moduleName, platform) => {
|
|
89
|
+
const switchPrefix = SWITCH_TARGET_PREFIXES.find(prefix => moduleName.startsWith(prefix));
|
|
90
|
+
if (switchPrefix) {
|
|
91
|
+
if (!shim) {
|
|
92
|
+
throw new Error(`KeepSnappy: ${moduleName} required before React Native's renderer shim`);
|
|
93
|
+
}
|
|
94
|
+
const file = moduleName.slice(switchPrefix.length);
|
|
95
|
+
return resolve({ ...context, originModulePath: shim }, `../implementations/${file}`, platform);
|
|
96
|
+
}
|
|
97
|
+
if (FABRIC_RENDERERS.has(moduleName)) {
|
|
98
|
+
shim = context.originModulePath;
|
|
99
|
+
return { type: 'sourceFile', filePath: SWITCH_PATH };
|
|
100
|
+
}
|
|
101
|
+
return resolve(context, moduleName, platform);
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const STORE_PATH = path.join(__dirname, 'js', 'zustand-vanilla.js');
|
|
108
|
+
const STORE_MODULE = 'zustand/vanilla';
|
|
109
|
+
// The name js/zustand-vanilla.js requires; it resolves to the app's own zustand.
|
|
110
|
+
const STORE_TARGETS = SDK_NAMES.map(name => `${name}/${STORE_MODULE}`);
|
|
111
|
+
|
|
112
|
+
// zustand 5 loads its vanilla module by package name from every entry, so one
|
|
113
|
+
// name covers them. There is one wrapper module, so the app must have one
|
|
114
|
+
// zustand copy: a request that resolves to another copy fails the build rather
|
|
115
|
+
// than give that copy's entries a vanilla module of another version.
|
|
116
|
+
function withStoreWrap(config) {
|
|
117
|
+
const resolver = config.resolver ?? {};
|
|
118
|
+
const upstream = resolver.resolveRequest;
|
|
119
|
+
const resolve = (context, moduleName, platform) =>
|
|
120
|
+
upstream
|
|
121
|
+
? upstream(context, moduleName, platform)
|
|
122
|
+
: context.resolveRequest(context, moduleName, platform);
|
|
123
|
+
let real = null;
|
|
124
|
+
return {
|
|
125
|
+
...config,
|
|
126
|
+
resolver: {
|
|
127
|
+
...resolver,
|
|
128
|
+
resolveRequest: (context, moduleName, platform) => {
|
|
129
|
+
if (moduleName === STORE_MODULE) {
|
|
130
|
+
const found = resolve(context, STORE_MODULE, platform);
|
|
131
|
+
if (!real) {
|
|
132
|
+
real = found;
|
|
133
|
+
} else if (found.filePath !== real.filePath) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`KeepSnappy: two zustand copies (${real.filePath}, ${found.filePath}); the KeepSnappy store wrap needs one`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return { type: 'sourceFile', filePath: STORE_PATH };
|
|
139
|
+
}
|
|
140
|
+
if (STORE_TARGETS.includes(moduleName)) {
|
|
141
|
+
if (!real) {
|
|
142
|
+
throw new Error(`KeepSnappy: ${moduleName} required before ${STORE_MODULE}`);
|
|
143
|
+
}
|
|
144
|
+
return real;
|
|
145
|
+
}
|
|
146
|
+
return resolve(context, moduleName, platform);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const SOURCE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
153
|
+
|
|
154
|
+
const resolveEntry = projectRoot => {
|
|
155
|
+
let main = 'index';
|
|
156
|
+
try {
|
|
157
|
+
const pkg = JSON.parse(
|
|
158
|
+
fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'),
|
|
159
|
+
);
|
|
160
|
+
if (typeof pkg.main === 'string') {
|
|
161
|
+
main = pkg.main;
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// no package.json main: Metro's own default is index
|
|
165
|
+
}
|
|
166
|
+
const base = path.resolve(projectRoot, main);
|
|
167
|
+
if (fs.existsSync(base) && fs.statSync(base).isFile()) {
|
|
168
|
+
return base;
|
|
169
|
+
}
|
|
170
|
+
for (const ext of SOURCE_EXTS) {
|
|
171
|
+
if (fs.existsSync(base + ext)) {
|
|
172
|
+
return base + ext;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function withKeepSnappy(input) {
|
|
179
|
+
if (!isEnabled()) {
|
|
180
|
+
return isProfiling() ? withReactProfiling(input) : input;
|
|
181
|
+
}
|
|
182
|
+
let config = input;
|
|
183
|
+
if (isProfiling()) {
|
|
184
|
+
config = withReactProfiling(input);
|
|
185
|
+
} else if (isSwitch()) {
|
|
186
|
+
config = withRendererSwitch(input);
|
|
187
|
+
}
|
|
188
|
+
config = withStoreWrap(config);
|
|
189
|
+
const projectRoot = config.projectRoot ?? process.cwd();
|
|
190
|
+
const entry = resolveEntry(projectRoot);
|
|
191
|
+
const upstream = config.transformer?.babelTransformerPath;
|
|
192
|
+
if (!entry || !upstream) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`KeepSnappy: cannot ${entry ? 'find transformer.babelTransformerPath' : 'resolve the entry file from package.json main'}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Transform workers are forked after the config loads and inherit this env.
|
|
199
|
+
process.env[ENV_UPSTREAM] = upstream;
|
|
200
|
+
process.env[ENV_ENTRY] = entry;
|
|
201
|
+
process.env[ENV_BOOT] = BOOT_PATH;
|
|
202
|
+
|
|
203
|
+
const serializer = config.serializer ?? {};
|
|
204
|
+
const upstreamPolyfills = serializer.getPolyfills;
|
|
205
|
+
const upstreamPreMain = serializer.getModulesRunBeforeMainModule;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
...config,
|
|
209
|
+
serializer: {
|
|
210
|
+
...serializer,
|
|
211
|
+
getPolyfills: options => [
|
|
212
|
+
...(upstreamPolyfills ? upstreamPolyfills(options) : []),
|
|
213
|
+
POLYFILL_PATH,
|
|
214
|
+
],
|
|
215
|
+
getModulesRunBeforeMainModule: entryFilePath => [
|
|
216
|
+
...(upstreamPreMain ? upstreamPreMain(entryFilePath) : []),
|
|
217
|
+
BOOT_PATH,
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
transformer: {
|
|
221
|
+
...config.transformer,
|
|
222
|
+
babelTransformerPath: TRANSFORMER_PATH,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
withKeepSnappy,
|
|
229
|
+
withReactProfiling,
|
|
230
|
+
withRendererSwitch,
|
|
231
|
+
withStoreWrap,
|
|
232
|
+
profilingModuleName,
|
|
233
|
+
STORE_PATH,
|
|
234
|
+
SWITCH_PATH,
|
|
235
|
+
POLYFILL_PATH,
|
|
236
|
+
BOOT_PATH,
|
|
237
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keepsnappy/react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
|
+
"description": "KeepSnappy in-app SDK for React Native / Expo: zero-code measurement windows, React commit hook, native signposts and log file",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/arekkubaczkowski/keepsnappy.git",
|
|
9
|
+
"directory": "packages/react-native"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"app.plugin.js",
|
|
16
|
+
"metro.js",
|
|
17
|
+
"expo-module.config.json",
|
|
18
|
+
"js",
|
|
19
|
+
"ios",
|
|
20
|
+
"android/build.gradle",
|
|
21
|
+
"android/src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"expo": "*",
|
|
26
|
+
"react-native": "*"
|
|
27
|
+
}
|
|
28
|
+
}
|