@keepsnappy/react-native 0.1.0-alpha.12 → 0.1.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/{perflab/PerfLabModule.kt → keepsnappy/KeepSnappyModule.kt} +69 -28
- package/android/src/main/java/expo/modules/{perflab/PerfLabSampler.kt → keepsnappy/KeepSnappySampler.kt} +17 -17
- package/app.plugin.js +7 -6
- package/expo-module.config.json +2 -2
- package/ios/{PerfLab.podspec → KeepSnappy.podspec} +2 -2
- package/ios/{PerfLabHermes.h → KeepSnappyHermes.h} +1 -1
- package/ios/{PerfLabHermes.mm → KeepSnappyHermes.mm} +2 -2
- package/ios/{PerfLabModule.swift → KeepSnappyModule.swift} +65 -26
- package/ios/{PerfLabSampler.swift → KeepSnappySampler.swift} +10 -10
- package/js/babel-transformer.js +8 -4
- package/js/build-version.js +23 -0
- package/js/hook.polyfill.js +11 -11
- package/js/renderer-switch.js +1 -1
- package/js/sdk/activation.ts +15 -15
- package/js/sdk/active.ts +3 -3
- package/js/sdk/artifact-pending.ts +167 -0
- package/js/sdk/artifacts.ts +5 -4
- package/js/sdk/boot.ts +4 -4
- package/js/sdk/causes.ts +2 -2
- package/js/sdk/core.ts +143 -53
- package/js/sdk/cpuprof.ts +1 -1
- package/js/sdk/dormant.ts +7 -7
- package/js/sdk/links.ts +13 -13
- package/js/sdk/net.ts +2 -2
- package/js/sdk/profile-export.ts +7 -7
- package/js/sdk/push-install.ts +3 -3
- package/js/sdk/push.ts +52 -17
- package/js/sdk/sampler.ts +1 -1
- package/js/zustand-vanilla.js +2 -2
- package/metro.js +2 -0
- package/package.json +1 -1
package/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();
|
|
@@ -4,38 +4,38 @@ import Foundation
|
|
|
4
4
|
import UIKit
|
|
5
5
|
import os
|
|
6
6
|
|
|
7
|
-
private let pointsOfInterest = OSLog(subsystem: "com.gigaverse.
|
|
8
|
-
private let logger = OSLog(subsystem: "com.gigaverse.
|
|
7
|
+
private let pointsOfInterest = OSLog(subsystem: "com.gigaverse.keepsnappy", category: .pointsOfInterest)
|
|
8
|
+
private let logger = OSLog(subsystem: "com.gigaverse.keepsnappy", category: "log")
|
|
9
9
|
|
|
10
10
|
// os_log truncates long messages, so long payloads go out in chunks.
|
|
11
11
|
private let logChunk = 800
|
|
12
12
|
|
|
13
13
|
// Created by Expo at app start whether or not the SDK measures: nothing here may
|
|
14
14
|
// do work before a function is called (a dormant build calls activationRead only).
|
|
15
|
-
public class
|
|
16
|
-
private lazy var logQueue = DispatchQueue(label: "com.gigaverse.
|
|
15
|
+
public class KeepSnappyModule: Module {
|
|
16
|
+
private lazy var logQueue = DispatchQueue(label: "com.gigaverse.keepsnappy.log")
|
|
17
17
|
private var logHandle: FileHandle?
|
|
18
18
|
private var spoolHandle: FileHandle?
|
|
19
19
|
private var samplerCreated = false
|
|
20
|
-
private lazy var sampler:
|
|
20
|
+
private lazy var sampler: KeepSnappySampler = {
|
|
21
21
|
self.samplerCreated = true
|
|
22
|
-
return
|
|
22
|
+
return KeepSnappySampler { [weak self] line in self?.writeLog(line) }
|
|
23
23
|
}()
|
|
24
24
|
|
|
25
25
|
public func definition() -> ModuleDefinition {
|
|
26
|
-
Name("
|
|
26
|
+
Name("KeepSnappy")
|
|
27
27
|
|
|
28
28
|
// Signpost names must be static, so the window name travels in the message.
|
|
29
29
|
Function("beginAsyncSection") { (name: String, cookie: Int) in
|
|
30
|
-
os_signpost(.begin, log: pointsOfInterest, name: "
|
|
30
|
+
os_signpost(.begin, log: pointsOfInterest, name: "keepsnappy", signpostID: OSSignpostID(UInt64(cookie)), "%{public}s", name)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
Function("endAsyncSection") { (name: String, cookie: Int) in
|
|
34
|
-
os_signpost(.end, log: pointsOfInterest, name: "
|
|
34
|
+
os_signpost(.end, log: pointsOfInterest, name: "keepsnappy", signpostID: OSSignpostID(UInt64(cookie)), "%{public}s", name)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
Function("setCounter") { (name: String, value: Double) in
|
|
38
|
-
os_signpost(.event, log: pointsOfInterest, name: "
|
|
38
|
+
os_signpost(.event, log: pointsOfInterest, name: "keepsnappy-counter", "%{public}s=%f", name, value)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
Function("isTracing") { () -> Bool in
|
|
@@ -67,7 +67,7 @@ public class PerfLabModule: Module {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
Function("samplerNow") { () -> Double in
|
|
70
|
-
|
|
70
|
+
keepSnappyMonotonicMs()
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
OnDestroy {
|
|
@@ -98,7 +98,7 @@ public class PerfLabModule: Module {
|
|
|
98
98
|
Self.emit(message)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Push journal (
|
|
101
|
+
// Push journal (keepsnappy/push-spool.jsonl): append is queued, read and replace wait for
|
|
102
102
|
// the queue so they see every earlier append.
|
|
103
103
|
Function("spoolAppend") { (record: String) in
|
|
104
104
|
self.logQueue.async {
|
|
@@ -117,18 +117,18 @@ public class PerfLabModule: Module {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// Result of a `flush=1` link for the test driver: a transparent 1x1 pt element at the
|
|
120
|
-
// top-left of the key window, accessibility id `
|
|
120
|
+
// top-left of the key window, accessibility id `keepsnappy.flushed`, label ok / error / off.
|
|
121
121
|
// Only ever created on request in a measuring process; replaced by the next flush.
|
|
122
122
|
Function("flushSignal") { (status: String, detail: String) in
|
|
123
123
|
Task { @MainActor in
|
|
124
|
-
Self.showSignal(tag: Self.flushSignalTag, id: "
|
|
124
|
+
Self.showSignal(tag: Self.flushSignalTag, id: "keepsnappy.flushed", label: status, value: detail)
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// The answer to a driver command (GC before the action): the same kind of element, id `
|
|
128
|
+
// The answer to a driver command (GC before the action): the same kind of element, id `keepsnappy.ack`, label `gc:<n> ok`.
|
|
129
129
|
Function("ackSignal") { (text: String) in
|
|
130
130
|
Task { @MainActor in
|
|
131
|
-
Self.showSignal(tag: Self.ackSignalTag, id: "
|
|
131
|
+
Self.showSignal(tag: Self.ackSignalTag, id: "keepsnappy.ack", label: text, value: text)
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -139,6 +139,31 @@ public class PerfLabModule: Module {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// The profile files a flush has not uploaded yet, with their run tokens; read by a later process.
|
|
143
|
+
Function("artifactsPendingRead") { () -> String? in
|
|
144
|
+
self.logQueue.sync {
|
|
145
|
+
guard let file = Self.fileIfPresent("artifacts-pending.json") else { return nil }
|
|
146
|
+
return try? String(contentsOf: file, encoding: .utf8)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Function("artifactsPendingWrite") { (content: String) -> Bool in
|
|
151
|
+
self.logQueue.sync {
|
|
152
|
+
guard let dir = Self.outputDir() else { return false }
|
|
153
|
+
let file = dir.appendingPathComponent("artifacts-pending.json")
|
|
154
|
+
if content.isEmpty {
|
|
155
|
+
try? FileManager.default.removeItem(at: file)
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
do {
|
|
159
|
+
try content.write(to: file, atomically: true, encoding: .utf8)
|
|
160
|
+
return true
|
|
161
|
+
} catch {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
Function("spoolReplace") { (content: String) -> Bool in
|
|
143
168
|
self.logQueue.sync {
|
|
144
169
|
try? self.spoolHandle?.close()
|
|
@@ -175,7 +200,7 @@ public class PerfLabModule: Module {
|
|
|
175
200
|
]
|
|
176
201
|
}
|
|
177
202
|
|
|
178
|
-
// Documents/
|
|
203
|
+
// Documents/keepsnappy is pulled with `xcrun devicectl device copy from --domain-type appDataContainer`.
|
|
179
204
|
Function("writeText") { (fileName: String, content: String) -> String? in
|
|
180
205
|
guard let dir = Self.outputDir() else { return nil }
|
|
181
206
|
let file = dir.appendingPathComponent(fileName.replacingOccurrences(of: "/", with: "_"))
|
|
@@ -187,14 +212,14 @@ public class PerfLabModule: Module {
|
|
|
187
212
|
}
|
|
188
213
|
}
|
|
189
214
|
|
|
190
|
-
// Hermes sampling profiler (
|
|
215
|
+
// Hermes sampling profiler (KeepSnappyHermes.mm). Sync functions run on the JS
|
|
191
216
|
// thread, which the per-runtime dump needs.
|
|
192
217
|
Function("cpuProfileStart") { (hz: Double) -> Bool in
|
|
193
|
-
|
|
218
|
+
KeepSnappyHermes.start(withHz: hz)
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
Function("cpuProfileStop") { () -> Bool in
|
|
197
|
-
|
|
222
|
+
KeepSnappyHermes.stop()
|
|
198
223
|
}
|
|
199
224
|
|
|
200
225
|
// A raw profile file of a run (plan 40 §3.6): gzip, sha256 of the gzip, PUT to the
|
|
@@ -245,10 +270,10 @@ public class PerfLabModule: Module {
|
|
|
245
270
|
guard let dir = Self.outputDir() else { return nil }
|
|
246
271
|
let path = dir.appendingPathComponent(fileName.replacingOccurrences(of: "/", with: "_")).path
|
|
247
272
|
guard let runtime = try? self.appContext?.runtime else {
|
|
248
|
-
return
|
|
273
|
+
return KeepSnappyHermes.dumpRuntime(nil, toPath: path)
|
|
249
274
|
}
|
|
250
275
|
return runtime.withUnsafePointee { pointer in
|
|
251
|
-
|
|
276
|
+
KeepSnappyHermes.dumpRuntime(pointer, toPath: path)
|
|
252
277
|
}
|
|
253
278
|
}
|
|
254
279
|
}
|
|
@@ -258,7 +283,7 @@ public class PerfLabModule: Module {
|
|
|
258
283
|
|
|
259
284
|
private static func outputDir() -> URL? {
|
|
260
285
|
guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
|
261
|
-
let dir = docs.appendingPathComponent("
|
|
286
|
+
let dir = docs.appendingPathComponent("keepsnappy", isDirectory: true)
|
|
262
287
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
263
288
|
return dir
|
|
264
289
|
}
|
|
@@ -296,12 +321,26 @@ public class PerfLabModule: Module {
|
|
|
296
321
|
private static let flushSignalTag = 0x9E7F_1A5
|
|
297
322
|
private static let ackSignalTag = 0x9E7F_1A6
|
|
298
323
|
|
|
324
|
+
// No window yet (a relaunch, a link mid-transition): tried again this often, this far apart.
|
|
325
|
+
private static let signalRetries = 3
|
|
326
|
+
private static let signalRetryNanoseconds: UInt64 = 1_000_000_000
|
|
327
|
+
|
|
299
328
|
@MainActor
|
|
300
|
-
private static func showSignal(tag: Int, id: String, label status: String, value detail: String) {
|
|
329
|
+
private static func showSignal(tag: Int, id: String, label status: String, value detail: String, attempt: Int = 0) {
|
|
301
330
|
let windows = UIApplication.shared.connectedScenes
|
|
302
331
|
.compactMap { $0 as? UIWindowScene }
|
|
303
332
|
.flatMap { $0.windows }
|
|
304
|
-
guard let window = windows.first(where: { $0.isKeyWindow }) ?? windows.first else {
|
|
333
|
+
guard let window = windows.first(where: { $0.isKeyWindow }) ?? windows.first else {
|
|
334
|
+
if attempt < signalRetries {
|
|
335
|
+
Task { @MainActor in
|
|
336
|
+
try? await Task.sleep(nanoseconds: signalRetryNanoseconds)
|
|
337
|
+
Self.showSignal(tag: tag, id: id, label: status, value: detail, attempt: attempt + 1)
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
emit("KEEPSNAPPY \(id) not shown: no window after \(attempt + 1) tries (\(status))")
|
|
341
|
+
}
|
|
342
|
+
return
|
|
343
|
+
}
|
|
305
344
|
window.viewWithTag(tag)?.removeFromSuperview()
|
|
306
345
|
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
307
346
|
label.tag = tag
|
|
@@ -334,7 +373,7 @@ public class PerfLabModule: Module {
|
|
|
334
373
|
|
|
335
374
|
private static func fileIfPresent(_ name: String) -> URL? {
|
|
336
375
|
guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
|
337
|
-
let file = docs.appendingPathComponent("
|
|
376
|
+
let file = docs.appendingPathComponent("keepsnappy", isDirectory: true).appendingPathComponent(name)
|
|
338
377
|
return FileManager.default.fileExists(atPath: file.path) ? file : nil
|
|
339
378
|
}
|
|
340
379
|
|
|
@@ -19,7 +19,7 @@ private let timebase: mach_timebase_info_data_t = {
|
|
|
19
19
|
}()
|
|
20
20
|
|
|
21
21
|
// performance.now() in Hermes on iOS is steady_clock = mach_absolute_time.
|
|
22
|
-
func
|
|
22
|
+
func keepSnappyMonotonicMs() -> Double {
|
|
23
23
|
Double(mach_absolute_time()) * Double(timebase.numer) / Double(timebase.denom) / 1_000_000
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -82,7 +82,7 @@ private func memoryMb() -> (footprint: Double, resident: Double)? {
|
|
|
82
82
|
// on ProMotion screens the link runs at 60 Hz while duration reports the 120 Hz budget.
|
|
83
83
|
// The link asks for no frame rate, so it never raises the screen's refresh rate; a single
|
|
84
84
|
// 120 Hz drop between two 60 Hz callbacks is invisible.
|
|
85
|
-
final class
|
|
85
|
+
final class KeepSnappyFrameCounter: NSObject {
|
|
86
86
|
private var link: CADisplayLink?
|
|
87
87
|
private var lastTimestamp: CFTimeInterval = 0
|
|
88
88
|
private var lastBudget: CFTimeInterval = 0
|
|
@@ -176,11 +176,11 @@ final class PerfLabFrameCounter: NSObject {
|
|
|
176
176
|
|
|
177
177
|
// Off unless configured. `auto` samples on a utility queue every intervalMs and needs no JS;
|
|
178
178
|
// `manual` only keeps the frame counter running and samples when asked. Every sample is logged
|
|
179
|
-
// as `
|
|
180
|
-
final class
|
|
181
|
-
private let queue = DispatchQueue(label: "com.gigaverse.
|
|
179
|
+
// as `KEEPSNAPPY sample {json}` through the module's native log and kept in a short ring for windows.
|
|
180
|
+
final class KeepSnappySampler {
|
|
181
|
+
private let queue = DispatchQueue(label: "com.gigaverse.keepsnappy.sampler", qos: .utility)
|
|
182
182
|
private let log: (String) -> Void
|
|
183
|
-
private let frames =
|
|
183
|
+
private let frames = KeepSnappyFrameCounter()
|
|
184
184
|
|
|
185
185
|
private var timer: DispatchSourceTimer?
|
|
186
186
|
private var mode = "off"
|
|
@@ -231,7 +231,7 @@ final class PerfLabSampler {
|
|
|
231
231
|
timer = source
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
return ["mode": mode, "intervalMs": intervalMs, "nowMs":
|
|
234
|
+
return ["mode": mode, "intervalMs": intervalMs, "nowMs": keepSnappyMonotonicMs()]
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -300,7 +300,7 @@ final class PerfLabSampler {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
private func collect(reason: String) -> [String: Any] {
|
|
303
|
-
let started =
|
|
303
|
+
let started = keepSnappyMonotonicMs()
|
|
304
304
|
let t = started
|
|
305
305
|
let processMs = processCpuMs()
|
|
306
306
|
let main = frames.mainThread
|
|
@@ -349,7 +349,7 @@ final class PerfLabSampler {
|
|
|
349
349
|
previous = (t, processMs, mainMs, jsMs, frame.presentedTotal, frame.missedTotal)
|
|
350
350
|
record["cpu"] = cpu
|
|
351
351
|
record["frames"] = framesOut
|
|
352
|
-
record["collectMs"] =
|
|
352
|
+
record["collectMs"] = keepSnappyMonotonicMs() - started
|
|
353
353
|
|
|
354
354
|
ring.append(record)
|
|
355
355
|
if ring.count > ringSize {
|
|
@@ -357,7 +357,7 @@ final class PerfLabSampler {
|
|
|
357
357
|
}
|
|
358
358
|
if let data = try? JSONSerialization.data(withJSONObject: record),
|
|
359
359
|
let json = String(data: data, encoding: .utf8) {
|
|
360
|
-
log("
|
|
360
|
+
log("KEEPSNAPPY sample \(json)")
|
|
361
361
|
}
|
|
362
362
|
return record
|
|
363
363
|
}
|
package/js/babel-transformer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Wraps the app's Babel transformer (set by withKeepSnappy) and adds one import to
|
|
2
|
-
// the entry module: the
|
|
2
|
+
// the entry module: the keepsnappy boot module. That import is what puts the SDK
|
|
3
3
|
// in the graph; getModulesRunBeforeMainModule then runs it before the entry.
|
|
4
4
|
//
|
|
5
5
|
// babel-preset-expo inlines EXPO_PUBLIC_* only outside node_modules, and the
|
|
@@ -11,8 +11,9 @@ const path = require('path');
|
|
|
11
11
|
const upstreamPath = (process.env.KEEPSNAPPY_UPSTREAM_BABEL_TRANSFORMER);
|
|
12
12
|
const entryFile = (process.env.KEEPSNAPPY_ENTRY_FILE);
|
|
13
13
|
const bootFile = (process.env.KEEPSNAPPY_BOOT_FILE);
|
|
14
|
+
const sdkVersion = process.env.KEEPSNAPPY_SDK_VERSION;
|
|
14
15
|
|
|
15
|
-
if (!upstreamPath || !entryFile || !bootFile) {
|
|
16
|
+
if (!upstreamPath || !entryFile || !bootFile || !sdkVersion) {
|
|
16
17
|
throw new Error(
|
|
17
18
|
'KeepSnappy Babel transformer loaded without withKeepSnappy env (KEEPSNAPPY_*)',
|
|
18
19
|
);
|
|
@@ -57,7 +58,10 @@ module.exports = {
|
|
|
57
58
|
return upstream.transform({ ...args, src: bootImport() + args.src });
|
|
58
59
|
}
|
|
59
60
|
if (file.startsWith(packageRoot)) {
|
|
60
|
-
|
|
61
|
+
const src = file === path.join(packageRoot, 'js', 'sdk', 'push-install.ts')
|
|
62
|
+
? args.src.replace("require('../../package.json')", JSON.stringify({ version: sdkVersion }))
|
|
63
|
+
: args.src;
|
|
64
|
+
return upstream.transform({ ...args, src: inlineEnv(src) });
|
|
61
65
|
}
|
|
62
66
|
return upstream.transform(args);
|
|
63
67
|
},
|
|
@@ -66,6 +70,6 @@ module.exports = {
|
|
|
66
70
|
typeof upstream.getCacheKey === 'function'
|
|
67
71
|
? upstream.getCacheKey(...args)
|
|
68
72
|
: '';
|
|
69
|
-
return `${base}$
|
|
73
|
+
return `${base}$keepsnappy:${entryFile}:${bootFile}:${sdkVersion}:${envSignature()}`;
|
|
70
74
|
},
|
|
71
75
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Build tooling only: Metro workers receive the identity in their inherited env.
|
|
2
|
+
const { execFileSync } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function sdkBuildVersion() {
|
|
7
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
9
|
+
if (!pkg.private) return pkg.version;
|
|
10
|
+
const git = args => execFileSync('git', args, { cwd: packageRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
11
|
+
try {
|
|
12
|
+
const root = git(['rev-parse', '--show-toplevel']);
|
|
13
|
+
const release = JSON.parse(fs.readFileSync(path.join(root, 'release.json'), 'utf8')).version;
|
|
14
|
+
if (typeof release !== 'string' || !release) throw new Error('release.json has no version');
|
|
15
|
+
const sha = git(['rev-parse', '--short', 'HEAD']);
|
|
16
|
+
const dirty = git(['status', '--porcelain', '--untracked-files=normal']) ? '.dirty' : '';
|
|
17
|
+
return `${release}+local.${sha}${dirty}`;
|
|
18
|
+
} catch (cause) {
|
|
19
|
+
throw new Error('KeepSnappy: cannot identify this unpublished SDK; build from its Git checkout or use a staged release package', { cause });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { sdkBuildVersion };
|
package/js/hook.polyfill.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// keepsnappy React DevTools hook, installed as a Metro polyfill: it runs before
|
|
2
2
|
// any module, so it exists before the React renderer evaluates and injects.
|
|
3
3
|
// A polyfill is a plain script (no require), so this file stays self-contained:
|
|
4
4
|
// it only counts and buffers. The SDK boot module attaches a listener later.
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
// module cannot be reached yet, the hook is prepared but not installed and the
|
|
11
11
|
// boot module decides (it still runs before the renderer loads).
|
|
12
12
|
//
|
|
13
|
-
// `profile` on
|
|
13
|
+
// `profile` on __KEEPSNAPPY_ACTIVATION__ picks the React renderer in a
|
|
14
14
|
// KEEPSNAPPY_REACT_PROFILING=switch bundle (js/renderer-switch.js): the
|
|
15
15
|
// activation's `profile` in a dormant build, EXPO_PUBLIC_KEEPSNAPPY_PROFILE otherwise.
|
|
16
16
|
(function (g) {
|
|
17
|
-
if (g.
|
|
17
|
+
if (g.__KEEPSNAPPY_HOOK__ || g.__KEEPSNAPPY_ACTIVATION__) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
var HOOK = '__REACT_DEVTOOLS_GLOBAL_HOOK__';
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
var modules = g.expo && g.expo.modules;
|
|
88
|
-
var native = modules ? modules.
|
|
88
|
+
var native = modules ? modules.KeepSnappy : undefined;
|
|
89
89
|
if (native && typeof native.activationRead === 'function') {
|
|
90
90
|
activation.raw = native.activationRead();
|
|
91
91
|
activation.read = 'polyfill';
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
activation.read = 'deferred';
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
g.
|
|
102
|
+
g.__KEEPSNAPPY_ACTIVATION__ = activation;
|
|
103
103
|
if (MODE === 'dormant' && activation.read === 'polyfill' && !activation.active) {
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
var log = function (message) {
|
|
136
136
|
try {
|
|
137
137
|
if (g.console && typeof g.console.log === 'function') {
|
|
138
|
-
g.console.log('
|
|
138
|
+
g.console.log('KEEPSNAPPY ' + message);
|
|
139
139
|
}
|
|
140
140
|
} catch (_e) {
|
|
141
141
|
return;
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
supportsFiber: true,
|
|
221
221
|
isDisabled: false,
|
|
222
222
|
renderers: new Map(),
|
|
223
|
-
|
|
223
|
+
__keepsnappy: true,
|
|
224
224
|
inject: function (internals) {
|
|
225
225
|
var id = nextId++;
|
|
226
226
|
ownHook.renderers.set(id, internals);
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
// Wrap a hook someone else installed (React DevTools, bippy, ...): theirs runs
|
|
242
242
|
// first, ours after, and the renderer id stays theirs.
|
|
243
243
|
var chain = function (hook) {
|
|
244
|
-
if (!hook || hook.
|
|
244
|
+
if (!hook || hook.__keepsnappy || hook.__keepsnappyChained) {
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
247
|
var origInject = hook.inject;
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
recordInject(id, internals);
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
|
-
hook.
|
|
270
|
+
hook.__keepsnappyChained = true;
|
|
271
271
|
state.chainedTargets++;
|
|
272
272
|
state.mode = 'chained';
|
|
273
273
|
};
|
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
var existing = g[HOOK];
|
|
302
302
|
if (existing === undefined || existing === null) {
|
|
303
303
|
installOwn();
|
|
304
|
-
} else if (!existing.
|
|
304
|
+
} else if (!existing.__keepsnappy) {
|
|
305
305
|
chain(existing);
|
|
306
306
|
}
|
|
307
307
|
return state.mode;
|
|
@@ -314,7 +314,7 @@
|
|
|
314
314
|
ensure();
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
g.
|
|
317
|
+
g.__KEEPSNAPPY_HOOK__ = {
|
|
318
318
|
state: state,
|
|
319
319
|
ensure: ensure,
|
|
320
320
|
attach: function (listener) {
|
package/js/renderer-switch.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// resolves the two module names below to RN's own implementation files.
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
var activation = globalThis.
|
|
8
|
+
var activation = globalThis.__KEEPSNAPPY_ACTIVATION__;
|
|
9
9
|
var profiling = !!activation && activation.profile === true;
|
|
10
10
|
if (activation) {
|
|
11
11
|
activation.renderer = profiling ? 'profiling' : 'prod';
|
package/js/sdk/activation.ts
CHANGED
|
@@ -6,29 +6,29 @@
|
|
|
6
6
|
// manual - markers only until a settings link turns collection on (no mode set)
|
|
7
7
|
// dormant - nothing installed until an activation link; it applies from the NEXT launch
|
|
8
8
|
//
|
|
9
|
-
// The activation record lives in the app sandbox (`
|
|
9
|
+
// The activation record lives in the app sandbox (`keepsnappy/activation.json`,
|
|
10
10
|
// written by the native module). hook.polyfill.js reads it once, synchronously,
|
|
11
|
-
// before React loads, and leaves the raw value in
|
|
11
|
+
// before React loads, and leaves the raw value in __KEEPSNAPPY_ACTIVATION__.
|
|
12
12
|
// Its `profile` field picks the React renderer in a KEEPSNAPPY_REACT_PROFILING=switch
|
|
13
13
|
// bundle (js/renderer-switch.js), so a process is wholly one renderer.
|
|
14
14
|
|
|
15
|
-
export const
|
|
15
|
+
export const KEEPSNAPPY_ENABLED = (process.env.EXPO_PUBLIC_KEEPSNAPPY) === '1';
|
|
16
16
|
|
|
17
|
-
export const
|
|
17
|
+
export const KEEPSNAPPY_DEEP_LINK_PATH = 'devtools/keepsnappy';
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type KeepSnappyMode = 'auto' | 'manual' | 'dormant';
|
|
20
20
|
|
|
21
21
|
export const resolveMode = (
|
|
22
22
|
mode: string | undefined,
|
|
23
23
|
auto: string | undefined,
|
|
24
|
-
):
|
|
24
|
+
): KeepSnappyMode => {
|
|
25
25
|
if (mode === 'dormant' || mode === 'auto') {
|
|
26
26
|
return mode;
|
|
27
27
|
}
|
|
28
28
|
return auto === '1' ? 'auto' : 'manual';
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const MODE:
|
|
31
|
+
export const MODE: KeepSnappyMode = resolveMode(
|
|
32
32
|
(process.env.EXPO_PUBLIC_KEEPSNAPPY_MODE),
|
|
33
33
|
(process.env.EXPO_PUBLIC_KEEPSNAPPY_AUTO),
|
|
34
34
|
);
|
|
@@ -53,7 +53,7 @@ export interface ActivationRecord {
|
|
|
53
53
|
export type ActivationRead = 'polyfill' | 'boot' | 'unavailable' | 'none';
|
|
54
54
|
|
|
55
55
|
export interface Decision {
|
|
56
|
-
mode:
|
|
56
|
+
mode: KeepSnappyMode;
|
|
57
57
|
active: boolean;
|
|
58
58
|
runToken: string | null;
|
|
59
59
|
expiresAt: number | null;
|
|
@@ -135,7 +135,7 @@ const OFF: ActivationRecord = {
|
|
|
135
135
|
|
|
136
136
|
// Must agree with activeRecord() in hook.polyfill.js (tested on the same fixtures).
|
|
137
137
|
export const decide = (
|
|
138
|
-
mode:
|
|
138
|
+
mode: KeepSnappyMode,
|
|
139
139
|
record: ActivationRecord | null,
|
|
140
140
|
nowMs: number,
|
|
141
141
|
read: ActivationRead,
|
|
@@ -252,8 +252,8 @@ export const parseQuery = (
|
|
|
252
252
|
export const queryOf = (url: string) => parseQuery(url.split('?')[1]);
|
|
253
253
|
|
|
254
254
|
interface ActivationGlobal {
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
__KEEPSNAPPY_ACTIVATION__?: PolyfillActivation;
|
|
256
|
+
__KEEPSNAPPY_HOOK__?: { ensure: () => string };
|
|
257
257
|
expo?: { modules?: Record<string, unknown> };
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -266,7 +266,7 @@ const isNative = (value: unknown): value is ActivationNative =>
|
|
|
266
266
|
|
|
267
267
|
// Found lazily: only links and a deferred startup read need it.
|
|
268
268
|
export const findActivationNative = (): ActivationNative | null => {
|
|
269
|
-
const direct = g.expo?.modules?.
|
|
269
|
+
const direct = g.expo?.modules?.KeepSnappy;
|
|
270
270
|
if (isNative(direct)) {
|
|
271
271
|
return direct;
|
|
272
272
|
}
|
|
@@ -278,7 +278,7 @@ export const findActivationNative = (): ActivationNative | null => {
|
|
|
278
278
|
const core = require('expo-modules-core') as {
|
|
279
279
|
requireOptionalNativeModule?: (name: string) => unknown;
|
|
280
280
|
};
|
|
281
|
-
const found = core.requireOptionalNativeModule?.('
|
|
281
|
+
const found = core.requireOptionalNativeModule?.('KeepSnappy');
|
|
282
282
|
return isNative(found) ? found : null;
|
|
283
283
|
} catch {
|
|
284
284
|
return null;
|
|
@@ -290,7 +290,7 @@ export const resolveDecision = (
|
|
|
290
290
|
nowMs: number = Date.now(),
|
|
291
291
|
findNative: () => ActivationNative | null = findActivationNative,
|
|
292
292
|
): Decision => {
|
|
293
|
-
const fromPolyfill = g.
|
|
293
|
+
const fromPolyfill = g.__KEEPSNAPPY_ACTIVATION__;
|
|
294
294
|
if (MODE !== 'dormant') {
|
|
295
295
|
// Not gated: the record only carries a persisted token, read when it is cheap.
|
|
296
296
|
if (fromPolyfill && fromPolyfill.read === 'polyfill') {
|
|
@@ -324,7 +324,7 @@ export const resolveDecision = (
|
|
|
324
324
|
decision = decide(MODE, null, nowMs, 'unavailable');
|
|
325
325
|
}
|
|
326
326
|
if (decision.active) {
|
|
327
|
-
g.
|
|
327
|
+
g.__KEEPSNAPPY_HOOK__?.ensure();
|
|
328
328
|
}
|
|
329
329
|
// The polyfill could not choose the renderer; boot still runs before it loads.
|
|
330
330
|
if (fromPolyfill && fromPolyfill.read === 'deferred') {
|
package/js/sdk/active.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
applySettings,
|
|
8
8
|
attachHook,
|
|
9
9
|
exposeGlobal,
|
|
10
|
-
|
|
10
|
+
handleKeepSnappyDeepLink,
|
|
11
11
|
logStartup,
|
|
12
12
|
perfLog,
|
|
13
13
|
setActivation,
|
|
@@ -25,7 +25,7 @@ const guarded = (label: string, run: () => void) => {
|
|
|
25
25
|
run();
|
|
26
26
|
} catch (error) {
|
|
27
27
|
globalThis.console.log(
|
|
28
|
-
`
|
|
28
|
+
`KEEPSNAPPY boot step failed ${label} ${String(error)}`,
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
};
|
|
@@ -41,7 +41,7 @@ export const startActive = (decision: Decision) => {
|
|
|
41
41
|
installLinks(
|
|
42
42
|
(url, via) => {
|
|
43
43
|
perfLog(`link via=${via} ${redactUrl(url)}`);
|
|
44
|
-
|
|
44
|
+
handleKeepSnappyDeepLink(url);
|
|
45
45
|
},
|
|
46
46
|
noteAppLink,
|
|
47
47
|
),
|