@keepsnappy/react-native 0.1.0-alpha.12 → 0.1.0-alpha.13

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 CHANGED
@@ -25,14 +25,14 @@ module.exports = withKeepSnappy(config);
25
25
  plugins: [/* ... */, '@keepsnappy/react-native'],
26
26
  ```
27
27
 
28
- Native code needs no further setup: Expo autolinking links the module from `expo-module.config.json` (iOS pod `PerfLab`, Android `expo.modules.perflab.PerfLabModule`) when the flag is on. Set `EXPO_PUBLIC_KEEPSNAPPY` the same way for `expo prebuild` (or the EAS build) and for the JS bundle, then `pod install` / rebuild.
28
+ Native code needs no further setup: Expo autolinking links the module from `expo-module.config.json` (iOS pod `KeepSnappy`, Android `expo.modules.keepsnappy.KeepSnappyModule`) when the flag is on. Set `EXPO_PUBLIC_KEEPSNAPPY` the same way for `expo prebuild` (or the EAS build) and for the JS bundle, then `pod install` / rebuild.
29
29
 
30
30
  ### What the config plugin does
31
31
 
32
32
  It decides whether the native module is linked, from `EXPO_PUBLIC_KEEPSNAPPY` at prebuild time:
33
33
 
34
- - **Flag off:** the package is excluded from Expo autolinking. iOS: the Podfile's `use_expo_modules!` becomes `use_expo_modules!({ exclude: ['<package>'] })` (this also reaches the `expo-configure-project.sh` build phase that regenerates `ExpoModulesProvider.swift`). Android: `expoAutolinking.exclude` is set in `settings.gradle` before `expoAutolinking.useExpoModules()`. `expo-modules-autolinking` 57 reads exclusions only from the app's `package.json` and from these call sites. Result: no `PerfLab` pod, no Android project, no provider entry.
35
- - **Flag on:** the exclusion is removed again (prebuild without `--clean` edits existing files) and `extra.perflab = { native: true, profilingRenderer }` is added to the app config, so flag-on and flag-off builds have different native fingerprints. `profilingRenderer` is `always` (`KEEPSNAPPY_REACT_PROFILING=1`), `switch` (`=switch`) or `off`, read at prebuild time, so the three builds fingerprint differently too. Set `KEEPSNAPPY_REACT_PROFILING` the same way for prebuild and for the bundle.
34
+ - **Flag off:** the package is excluded from Expo autolinking. iOS: the Podfile's `use_expo_modules!` becomes `use_expo_modules!({ exclude: ['<package>'] })` (this also reaches the `expo-configure-project.sh` build phase that regenerates `ExpoModulesProvider.swift`). Android: `expoAutolinking.exclude` is set in `settings.gradle` before `expoAutolinking.useExpoModules()`. `expo-modules-autolinking` 57 reads exclusions only from the app's `package.json` and from these call sites. Result: no `KeepSnappy` pod, no Android project, no provider entry.
35
+ - **Flag on:** the exclusion is removed again (prebuild without `--clean` edits existing files) and `extra.keepsnappy = { native: true, profilingRenderer }` is added to the app config, so flag-on and flag-off builds have different native fingerprints. `profilingRenderer` is `always` (`KEEPSNAPPY_REACT_PROFILING=1`), `switch` (`=switch`) or `off`, read at prebuild time, so the three builds fingerprint differently too. Set `KEEPSNAPPY_REACT_PROFILING` the same way for prebuild and for the bundle.
36
36
 
37
37
  A Podfile whose `use_expo_modules!` already takes arguments (or a settings.gradle without `expoAutolinking.useExpoModules()`) cannot be edited safely; with the flag off, prebuild then fails with a message saying what to add. No Info.plist, entitlement or manifest change is needed: the module only uses `os_signpost`/`OSLog`, `android.os.Trace`, logcat and the app's own files directory.
38
38
 
@@ -52,23 +52,23 @@ All values are read at **bundle time** (Expo CLI loads `.env*` into the Metro pr
52
52
 
53
53
  | Variable (current name) | Proposed public name | Values | Effect |
54
54
  |---|---|---|---|
55
- | `EXPO_PUBLIC_KEEPSNAPPY` | `PERFLAB` | `1` = on, anything else = off | Build flag. Off: Metro config untouched, SDK not in the bundle. On: hook polyfill + SDK boot in the bundle. |
55
+ | `EXPO_PUBLIC_KEEPSNAPPY` | `KEEPSNAPPY` | `1` = on, anything else = off | Build flag. Off: Metro config untouched, SDK not in the bundle. On: hook polyfill + SDK boot in the bundle. |
56
56
  | `EXPO_PUBLIC_KEEPSNAPPY_MODE` | `KEEPSNAPPY_MODE` | `dormant` / `auto` | `auto`: measure from the first frame (same as `KEEPSNAPPY_AUTO=1`). `dormant`: nothing runs until activated (below); once activated the process measures like `auto`. Unset: `auto` if `KEEPSNAPPY_AUTO=1`, else manual (markers until a settings link turns collection on). |
57
57
  | `EXPO_PUBLIC_KEEPSNAPPY_AUTO` | `KEEPSNAPPY_AUTO` | `1` = on | Measure-only builds: collection (React hook walk, PerformanceObserver, log to console and native file) is on from the first frame, with no deep link needed. Off: only markers until a settings link turns collection on. Equivalent to `KEEPSNAPPY_MODE=auto`; `KEEPSNAPPY_MODE` wins when both are set. |
58
58
  | `EXPO_PUBLIC_KEEPSNAPPY_INGEST_URL` | `KEEPSNAPPY_INGEST_URL` | URL of the farm ingest (`POST /v1/ingest`) | Push client. Push is on only if both the URL and the key are set. |
59
- | `EXPO_PUBLIC_KEEPSNAPPY_KEY` | `KEEPSNAPPY_KEY` | ingest key | Sent as `x-perflab-key`. |
59
+ | `EXPO_PUBLIC_KEEPSNAPPY_KEY` | `KEEPSNAPPY_KEY` | ingest key | Sent as `x-keepsnappy-key`. |
60
60
  | `EXPO_PUBLIC_KEEPSNAPPY_SAMPLER` | `KEEPSNAPPY_SAMPLER` | `auto` / `manual` / `off` | Native resource sampler default. Unset: `auto` when `KEEPSNAPPY_AUTO=1`, else `off`. |
61
61
  | `EXPO_PUBLIC_KEEPSNAPPY_SAMPLER_MS` | `KEEPSNAPPY_SAMPLER_MS` | ms, 250–60000 | Auto sampling interval (default 4000; windows also get a sample at open and at close). |
62
62
  | `EXPO_PUBLIC_KEEPSNAPPY_PROFILE` | `KEEPSNAPPY_PROFILE` | `1` = on | Default of `profile`: per-component update causes per window (any renderer), plus self/total ms and updaters with the profiling renderer (`react.profile`). |
63
- | `EXPO_PUBLIC_KEEPSNAPPY_REACT_EXPORT` | `KEEPSNAPPY_REACT_EXPORT` | `1` = on | Default of `rexport`: a React DevTools v5 export per window (`Documents/perf-lab/react-<process>-<window id>-<name>.json`, TraceSift's input). Inside a run (a run token is set) the files are held and written at `flush=1`; otherwise at window end. |
64
- | `EXPO_PUBLIC_KEEPSNAPPY_CPUPROF`, `..._CPUPROF_HZ` | `KEEPSNAPPY_CPUPROF` | `1` = on; Hz (default 250) | Default of `cpuprof`/`cpuhz`: Hermes sampling profiler (iOS), `Documents/perf-lab/cpu-<process>-<span>.cpuprofile`. Inside a run it samples from the first window to `flush=1` and stops and dumps there, so the synchronous dump never lands in a measured window; otherwise one file per span of overlapping windows, dumped when the last one ends. `<process>` is the launch's `processId` (startup line), so launches never overwrite each other. |
63
+ | `EXPO_PUBLIC_KEEPSNAPPY_REACT_EXPORT` | `KEEPSNAPPY_REACT_EXPORT` | `1` = on | Default of `rexport`: a React DevTools v5 export per window (`Documents/keepsnappy/react-<process>-<window id>-<name>.json`, TraceSift's input). Inside a run (a run token is set) the files are held and written at `flush=1`; otherwise at window end. |
64
+ | `EXPO_PUBLIC_KEEPSNAPPY_CPUPROF`, `..._CPUPROF_HZ` | `KEEPSNAPPY_CPUPROF` | `1` = on; Hz (default 250) | Default of `cpuprof`/`cpuhz`: Hermes sampling profiler (iOS), `Documents/keepsnappy/cpu-<process>-<span>.cpuprofile`. Inside a run it samples from the first window to `flush=1` and stops and dumps there, so the synchronous dump never lands in a measured window; otherwise one file per span of overlapping windows, dumped when the last one ends. `<process>` is the launch's `processId` (startup line), so launches never overwrite each other. |
65
65
  | `KEEPSNAPPY_REACT_PROFILING` (Metro process; also read by the config plugin) | | `1` / `switch` / unset | `1`: `withKeepSnappy` resolves RN's production renderer to `ReactFabric-profiling` (fiber durations, updaters), flag or not; for dedicated perf builds (docs/plans/37). `switch` (flag on only, ignored with the flag off): both Fabric renderers go into the bundle and each process loads one at launch (below). Unset: production renderer only. |
66
66
 
67
- Only the names above are read today. The proposed names drop the `EXPO_PUBLIC_` prefix because the values never need to be readable by app code: the transformer inlines them into the SDK only, so a non-prefixed `PERFLAB=1` would also keep the flag out of the app's own `process.env` surface and match the CLI / CI wording (`PERFLAB=1` in doc 23). Migration plan: accept both names (new name wins), then drop the old ones.
67
+ Only the names above are read today. The proposed names drop the `EXPO_PUBLIC_` prefix because the values never need to be readable by app code: the transformer inlines them into the SDK only, so a non-prefixed `KEEPSNAPPY=1` would also keep the flag out of the app's own `process.env` surface and match the CLI / CI wording (`KEEPSNAPPY=1` in doc 23). Migration plan: accept both names (new name wins), then drop the old ones.
68
68
 
69
69
  ### Renderer switch (`KEEPSNAPPY_REACT_PROFILING=switch`)
70
70
 
71
- For a shared build (plan 40 §2.1): `withKeepSnappy` resolves `ReactFabric-prod` and `ReactFabric-profiling` to `js/renderer-switch.js`, which `require`s the profiling renderer when `__PERFLAB_ACTIVATION__.profile` is true and the production one otherwise. Both are in the bundle; only the chosen one is evaluated. The choice is made once, before React loads:
71
+ For a shared build (plan 40 §2.1): `withKeepSnappy` resolves `ReactFabric-prod` and `ReactFabric-profiling` to `js/renderer-switch.js`, which `require`s the profiling renderer when `__KEEPSNAPPY_ACTIVATION__.profile` is true and the production one otherwise. Both are in the bundle; only the chosen one is evaluated. The choice is made once, before React loads:
72
72
 
73
73
  - **dormant build:** the activation's `profile` (link `activate=1&profile=1`), read by the hook polyfill. A process that is not activated, or whose activation expired, runs the production renderer, as without the switch. If the polyfill cannot reach the native module (Android may install `expo.modules` lazily), the boot module reads the activation and sets `profile` before the renderer loads;
74
74
  - **auto / manual build:** `EXPO_PUBLIC_KEEPSNAPPY_PROFILE=1`. The runtime `profile=1` link turns causes and render times on, but cannot change the renderer of a running process.
@@ -84,21 +84,21 @@ A dormant process installs nothing: `__REACT_DEVTOOLS_GLOBAL_HOOK__` is left as
84
84
 
85
85
  Activation is by settings link and **applies from the next launch**, never in the running process, so a process is wholly measured or wholly dormant and carries exactly one run token:
86
86
 
87
- | Link | Writes `perf-lab/activation.json` |
87
+ | Link | Writes `keepsnappy/activation.json` |
88
88
  |---|---|
89
- | `devtools/perf-lab?activate=1&runToken=<pl_rt_…>&ttl=<s>&diag=1&profile=1&cpuprof=1&cpuhz=<Hz>` | `{ active: true, runToken, expiresAt, diag, profile, cpuprof }`; `ttl` default 7200 s, max 21600 s (the run token's own life); `diag` (default off) marks the launch as a diagnostic iteration; `profile` (default off) loads the profiling renderer in a switch build and turns causes and render times on; `cpuprof` (default off) stores a Hermes sampling rate in Hz: `1`/`true` means `cpuhz` (default 250), a number above 1 is the rate itself, clamped to 10–10000 |
90
- | `devtools/perf-lab?activate=0` | `{ active: false }`, every field off |
91
- | `devtools/perf-lab?runToken=<t>` | keeps the current activation, replaces the token (empty clears it); keeps `diag`, `profile` and `cpuprof` unless the link sets them |
89
+ | `devtools/keepsnappy?activate=1&runToken=<pl_rt_…>&ttl=<s>&diag=1&profile=1&cpuprof=1&cpuhz=<Hz>` | `{ active: true, runToken, expiresAt, diag, profile, cpuprof }`; `ttl` default 7200 s, max 21600 s (the run token's own life); `diag` (default off) marks the launch as a diagnostic iteration; `profile` (default off) loads the profiling renderer in a switch build and turns causes and render times on; `cpuprof` (default off) stores a Hermes sampling rate in Hz: `1`/`true` means `cpuhz` (default 250), a number above 1 is the rate itself, clamped to 10–10000 |
90
+ | `devtools/keepsnappy?activate=0` | `{ active: false }`, every field off |
91
+ | `devtools/keepsnappy?runToken=<t>` | keeps the current activation, replaces the token (empty clears it); keeps `diag`, `profile` and `cpuprof` unless the link sets them |
92
92
 
93
93
  The launch that reads the record applies `profile` and `cpuprof` for its whole life (they switch the settings of the same name on; off leaves the build's env defaults). Records written before these fields existed read as both off.
94
94
 
95
- An expired activation is dormant. Uninstall and `clearState` remove the file. In a dormant (not activated) process the link wrapper only handles these parameters and `dump=1`, which answers with one `PERF_LAB dormant {json}` line on os_log / logcat (no file): hook, `fetch`/XHR wrapping, SDK global and link patch status. In auto and manual builds a `runToken` link applies at once, as before, and is also persisted, so a relaunched process keeps it until it expires.
95
+ An expired activation is dormant. Uninstall and `clearState` remove the file. In a dormant (not activated) process the link wrapper only handles these parameters and `dump=1`, which answers with one `KEEPSNAPPY dormant {json}` line on os_log / logcat (no file): hook, `fetch`/XHR wrapping, SDK global and link patch status. In auto and manual builds a `runToken` link applies at once, as before, and is also persisted, so a relaunched process keeps it until it expires.
96
96
 
97
97
  Launch arguments as a second activation path are not implemented.
98
98
 
99
99
  ### Settings link (runtime)
100
100
 
101
- `<app scheme>://devtools/perf-lab?<params>` is intercepted before the router sees it (`Linking.getInitialURL`, `Linking.addEventListener('url')`, `expo-linking`'s `getLinkingURL` and `getInitialURL` are wrapped), so it never navigates the app.
101
+ `<app scheme>://devtools/keepsnappy?<params>` is intercepted before the router sees it (`Linking.getInitialURL`, `Linking.addEventListener('url')`, `expo-linking`'s `getLinkingURL` and `getInitialURL` are wrapped), so it never navigates the app.
102
102
 
103
103
  | Param | Values | Meaning |
104
104
  |---|---|---|
@@ -110,8 +110,8 @@ Launch arguments as a second activation path are not implemented.
110
110
  | `runToken` | string, empty clears | Correlates pushed batches with a farm run (redacted in logs). Persisted; in a dormant build it applies from the next launch only |
111
111
  | `activate`, `ttl` | `1`/`0`, seconds | Activation of a dormant build, from the next launch (see above) |
112
112
  | `gc` | `1` | Force a Hermes GC now and log heap before/after |
113
- | `flush` | `1` | End the open windows and send every unsent line now; result as the `perflab.flushed` element (see Push). Ignored by a dormant process |
114
- | `dump` | `1` | Write a `startup` report now (its `activation` section has `mode: active`, the build mode and the next launch's activation); in a dormant process: the `PERF_LAB dormant` line |
113
+ | `flush` | `1` | End the open windows and send every unsent line now; result as the `keepsnappy.flushed` element (see Push). Ignored by a dormant process |
114
+ | `dump` | `1` | Write a `startup` report now (its `activation` section has `mode: active`, the build mode and the next launch's activation); in a dormant process: the `KEEPSNAPPY dormant` line |
115
115
  | `sampler` | `auto` / `manual` / `off` | Native resource sampler mode |
116
116
  | `samplerMs` | 250–60000 | Auto sampling interval |
117
117
  | `sample` | `1` | Take one sample now (ignored when the sampler is off) |
@@ -126,23 +126,23 @@ With `activate=1` (or a bare `runToken`), `profile`, `cpuprof` and `cpuhz` are a
126
126
 
127
127
  With `EXPO_PUBLIC_KEEPSNAPPY_INGEST_URL` and `EXPO_PUBLIC_KEEPSNAPPY_KEY` set, every window line is queued for `POST /v1/windows`.
128
128
 
129
- - **Measured runs** (a dormant build that was activated, or any process started with a run token): nothing is sent while the run is active. Lines go to the log file and the push journal only. At the end of the iteration the flow opens `devtools/perf-lab?flush=1`: the SDK ends the open windows, sends every unsent line (retrying a failed batch up to 3 times) and then shows the result as an accessibility element for the driver, id `perflab.flushed`, label `ok` / `error` / `off` (no ingest configured), value with batch and line counts or the error. It is a transparent 1x1 pt view on the key window (iOS), created only on request in a measuring process; Android only logs `PERF_LAB flushed <status>` for now. A dormant process ignores `flush`.
129
+ - **Measured runs** (a dormant build that was activated, or any process started with a run token): nothing is sent while the run is active. Lines go to the log file and the push journal only. At the end of the iteration the flow opens `devtools/keepsnappy?flush=1`: the SDK ends the open windows, sends every unsent line (retrying a failed batch up to 3 times) and then shows the result as an accessibility element for the driver, id `keepsnappy.flushed`, label `ok` / `error` / `off` (no ingest configured), value with batch and line counts or the error. It is a transparent 1x1 pt view on the key window (iOS), created only on request in a measuring process; Android only logs `KEEPSNAPPY flushed <status>` for now. A dormant process ignores `flush`.
130
130
  - **Other builds:** idle flush after 10 s, never while a window is open, and when the app goes to the background, as before.
131
131
 
132
132
  Maestro, end of an iteration:
133
133
 
134
134
  ```yaml
135
- - openLink: <scheme>://devtools/perf-lab?flush=1&activate=0
135
+ - openLink: <scheme>://devtools/keepsnappy?flush=1&activate=0
136
136
  - extendedWaitUntil:
137
137
  visible:
138
- id: perflab.flushed
138
+ id: keepsnappy.flushed
139
139
  timeout: 60000
140
140
  - assertVisible:
141
- id: perflab.flushed
141
+ id: keepsnappy.flushed
142
142
  text: ok
143
143
  ```
144
144
 
145
- The run token is stamped on each line when it is queued, and a batch never mixes tokens, so a window belongs to the run that was current when it was recorded. Every queued line is also journalled in `perf-lab/push-spool.jsonl` (with its token); delivered lines are marked done and the journal is emptied whenever the queue drains. A process that starts pushing first queues what earlier processes recorded and never sent, ahead of its own lines and with their own tokens. A batch id names exactly its lines (process, first and last sequence, count), and the batch is journalled before it is first sent, so a later process resends the same lines under the same id: the server drops a replay it already stored and never drops lines it does not have. The journal is read at the first idle send or at the `flush=1` link, never at startup or inside a measured window, and is rewritten to the live queue whenever it passes twice the queue caps; lines dropped by the cap are counted (`dropped`, `resumeDropped`) across processes. On Android, `activation.json` and the journal live in internal storage (`filesDir`); only logs and dumps go to external storage. An install from before that move has both files moved in at the first state access, and the external copies deleted.
145
+ The run token is stamped on each line when it is queued, and a batch never mixes tokens, so a window belongs to the run that was current when it was recorded. Every queued line is also journalled in `keepsnappy/push-spool.jsonl` (with its token); delivered lines are marked done and the journal is emptied whenever the queue drains. A process that starts pushing first queues what earlier processes recorded and never sent, ahead of its own lines and with their own tokens. A batch id names exactly its lines (process, first and last sequence, count), and the batch is journalled before it is first sent, so a later process resends the same lines under the same id: the server drops a replay it already stored and never drops lines it does not have. The journal is read at the first idle send or at the `flush=1` link, never at startup or inside a measured window, and is rewritten to the live queue whenever it passes twice the queue caps; lines dropped by the cap are counted (`dropped`, `resumeDropped`) across processes. On Android, `activation.json` and the journal live in internal storage (`filesDir`); only logs and dumps go to external storage. An install from before that move has both files moved in at the first state access, and the external copies deleted.
146
146
 
147
147
  ### Profile files of a run
148
148
 
@@ -159,7 +159,7 @@ Inside a run, at `flush=1` the SDK keeps the raw files plan 40 §3.3 asks for:
159
159
  - in a diagnostic iteration (`diag`), every `.cpuprofile` and React export;
160
160
  - otherwise only the files that cover a window with a commit over the device frame budget or a long task > 50 ms.
161
161
 
162
- It lists them in `perf-lab/artifacts-<processId>.json` and, after the lines, uploads them: a signed URL from `POST /v1/artifacts`, then a gzip PUT by the native module, then `complete` (contract in plan 40 §3.6). It never uploads while a window is open. A missing endpoint (404), a missing native function (older binary) or no ingest config leaves the files on the device with the reason in the manifest. The flush still ends normally, and its detail gets `artifacts=<uploaded>/<total>`. Each file goes up under the run token that was current when it was recorded, also when a later link changed the token (the token is kept in memory, never in the manifest). The native upload streams the gzip through a temp file on Android, maps the file on iOS, and skips a file over the API's 64 MiB cap without reading it.
162
+ It lists them in `keepsnappy/artifacts-<processId>.json` and, after the lines, uploads them: a signed URL from `POST /v1/artifacts`, then a gzip PUT by the native module, then `complete` (contract in plan 40 §3.6). It never uploads while a window is open. A missing endpoint (404), a missing native function (older binary) or no ingest config leaves the files on the device with the reason in the manifest. The flush still ends normally, and its detail gets `artifacts=<uploaded>/<total>`. Each file goes up under the run token that was current when it was recorded, also when a later link changed the token (the token is kept in memory, never in the manifest). The native upload streams the gzip through a temp file on Android, maps the file on iOS, and skips a file over the API's 64 MiB cap without reading it.
163
163
 
164
164
  ### What caused the work (REQ-MEAS-32)
165
165
 
@@ -168,16 +168,16 @@ A window ends at the next action, so it also catches work the test only waits th
168
168
  - A cause follows the code it started through timers, animation frames, microtasks, `Promise.prototype.then` (so `await` on Hermes), `XMLHttpRequest` responses, WebSocket open/close, React's scheduler tasks and the first `onLayout`/`onLoad` of the views it mounted. Scrolling, momentum, gesture-handler, refresh and slider events belong to the latest touch while it is the newest action (after a deep link they have no cause until the next touch: a push can scroll the list of the screen the link opened); other host events, including the pager-view page events, have no cause.
169
169
  - A commit is foreign to a window when every cause is a push or an action older than the window's anchor (the newest action when the window started). It then counts in `react.external.<source>` (`commits`, `renderMs`) instead of every other `react.*` field. Anything else, including work with no cause and other native events, stays in the window.
170
170
  - Foreign work that no open window takes, including a back-dated window that opens later, goes to a window `background:<source>` (kind `background`: `websocket`, `launch`, `link`, `touch`), open from its first commit until the app leaves the foreground or the flush. A commit is routed there only when every window ends (app background or flush), so a back-dated window that opens later still takes it however many commits came after; the recent-commit ring bounds only `react.backfill` of judged work. The foreign commits that left the ring wait in memory until that end, each with its profile and, with the React export on, its exported fibers: on the S23 TapIn "join live" iteration that is about 71 push commits (the chat burst, 6 A/A exports), and the flush at the end of every iteration empties the list. It has `react` only.
171
- - The React export marks each commit with `perflabCauses`; every window carries `react.causes.awaitThroughThen`.
172
- - A timer callback that runs after a newer action than the one its registration carries is a carried tick: its work keeps the old cause (so it stays foreign to later windows) and is counted again in `react.external.<source>.timerCarried`, the upper bound of work a queue-serving timer did for the open window. Every run of the callback is its own tick, for repeating and re-registering timers alike; a run that starts a request (itself or in a continuation) is `timer-net` while the request counter runs (`react.causes.requestsMarked`). The export marks each commit with `perflabCarrier`.
173
- - A zustand store write made under a carried tick, to a store whose last write came from a newer action, runs under that action (`withKeepSnappy` resolves `zustand/vanilla` to `js/zustand-vanilla.js`, which routes every store's `setState` through the tracker): a timer that serves a queue does the work of the action that queued it. A store remembers the newest action that wrote it. The export marks such commits with `perflabStore` (`touch#3>touch#9`) and `perflabTask` (their causes and carrier without the move); the app must have one zustand copy; `react.causes.storesWrapped` says how many stores the wrapper saw (0: none can move).
171
+ - The React export marks each commit with `keepsnappyCauses`; every window carries `react.causes.awaitThroughThen`.
172
+ - A timer callback that runs after a newer action than the one its registration carries is a carried tick: its work keeps the old cause (so it stays foreign to later windows) and is counted again in `react.external.<source>.timerCarried`, the upper bound of work a queue-serving timer did for the open window. Every run of the callback is its own tick, for repeating and re-registering timers alike; a run that starts a request (itself or in a continuation) is `timer-net` while the request counter runs (`react.causes.requestsMarked`). The export marks each commit with `keepsnappyCarrier`.
173
+ - A zustand store write made under a carried tick, to a store whose last write came from a newer action, runs under that action (`withKeepSnappy` resolves `zustand/vanilla` to `js/zustand-vanilla.js`, which routes every store's `setState` through the tracker): a timer that serves a queue does the work of the action that queued it. A store remembers the newest action that wrote it. The export marks such commits with `keepsnappyStore` (`touch#3>touch#9`) and `keepsnappyTask` (their causes and carrier without the move); the app must have one zustand copy; `react.causes.storesWrapped` says how many stores the wrapper saw (0: none can move).
174
174
  - The commit stream (`js/sdk/stream.ts`) records every commit of the process once, with its cause facts (root ids, timer runs, store writes, render time or `null`) and the tables of roots, runs and writes, in segment files `stream-<process>-<seq>.json` kept in every run (artifact kind `commit-stream`). The backend replays the split from it; until it does, the window values above are the SDK's own.
175
175
 
176
176
  Known limits: a server reply to a tap that arrives over the WebSocket is foreign, and so is a screen the app opens from a socket handler; direct JSI calls into JS (Reanimated `runOnJS`) carry no cause and stay in the window.
177
177
 
178
178
  ### Native resource sampler
179
179
 
180
- Off unless enabled. `auto` samples every `samplerMs` on a native utility thread (no JS work); `manual` keeps only the frame counter running and samples on request (`sample=1` link or `__PERF_LAB__.sample()`), plus one sample when each window opens and one when it closes in `auto` and `manual`. Every sample is one `PERF_LAB sample {json}` line in the native log (os_log + `Documents/perf-lab/log.txt` on iOS, logcat on Android), whatever the `log` sink. Windows get a `sampler` object (footprint start/end/delta, CPU cores, frames missed, longest frame gap) when samples bracket them. Record format and limits: `docs/research/31-native-sampler.md`.
180
+ Off unless enabled. `auto` samples every `samplerMs` on a native utility thread (no JS work); `manual` keeps only the frame counter running and samples on request (`sample=1` link or `__KEEPSNAPPY__.sample()`), plus one sample when each window opens and one when it closes in `auto` and `manual`. Every sample is one `KEEPSNAPPY sample {json}` line in the native log (os_log + `Documents/keepsnappy/log.txt` on iOS, logcat on Android), whatever the `log` sink. Windows get a `sampler` object (footprint start/end/delta, CPU cores, frames missed, longest frame gap) when samples bracket them. Record format and limits: `docs/research/31-native-sampler.md`.
181
181
 
182
182
  ### Network requests
183
183
 
@@ -203,7 +203,7 @@ The first screen window of the process is the launch screen (REQ-MEAS-22): it st
203
203
 
204
204
  ### App log lines
205
205
 
206
- `globalThis.__PERF_LAB__?.log(message)` writes `PERF_LAB app <message>` to the native log (and the console), the same file and stream as the SDK's own lines, whatever the `log` sink. `__PERF_LAB__` exists only when the SDK is in the bundle.
206
+ `globalThis.__KEEPSNAPPY__?.log(message)` writes `KEEPSNAPPY app <message>` to the native log (and the console), the same file and stream as the SDK's own lines, whatever the `log` sink. `__KEEPSNAPPY__` exists only when the SDK is in the bundle.
207
207
 
208
208
  ## Layout
209
209
 
@@ -212,9 +212,9 @@ The first screen window of the process is the launch screen (REQ-MEAS-22): it st
212
212
  | `metro.js` | `@keepsnappy/react-native/metro`: `withKeepSnappy(config)` |
213
213
  | `app.plugin.js` | Expo config plugin: excludes the native module from autolinking when the flag is off |
214
214
  | `expo-module.config.json` | Expo autolinking for the native module |
215
- | `ios/PerfLab.podspec`, `ios/PerfLabModule.swift` | iOS: signposts, OSLog, log file in Documents/perf-lab |
216
- | `ios/PerfLabSampler.swift` | iOS: resource sampler (task_vm_info, thread CPU, CADisplayLink frames) |
217
- | `android/build.gradle`, `android/src/...` | Android: `Trace` sections, logcat, log file, resource sampler (`PerfLabSampler.kt`, untested on device) |
215
+ | `ios/KeepSnappy.podspec`, `ios/KeepSnappyModule.swift` | iOS: signposts, OSLog, log file in Documents/keepsnappy |
216
+ | `ios/KeepSnappySampler.swift` | iOS: resource sampler (task_vm_info, thread CPU, CADisplayLink frames) |
217
+ | `android/build.gradle`, `android/src/...` | Android: `Trace` sections, logcat, log file, resource sampler (`KeepSnappySampler.kt`, untested on device) |
218
218
  | `js/sdk/sampler.ts` | Sampler settings parsing and per-window aggregation (pure) |
219
219
  | `js/sdk/net.ts`, `net-install.ts` | Request counter and fetch/XHR hooks (pure), and their wiring |
220
220
  | `js/hook.polyfill.js` | React DevTools hook (Metro polyfill, ES5, no `require`) |
@@ -234,7 +234,7 @@ The package has no JS entry point (`main`) on purpose: app code never imports it
234
234
  ## Local development
235
235
 
236
236
  ```sh
237
- bun install # at the perflab repo root (workspace)
237
+ bun install # at the keepsnappy repo root (workspace)
238
238
  bun run typecheck # in packages/react-native: SDK (tsconfig.json) + tests (tsconfig.test.json)
239
239
  bun test
240
240
  ```
@@ -247,7 +247,7 @@ The app consumes a packed tarball, the same artifact npm would serve:
247
247
  # in packages/react-native
248
248
  bun run pack:local # -> dist/keepsnappy-react-native-0.1.0.tgz (dist/ is gitignored)
249
249
  # in the app
250
- bun add @keepsnappy/react-native@file:../perflab/packages/react-native/dist/keepsnappy-react-native-0.1.0.tgz
250
+ bun add @keepsnappy/react-native@file:../keepsnappy/packages/react-native/dist/keepsnappy-react-native-0.1.0.tgz
251
251
  ```
252
252
 
253
253
  After editing the package, run both commands again (re-adding refreshes the integrity hash in `bun.lock`), then rebuild the JS bundle with `--reset-cache`; `pod install` only when native files changed. Bun 1.4 caches a `file:` tarball by path under `~/.bun/install/cache/@T@<hash>@@@1` and reuses it while the version is unchanged, so a repack with the same version can install the old contents: delete those entries (their `package.json` names the package), install with a fresh cache (`BUN_INSTALL_CACHE_DIR=$(mktemp -d) bun add …`, which also refreshes the `bun.lock` integrity) or bump the version, and check with `diff -r node_modules/@keepsnappy/react-native/js <repo>/packages/react-native/js`.
@@ -256,6 +256,6 @@ Why not a folder `file:` dependency or `bun link`: for a folder outside the app,
256
256
 
257
257
  ## Store builds
258
258
 
259
- With the flag off the JS bundle carries no SDK code (check: `grep -c __PERFLAB_HOOK__ main.jsbundle` is 0) and, after a prebuild with the flag off, the binary carries no SDK native code (no `PerfLab` in `Podfile.lock`, in `ExpoModulesProvider.swift` or in the app binary's symbols).
259
+ With the flag off the JS bundle carries no SDK code (check: `grep -c __KEEPSNAPPY_HOOK__ main.jsbundle` is 0) and, after a prebuild with the flag off, the binary carries no SDK native code (no `KeepSnappy` in `Podfile.lock`, in `ExpoModulesProvider.swift` or in the app binary's symbols).
260
260
 
261
- Use `EXPO_PUBLIC_KEEPSNAPPY*`, `KEEPSNAPPY_*`, and `withKeepSnappy`. Previous build environment names and `withPerfLab` are no longer supported. Native module identities, stored records and wire formats are unchanged.
261
+ Use `EXPO_PUBLIC_KEEPSNAPPY*`, `KEEPSNAPPY_*`, and `withKeepSnappy`. Previous build environment names and `withKeepSnappy` are no longer supported. Native module identities, stored records and wire formats are unchanged.
@@ -2,11 +2,11 @@ apply plugin: 'com.android.library'
2
2
  apply plugin: 'kotlin-android'
3
3
  apply plugin: 'expo-module-gradle-plugin'
4
4
 
5
- group = 'expo.modules.perflab'
5
+ group = 'expo.modules.keepsnappy'
6
6
  version = '1.0.0'
7
7
 
8
8
  android {
9
- namespace "expo.modules.perflab"
9
+ namespace "expo.modules.keepsnappy"
10
10
  compileSdk safeExtGet("compileSdkVersion", 34)
11
11
 
12
12
  defaultConfig {
@@ -1,4 +1,4 @@
1
- package expo.modules.perflab
1
+ package expo.modules.keepsnappy
2
2
 
3
3
  import android.app.Activity
4
4
  import android.content.BroadcastReceiver
@@ -31,11 +31,11 @@ import java.util.concurrent.ExecutorService
31
31
  import java.util.concurrent.Executors
32
32
  import java.util.zip.GZIPOutputStream
33
33
 
34
- private const val TAG = "PERF_LAB"
34
+ private const val TAG = "KEEPSNAPPY"
35
35
 
36
36
  // The element the test driver waits for after the flush link; same id as on iOS.
37
- private const val FLUSH_ELEMENT_ID = "perflab.flushed"
38
- private const val FLUSH_ELEMENT_TAG = "perflab.flushed.view"
37
+ private const val FLUSH_ELEMENT_ID = "keepsnappy.flushed"
38
+ private const val FLUSH_ELEMENT_TAG = "keepsnappy.flushed.view"
39
39
  private const val FLUSH_ELEMENT_DP = 8
40
40
  // Alpha 1 of 255: drawn, so it counts as visible, but no pixel changes on screen.
41
41
  private const val FLUSH_ELEMENT_BACKGROUND = 0x01000000
@@ -58,7 +58,7 @@ private val LEGACY_STATE_FILES = listOf("activation.json", "push-spool.jsonl")
58
58
  // do work before a function is called (a dormant build calls activationRead only).
59
59
  // UNTESTED ON DEVICE (activation, spool): compiled only, like the sampler.
60
60
  // NOT YET COMPILED: moveLegacyState and the streamed uploadArtifact (night of 2026-10-14).
61
- class PerfLabModule : Module() {
61
+ class KeepSnappyModule : Module() {
62
62
  private val mainHandler = Handler(Looper.getMainLooper())
63
63
 
64
64
  // GC before the action: the driver's command arrives as a broadcast, which pauses no activity (a link does, and the app
@@ -84,7 +84,7 @@ class PerfLabModule : Module() {
84
84
  resultData = "bad command"
85
85
  return
86
86
  }
87
- val tNative = perfLabMonotonicMs()
87
+ val tNative = keepSnappyMonotonicMs()
88
88
  pendingCommands.remove(n)?.finish()
89
89
  val pending = goAsync()
90
90
  pendingCommands[n] = pending
@@ -94,22 +94,22 @@ class PerfLabModule : Module() {
94
94
  pending.finish()
95
95
  }
96
96
  }, commandDeadlineMs)
97
- sendEvent("perflabCommand", mapOf("cmd" to cmd, "n" to n, "tNative" to tNative))
97
+ sendEvent("keepsnappyCommand", mapOf("cmd" to cmd, "n" to n, "tNative" to tNative))
98
98
  }
99
99
  }
100
- val filter = IntentFilter("${context.packageName}.perflab.COMMAND")
100
+ val filter = IntentFilter("${context.packageName}.keepsnappy.COMMAND")
101
101
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
102
102
  context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
103
103
  } else {
104
104
  context.registerReceiver(receiver, filter)
105
105
  }
106
106
  commandReceiver = receiver
107
- writeLog("PERF_LAB command receiver on")
107
+ writeLog("KEEPSNAPPY command receiver on")
108
108
  } else if (!on && commandReceiver != null) {
109
109
  try { context.unregisterReceiver(commandReceiver) } catch (e: Exception) {}
110
110
  commandReceiver = null
111
111
  for (n in pendingCommands.keys.toList()) pendingCommands.remove(n)?.finish()
112
- writeLog("PERF_LAB command receiver off")
112
+ writeLog("KEEPSNAPPY command receiver off")
113
113
  }
114
114
  }
115
115
  }
@@ -117,7 +117,7 @@ class PerfLabModule : Module() {
117
117
  private fun showFlushElement(status: String) {
118
118
  val activity = appContext.currentActivity
119
119
  if (activity == null) {
120
- writeLog("PERF_LAB flush element not shown: no foreground activity ($status)")
120
+ writeLog("KEEPSNAPPY flush element not shown: no foreground activity ($status)")
121
121
  return
122
122
  }
123
123
  mainHandler.post {
@@ -143,17 +143,17 @@ class PerfLabModule : Module() {
143
143
  private var samplerCreated = false
144
144
  private val sampler by lazy {
145
145
  samplerCreated = true
146
- PerfLabSampler({ appContext.reactContext }, { appContext.currentActivity }, ::writeLog)
146
+ KeepSnappySampler({ appContext.reactContext }, { appContext.currentActivity }, ::writeLog)
147
147
  }
148
148
  private val io: ExecutorService by lazy {
149
- Executors.newSingleThreadExecutor { r -> Thread(r, "perflab-io") }
149
+ Executors.newSingleThreadExecutor { r -> Thread(r, "keepsnappy-io") }
150
150
  }
151
151
 
152
152
  // activation.json and the push journal (run tokens) live in internal storage: on Android 9
153
153
  // and older another app with storage permission could read or plant files in external storage.
154
154
  private fun stateDir(create: Boolean): File? {
155
155
  val context = appContext.reactContext ?: return null
156
- val dir = File(context.filesDir ?: return null, "perf-lab")
156
+ val dir = File(context.filesDir ?: return null, "keepsnappy")
157
157
  moveLegacyState(context, dir)
158
158
  if (create) dir.mkdirs()
159
159
  return dir
@@ -170,7 +170,7 @@ class PerfLabModule : Module() {
170
170
  if (legacyChecked) return
171
171
  legacyChecked = true
172
172
  val external = try { context.getExternalFilesDir(null) } catch (e: Exception) { null }
173
- val old = external?.let { File(it, "perf-lab") } ?: return
173
+ val old = external?.let { File(it, "keepsnappy") } ?: return
174
174
  for (name in LEGACY_STATE_FILES) {
175
175
  val from = File(old, name)
176
176
  if (!from.exists()) continue
@@ -217,7 +217,7 @@ class PerfLabModule : Module() {
217
217
  private fun uploadArtifact(fileName: String, url: String, headers: Map<String, String>, timeoutMs: Double): Map<String, Any?> {
218
218
  val context = appContext.reactContext
219
219
  val base = context?.getExternalFilesDir(null)
220
- val file = base?.let { File(File(it, "perf-lab"), fileName.replace('/', '_')) }
220
+ val file = base?.let { File(File(it, "keepsnappy"), fileName.replace('/', '_')) }
221
221
  if (context == null || file == null || !file.exists()) {
222
222
  return mapOf("status" to 0, "error" to "file missing")
223
223
  }
@@ -226,7 +226,7 @@ class PerfLabModule : Module() {
226
226
  return mapOf("status" to 413, "error" to "file is $length bytes, over $ARTIFACT_MAX_BYTES")
227
227
  }
228
228
  val gzFile = try {
229
- File.createTempFile("perflab-upload-", ".gz", context.cacheDir)
229
+ File.createTempFile("keepsnappy-upload-", ".gz", context.cacheDir)
230
230
  } catch (e: Exception) {
231
231
  return mapOf("status" to 0, "bytes" to length.toInt(), "error" to "temp file: $e")
232
232
  }
@@ -282,9 +282,9 @@ class PerfLabModule : Module() {
282
282
  }
283
283
 
284
284
  override fun definition() = ModuleDefinition {
285
- Name("PerfLab")
285
+ Name("KeepSnappy")
286
286
 
287
- Events("perflabCommand")
287
+ Events("keepsnappyCommand")
288
288
 
289
289
  // Sync functions: called on the JS thread through JSI, no bridge hop, so a
290
290
  // marker lands at the moment JS asks for it.
@@ -335,7 +335,7 @@ class PerfLabModule : Module() {
335
335
  }
336
336
 
337
337
  Function("samplerNow") {
338
- perfLabMonotonicMs()
338
+ keepSnappyMonotonicMs()
339
339
  }
340
340
 
341
341
  OnDestroy {
@@ -376,10 +376,10 @@ class PerfLabModule : Module() {
376
376
  }
377
377
 
378
378
  // iOS shows the flush result as an accessibility element; Android adds a 1x1 transparent view
379
- // (contentDescription `perflab.flushed`, text = status) to the foreground activity's decor view;
379
+ // (contentDescription `keepsnappy.flushed`, text = status) to the foreground activity's decor view;
380
380
  // like iOS it stays until the next flush replaces it or the process ends. Always logged too.
381
381
  Function("flushSignal") { status: String, detail: String ->
382
- writeLog("PERF_LAB flushed $status $detail")
382
+ writeLog("KEEPSNAPPY flushed $status $detail")
383
383
  showFlushElement(status)
384
384
  }
385
385
 
@@ -435,7 +435,7 @@ class PerfLabModule : Module() {
435
435
  Function("writeText") { fileName: String, content: String ->
436
436
  val context = appContext.reactContext ?: return@Function null
437
437
  val base = context.getExternalFilesDir(null) ?: return@Function null
438
- val dir = File(base, "perf-lab")
438
+ val dir = File(base, "keepsnappy")
439
439
  dir.mkdirs()
440
440
  val file = File(dir, fileName.replace('/', '_'))
441
441
  file.writeText(content)
@@ -1,4 +1,4 @@
1
- package expo.modules.perflab
1
+ package expo.modules.keepsnappy
2
2
 
3
3
  import android.app.Activity
4
4
  import android.app.Application
@@ -25,7 +25,7 @@ import java.util.concurrent.ScheduledExecutorService
25
25
  import java.util.concurrent.TimeUnit
26
26
 
27
27
  // System.nanoTime() is CLOCK_MONOTONIC, the clock of performance.now() in React Native.
28
- internal fun perfLabMonotonicMs(): Double = System.nanoTime() / 1_000_000.0
28
+ internal fun keepSnappyMonotonicMs(): Double = System.nanoTime() / 1_000_000.0
29
29
 
30
30
  // PowerManager.THERMAL_STATUS_* (0 none .. 6 shutdown) mapped to the iOS ProcessInfo.thermalState
31
31
  // scale (0 nominal .. 3 critical) that derive and the heat rule read (`thermal >= 2` = serious):
@@ -34,7 +34,7 @@ private const val THERMAL_ANDROID_MODERATE = 2
34
34
  private const val THERMAL_ANDROID_SEVERE = 3
35
35
  private const val THERMAL_ANDROID_CRITICAL = 4
36
36
 
37
- internal fun perfLabThermalLevel(raw: Int): Int = when {
37
+ internal fun keepSnappyThermalLevel(raw: Int): Int = when {
38
38
  raw >= THERMAL_ANDROID_CRITICAL -> 3
39
39
  raw >= THERMAL_ANDROID_SEVERE -> 2
40
40
  raw >= THERMAL_ANDROID_MODERATE -> 1
@@ -53,7 +53,7 @@ private val JS_THREAD_NAMES = listOf("mqt_v_js", "mqt_js")
53
53
  // Counts presented and missed frames on the main thread's Choreographer. The budget follows the
54
54
  // display's current refresh rate (read off the frame path), so 90/120 Hz screens are judged right.
55
55
  // The callback allocates nothing: it reuses itself and touches only primitive fields.
56
- internal class PerfLabFrameCounter : Choreographer.FrameCallback {
56
+ internal class KeepSnappyFrameCounter : Choreographer.FrameCallback {
57
57
  private val lock = Any()
58
58
  private val main = Handler(Looper.getMainLooper())
59
59
  @Volatile private var running = false
@@ -127,7 +127,7 @@ internal const val JANK_BASIS_DISPLAY_RATE = "display-rate"
127
127
  // interval. Not seen: other windows (RN Modal dialogs, popups) and SurfaceView/TextureView content (video, Skia canvases);
128
128
  // `windowCovered` says another window held focus during the interval, so the counts of that interval are unknown.
129
129
  // Follows the foreground activity through lifecycle callbacks; the listener runs on its own thread, never the UI thread.
130
- internal class PerfLabFrameMetrics(private val context: () -> Context?) :
130
+ internal class KeepSnappyFrameMetrics(private val context: () -> Context?) :
131
131
  Window.OnFrameMetricsAvailableListener, Application.ActivityLifecycleCallbacks, ViewTreeObserver.OnWindowFocusChangeListener {
132
132
  private class Frame(val endNs: Long, val jank: Boolean, val firstDraw: Boolean, val durationNs: Long, val dropped: Int)
133
133
 
@@ -151,7 +151,7 @@ internal class PerfLabFrameMetrics(private val context: () -> Context?) :
151
151
  fun start(current: Activity?) {
152
152
  if (thread != null) return
153
153
  val app = context()?.applicationContext as? Application ?: return
154
- val t = HandlerThread("perflab-framemetrics").also { it.start() }
154
+ val t = HandlerThread("keepsnappy-framemetrics").also { it.start() }
155
155
  thread = t
156
156
  handler = Handler(t.looper)
157
157
  application = app
@@ -259,16 +259,16 @@ internal class PerfLabFrameMetrics(private val context: () -> Context?) :
259
259
 
260
260
  // Off unless configured. `auto` samples on its own thread every intervalMs and needs no JS;
261
261
  // `manual` only keeps the frame counter running and samples when asked. Every sample is logged
262
- // as `PERF_LAB sample {json}` and kept in a short ring for windows.
262
+ // as `KEEPSNAPPY sample {json}` and kept in a short ring for windows.
263
263
  // UNTESTED ON DEVICE: written against the iOS implementation, compiled only.
264
- internal class PerfLabSampler(
264
+ internal class KeepSnappySampler(
265
265
  private val context: () -> Context?,
266
266
  private val activity: () -> Activity?,
267
267
  private val log: (String) -> Unit,
268
268
  ) {
269
269
  private val lock = Any()
270
- private val frames = PerfLabFrameCounter()
271
- private val frameMetrics = PerfLabFrameMetrics(context)
270
+ private val frames = KeepSnappyFrameCounter()
271
+ private val frameMetrics = KeepSnappyFrameMetrics(context)
272
272
  private var executor: ScheduledExecutorService? = null
273
273
  private var mode = "off"
274
274
  private var intervalMs = SAMPLER_MS_DEFAULT
@@ -303,14 +303,14 @@ internal class PerfLabSampler(
303
303
  maxHz = maxRefreshHz()
304
304
  frames.start()
305
305
  frameMetrics.start(activity())
306
- val ex = Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "perflab-sampler") }
306
+ val ex = Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "keepsnappy-sampler") }
307
307
  if (mode == "auto") {
308
308
  val period = intervalMs.toLong()
309
309
  ex.scheduleAtFixedRate({ synchronized(lock) { collect("auto") } }, period, period, TimeUnit.MILLISECONDS)
310
310
  }
311
311
  executor = ex
312
312
  }
313
- mapOf("mode" to mode, "intervalMs" to intervalMs, "nowMs" to perfLabMonotonicMs())
313
+ mapOf("mode" to mode, "intervalMs" to intervalMs, "nowMs" to keepSnappyMonotonicMs())
314
314
  }
315
315
 
316
316
  fun sample(reason: String): Map<String, Any?>? = synchronized(lock) {
@@ -372,7 +372,7 @@ internal class PerfLabSampler(
372
372
  // figure: modes report 120.00001 for a 120 Hz panel.
373
373
  private fun maxRefreshHz(): Int {
374
374
  val modes = display()?.supportedModes ?: return 0
375
- log("PERF_LAB display modes " + modes.joinToString(" ") { "${it.modeId}:${it.physicalWidth}x${it.physicalHeight}@${it.refreshRate}" })
375
+ log("KEEPSNAPPY display modes " + modes.joinToString(" ") { "${it.modeId}:${it.physicalWidth}x${it.physicalHeight}@${it.refreshRate}" })
376
376
  return modes.maxOfOrNull { Math.round(it.refreshRate) } ?: 0
377
377
  }
378
378
 
@@ -443,7 +443,7 @@ internal class PerfLabSampler(
443
443
  }
444
444
 
445
445
  private fun collect(reason: String): Map<String, Any?> {
446
- val started = perfLabMonotonicMs()
446
+ val started = keepSnappyMonotonicMs()
447
447
  val t = started
448
448
  val processMs = Process.getElapsedCpuTime().toDouble()
449
449
  val mainMs = statCpuMs("/proc/self/task/${Process.myPid()}/stat")
@@ -484,7 +484,7 @@ internal class PerfLabSampler(
484
484
  "mem" to memory,
485
485
  )
486
486
  thermalRaw()?.let {
487
- record["thermal"] = perfLabThermalLevel(it)
487
+ record["thermal"] = keepSnappyThermalLevel(it)
488
488
  record["thermalRaw"] = it
489
489
  }
490
490
  val prev = previous
@@ -505,11 +505,11 @@ internal class PerfLabSampler(
505
505
  )
506
506
  record["cpu"] = cpu
507
507
  record["frames"] = framesOut
508
- record["collectMs"] = perfLabMonotonicMs() - started
508
+ record["collectMs"] = keepSnappyMonotonicMs() - started
509
509
 
510
510
  ring.addLast(record)
511
511
  while (ring.size > ringSize) ring.removeFirst()
512
- log("PERF_LAB sample ${JSONObject(record as Map<*, *>)}")
512
+ log("KEEPSNAPPY sample ${JSONObject(record as Map<*, *>)}")
513
513
  return record
514
514
  }
515
515
  }
package/app.plugin.js CHANGED
@@ -1,6 +1,6 @@
1
- // Expo config plugin for the PerfLab SDK.
1
+ // Expo config plugin for the KeepSnappy SDK.
2
2
  //
3
- // The native module (iOS pod PerfLab, Android expo.modules.perflab) is linked
3
+ // The native module (iOS pod KeepSnappy, Android expo.modules.keepsnappy) is linked
4
4
  // by Expo autolinking from expo-module.config.json. With the build flag off
5
5
  // (EXPO_PUBLIC_KEEPSNAPPY is not "1" when `expo prebuild` runs) this plugin
6
6
  // excludes the package from Expo autolinking, so a store build carries no SDK
@@ -20,8 +20,7 @@ const { withPodfile, withSettingsGradle } = require('expo/config-plugins');
20
20
  const PACKAGE_NAME = require('./package.json').name;
21
21
  const PACKAGE_NAMES = [...new Set([PACKAGE_NAME, '@keepsnappy/react-native'])];
22
22
  const namesFor = name => name === PACKAGE_NAME ? PACKAGE_NAMES : [name];
23
- // Keep this generated-line marker stable so old exclusions can be removed.
24
- const TAG = 'perflab: native module excluded, EXPO_PUBLIC_PERF_LAB is not 1';
23
+ const TAG = 'keepsnappy: native module excluded, EXPO_PUBLIC_KEEPSNAPPY is not 1';
25
24
 
26
25
  const isEnabled = () => (process.env.EXPO_PUBLIC_KEEPSNAPPY) === '1';
27
26
 
@@ -61,7 +60,9 @@ const gradleLine = (language, name) =>
61
60
  : `expoAutolinking.exclude = (expoAutolinking.exclude ?: []) + [${namesFor(name).map(quoted).join(', ')}] // ${TAG}`;
62
61
 
63
62
  const editSettingsGradle = (contents, enabled, language = 'groovy', name = PACKAGE_NAME) => {
64
- const lines = contents.split('\n').filter(line => !line.includes(TAG));
63
+ // Match the generated statement independently of its branding comment.
64
+ const generated = ['groovy', 'kt'].map(dsl => gradleLine(dsl, name).split(' // ')[0]);
65
+ const lines = contents.split('\n').filter(line => !generated.includes(line.split(' // ')[0].trim()));
65
66
  if (enabled) {
66
67
  return lines.join('\n');
67
68
  }
@@ -81,7 +82,7 @@ const withKeepSnappy = config => {
81
82
  if (enabled) {
82
83
  config.extra = {
83
84
  ...config.extra,
84
- perflab: { native: true, profilingRenderer: profilingRenderer() },
85
+ keepsnappy: { native: true, profilingRenderer: profilingRenderer() },
85
86
  };
86
87
  }
87
88
  config = withPodfile(config, mod => {
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "platforms": ["ios", "android"],
3
3
  "ios": {
4
- "modules": ["PerfLabModule"]
4
+ "modules": ["KeepSnappyModule"]
5
5
  },
6
6
  "android": {
7
- "modules": ["expo.modules.perflab.PerfLabModule"]
7
+ "modules": ["expo.modules.keepsnappy.KeepSnappyModule"]
8
8
  }
9
9
  }
@@ -3,7 +3,7 @@ require 'json'
3
3
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = 'PerfLab'
6
+ s.name = 'KeepSnappy'
7
7
  s.version = package['version']
8
8
  s.summary = package['description']
9
9
  s.description = package['description']
@@ -19,7 +19,7 @@ Pod::Spec.new do |s|
19
19
  s.dependency 'hermes-engine'
20
20
 
21
21
  s.source_files = "*.{swift,h,mm}"
22
- s.public_header_files = "PerfLabHermes.h"
22
+ s.public_header_files = "KeepSnappyHermes.h"
23
23
 
24
24
  s.pod_target_xcconfig = {
25
25
  'DEFINES_MODULE' => 'YES',
@@ -4,7 +4,7 @@ NS_ASSUME_NONNULL_BEGIN
4
4
 
5
5
  // Hermes sampling profiler (C++ API) for Swift. The runtime pointer is the
6
6
  // jsi::Runtime behind Expo's JavaScriptRuntime.withUnsafePointee.
7
- @interface PerfLabHermes : NSObject
7
+ @interface KeepSnappyHermes : NSObject
8
8
 
9
9
  + (BOOL)startWithHz:(double)hz;
10
10
  + (BOOL)stop;
@@ -1,4 +1,4 @@
1
- #import "PerfLabHermes.h"
1
+ #import "KeepSnappyHermes.h"
2
2
 
3
3
  #include <fstream>
4
4
  #include <sstream>
@@ -13,7 +13,7 @@ static fh::IHermesRootAPI *rootAPI() {
13
13
  return jsi::castInterface<fh::IHermesRootAPI>(fh::makeHermesRootAPI());
14
14
  }
15
15
 
16
- @implementation PerfLabHermes
16
+ @implementation KeepSnappyHermes
17
17
 
18
18
  + (BOOL)startWithHz:(double)hz {
19
19
  auto *api = rootAPI();