@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 +35 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/{perflab/PerfLabModule.kt → keepsnappy/KeepSnappyModule.kt} +23 -23
- package/android/src/main/java/expo/modules/{perflab/PerfLabSampler.kt → keepsnappy/KeepSnappySampler.kt} +17 -17
- package/app.plugin.js +7 -6
- package/expo-module.config.json +2 -2
- package/ios/{PerfLab.podspec → KeepSnappy.podspec} +2 -2
- package/ios/{PerfLabHermes.h → KeepSnappyHermes.h} +1 -1
- package/ios/{PerfLabHermes.mm → KeepSnappyHermes.mm} +2 -2
- package/ios/{PerfLabModule.swift → KeepSnappyModule.swift} +24 -24
- package/ios/{PerfLabSampler.swift → KeepSnappySampler.swift} +10 -10
- package/js/babel-transformer.js +2 -2
- package/js/hook.polyfill.js +11 -11
- package/js/renderer-switch.js +1 -1
- package/js/sdk/activation.ts +15 -15
- package/js/sdk/active.ts +3 -3
- package/js/sdk/artifacts.ts +3 -3
- package/js/sdk/boot.ts +4 -4
- package/js/sdk/causes.ts +2 -2
- package/js/sdk/core.ts +45 -45
- package/js/sdk/cpuprof.ts +1 -1
- package/js/sdk/dormant.ts +7 -7
- package/js/sdk/links.ts +13 -13
- package/js/sdk/net.ts +2 -2
- package/js/sdk/profile-export.ts +7 -7
- package/js/sdk/push-install.ts +3 -3
- package/js/sdk/push.ts +2 -2
- package/js/sdk/sampler.ts +1 -1
- package/js/zustand-vanilla.js +2 -2
- package/package.json +1 -1
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 `
|
|
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 `
|
|
35
|
-
- **Flag on:** the exclusion is removed again (prebuild without `--clean` edits existing files) and `extra.
|
|
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` | `
|
|
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-
|
|
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/
|
|
64
|
-
| `EXPO_PUBLIC_KEEPSNAPPY_CPUPROF`, `..._CPUPROF_HZ` | `KEEPSNAPPY_CPUPROF` | `1` = on; Hz (default 250) | Default of `cpuprof`/`cpuhz`: Hermes sampling profiler (iOS), `Documents/
|
|
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 `
|
|
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 `
|
|
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 `
|
|
87
|
+
| Link | Writes `keepsnappy/activation.json` |
|
|
88
88
|
|---|---|
|
|
89
|
-
| `devtools/
|
|
90
|
-
| `devtools/
|
|
91
|
-
| `devtools/
|
|
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 `
|
|
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/
|
|
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 `
|
|
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 `
|
|
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/
|
|
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/
|
|
135
|
+
- openLink: <scheme>://devtools/keepsnappy?flush=1&activate=0
|
|
136
136
|
- extendedWaitUntil:
|
|
137
137
|
visible:
|
|
138
|
-
id:
|
|
138
|
+
id: keepsnappy.flushed
|
|
139
139
|
timeout: 60000
|
|
140
140
|
- assertVisible:
|
|
141
|
-
id:
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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.
|
|
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/
|
|
216
|
-
| `ios/
|
|
217
|
-
| `android/build.gradle`, `android/src/...` | Android: `Trace` sections, logcat, log file, resource sampler (`
|
|
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
|
|
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:../
|
|
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
|
|
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 `
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
5
|
+
group = 'expo.modules.keepsnappy'
|
|
6
6
|
version = '1.0.0'
|
|
7
7
|
|
|
8
8
|
android {
|
|
9
|
-
namespace "expo.modules.
|
|
9
|
+
namespace "expo.modules.keepsnappy"
|
|
10
10
|
compileSdk safeExtGet("compileSdkVersion", 34)
|
|
11
11
|
|
|
12
12
|
defaultConfig {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package expo.modules.
|
|
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 = "
|
|
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 = "
|
|
38
|
-
private const val FLUSH_ELEMENT_TAG = "
|
|
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
|
|
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 =
|
|
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("
|
|
97
|
+
sendEvent("keepsnappyCommand", mapOf("cmd" to cmd, "n" to n, "tNative" to tNative))
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
val filter = IntentFilter("${context.packageName}.
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
146
|
+
KeepSnappySampler({ appContext.reactContext }, { appContext.currentActivity }, ::writeLog)
|
|
147
147
|
}
|
|
148
148
|
private val io: ExecutorService by lazy {
|
|
149
|
-
Executors.newSingleThreadExecutor { r -> Thread(r, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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("
|
|
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("
|
|
285
|
+
Name("KeepSnappy")
|
|
286
286
|
|
|
287
|
-
Events("
|
|
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
|
-
|
|
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 `
|
|
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("
|
|
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, "
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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 `
|
|
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
|
|
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 =
|
|
271
|
-
private val frameMetrics =
|
|
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, "
|
|
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
|
|
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("
|
|
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 =
|
|
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"] =
|
|
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"] =
|
|
508
|
+
record["collectMs"] = keepSnappyMonotonicMs() - started
|
|
509
509
|
|
|
510
510
|
ring.addLast(record)
|
|
511
511
|
while (ring.size > ringSize) ring.removeFirst()
|
|
512
|
-
log("
|
|
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
|
|
1
|
+
// Expo config plugin for the KeepSnappy SDK.
|
|
2
2
|
//
|
|
3
|
-
// The native module (iOS pod
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
keepsnappy: { native: true, profilingRenderer: profilingRenderer() },
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
config = withPodfile(config, mod => {
|
package/expo-module.config.json
CHANGED
|
@@ -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 = '
|
|
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 = "
|
|
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
|
|
7
|
+
@interface KeepSnappyHermes : NSObject
|
|
8
8
|
|
|
9
9
|
+ (BOOL)startWithHz:(double)hz;
|
|
10
10
|
+ (BOOL)stop;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#import "
|
|
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
|
|
16
|
+
@implementation KeepSnappyHermes
|
|
17
17
|
|
|
18
18
|
+ (BOOL)startWithHz:(double)hz {
|
|
19
19
|
auto *api = rootAPI();
|