@keepsnappy/react-native 0.1.0-alpha.11 → 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 +39 -39
- 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 +14 -13
- 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 +7 -7
- package/js/hook.polyfill.js +17 -17
- package/js/renderer-switch.js +3 -3
- package/js/sdk/activation.ts +19 -19
- 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 +52 -52
- 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 +7 -7
- package/js/sdk/push.ts +2 -2
- package/js/sdk/sampler.ts +1 -1
- package/js/zustand-vanilla.js +2 -2
- package/metro.js +10 -11
- package/package.json +1 -1
|
@@ -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
|
|
|
@@ -175,7 +175,7 @@ public class PerfLabModule: Module {
|
|
|
175
175
|
]
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// Documents/
|
|
178
|
+
// Documents/keepsnappy is pulled with `xcrun devicectl device copy from --domain-type appDataContainer`.
|
|
179
179
|
Function("writeText") { (fileName: String, content: String) -> String? in
|
|
180
180
|
guard let dir = Self.outputDir() else { return nil }
|
|
181
181
|
let file = dir.appendingPathComponent(fileName.replacingOccurrences(of: "/", with: "_"))
|
|
@@ -187,14 +187,14 @@ public class PerfLabModule: Module {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// Hermes sampling profiler (
|
|
190
|
+
// Hermes sampling profiler (KeepSnappyHermes.mm). Sync functions run on the JS
|
|
191
191
|
// thread, which the per-runtime dump needs.
|
|
192
192
|
Function("cpuProfileStart") { (hz: Double) -> Bool in
|
|
193
|
-
|
|
193
|
+
KeepSnappyHermes.start(withHz: hz)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
Function("cpuProfileStop") { () -> Bool in
|
|
197
|
-
|
|
197
|
+
KeepSnappyHermes.stop()
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// A raw profile file of a run (plan 40 §3.6): gzip, sha256 of the gzip, PUT to the
|
|
@@ -245,10 +245,10 @@ public class PerfLabModule: Module {
|
|
|
245
245
|
guard let dir = Self.outputDir() else { return nil }
|
|
246
246
|
let path = dir.appendingPathComponent(fileName.replacingOccurrences(of: "/", with: "_")).path
|
|
247
247
|
guard let runtime = try? self.appContext?.runtime else {
|
|
248
|
-
return
|
|
248
|
+
return KeepSnappyHermes.dumpRuntime(nil, toPath: path)
|
|
249
249
|
}
|
|
250
250
|
return runtime.withUnsafePointee { pointer in
|
|
251
|
-
|
|
251
|
+
KeepSnappyHermes.dumpRuntime(pointer, toPath: path)
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
}
|
|
@@ -258,7 +258,7 @@ public class PerfLabModule: Module {
|
|
|
258
258
|
|
|
259
259
|
private static func outputDir() -> URL? {
|
|
260
260
|
guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
|
261
|
-
let dir = docs.appendingPathComponent("
|
|
261
|
+
let dir = docs.appendingPathComponent("keepsnappy", isDirectory: true)
|
|
262
262
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
263
263
|
return dir
|
|
264
264
|
}
|
|
@@ -334,7 +334,7 @@ public class PerfLabModule: Module {
|
|
|
334
334
|
|
|
335
335
|
private static func fileIfPresent(_ name: String) -> URL? {
|
|
336
336
|
guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
|
337
|
-
let file = docs.appendingPathComponent("
|
|
337
|
+
let file = docs.appendingPathComponent("keepsnappy", isDirectory: true).appendingPathComponent(name)
|
|
338
338
|
return FileManager.default.fileExists(atPath: file.path) ? file : nil
|
|
339
339
|
}
|
|
340
340
|
|
|
@@ -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
|
-
// Wraps the app's Babel transformer (set by
|
|
2
|
-
// the entry module: the
|
|
1
|
+
// Wraps the app's Babel transformer (set by withKeepSnappy) and adds one import to
|
|
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
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
|
-
const upstreamPath = (process.env.KEEPSNAPPY_UPSTREAM_BABEL_TRANSFORMER
|
|
12
|
-
const entryFile = (process.env.KEEPSNAPPY_ENTRY_FILE
|
|
13
|
-
const bootFile = (process.env.KEEPSNAPPY_BOOT_FILE
|
|
11
|
+
const upstreamPath = (process.env.KEEPSNAPPY_UPSTREAM_BABEL_TRANSFORMER);
|
|
12
|
+
const entryFile = (process.env.KEEPSNAPPY_ENTRY_FILE);
|
|
13
|
+
const bootFile = (process.env.KEEPSNAPPY_BOOT_FILE);
|
|
14
14
|
|
|
15
15
|
if (!upstreamPath || !entryFile || !bootFile) {
|
|
16
16
|
throw new Error(
|
|
@@ -30,7 +30,7 @@ const inlineEnv = src =>
|
|
|
30
30
|
|
|
31
31
|
const envSignature = () =>
|
|
32
32
|
Object.keys(process.env)
|
|
33
|
-
.filter(name =>
|
|
33
|
+
.filter(name => name.startsWith('EXPO_PUBLIC_KEEPSNAPPY'))
|
|
34
34
|
.sort()
|
|
35
35
|
.map(name => `${name}=${process.env[name]}`)
|
|
36
36
|
.join('&');
|
|
@@ -66,6 +66,6 @@ module.exports = {
|
|
|
66
66
|
typeof upstream.getCacheKey === 'function'
|
|
67
67
|
? upstream.getCacheKey(...args)
|
|
68
68
|
: '';
|
|
69
|
-
return `${base}$
|
|
69
|
+
return `${base}$keepsnappy:${entryFile}:${bootFile}:${envSignature()}`;
|
|
70
70
|
},
|
|
71
71
|
};
|
package/js/hook.polyfill.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
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.
|
|
5
5
|
//
|
|
6
|
-
// Dormant builds (
|
|
6
|
+
// Dormant builds (EXPO_PUBLIC_KEEPSNAPPY_MODE=dormant) first read the activation
|
|
7
7
|
// record through the native module, synchronously (Expo installs
|
|
8
8
|
// `expo.modules` before the bundle runs; on Android the polyfill installs them itself). Not activated: nothing is installed
|
|
9
9
|
// and the React DevTools global stays exactly as without the SDK. If the native
|
|
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
|
|
14
|
-
//
|
|
15
|
-
// activation's `profile` in a dormant build,
|
|
13
|
+
// `profile` on __KEEPSNAPPY_ACTIVATION__ picks the React renderer in a
|
|
14
|
+
// KEEPSNAPPY_REACT_PROFILING=switch bundle (js/renderer-switch.js): the
|
|
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__';
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
var modeEnv = env(function () {
|
|
32
|
-
return (process.env.EXPO_PUBLIC_KEEPSNAPPY_MODE
|
|
32
|
+
return (process.env.EXPO_PUBLIC_KEEPSNAPPY_MODE);
|
|
33
33
|
});
|
|
34
34
|
var autoEnv = env(function () {
|
|
35
|
-
return (process.env.EXPO_PUBLIC_KEEPSNAPPY_AUTO
|
|
35
|
+
return (process.env.EXPO_PUBLIC_KEEPSNAPPY_AUTO);
|
|
36
36
|
});
|
|
37
37
|
var profileEnv = env(function () {
|
|
38
|
-
return (process.env.EXPO_PUBLIC_KEEPSNAPPY_PROFILE
|
|
38
|
+
return (process.env.EXPO_PUBLIC_KEEPSNAPPY_PROFILE);
|
|
39
39
|
});
|
|
40
40
|
var MODE =
|
|
41
41
|
modeEnv === 'dormant' || modeEnv === 'auto'
|
|
@@ -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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// React Native's production renderer in a
|
|
1
|
+
// React Native's production renderer in a KEEPSNAPPY_REACT_PROFILING=switch bundle.
|
|
2
2
|
// Both renderers are in the bundle; this module picks one when React Native
|
|
3
3
|
// first requires it, from what hook.polyfill.js (or the boot module) read out
|
|
4
|
-
// of the activation record before React loaded.
|
|
4
|
+
// of the activation record before React loaded. withKeepSnappy (metro.js)
|
|
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
|
@@ -2,35 +2,35 @@
|
|
|
2
2
|
// not, so it stays small and imports nothing: no React Native, no core.
|
|
3
3
|
//
|
|
4
4
|
// Modes (bundle time):
|
|
5
|
-
// auto - measuring from the first frame (
|
|
5
|
+
// auto - measuring from the first frame (EXPO_PUBLIC_KEEPSNAPPY_MODE=auto or KEEPSNAPPY_AUTO=1)
|
|
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
|
|
12
|
-
// Its `profile` field picks the React renderer in a
|
|
11
|
+
// before React loads, and leaves the raw value in __KEEPSNAPPY_ACTIVATION__.
|
|
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:
|
|
32
|
-
(process.env.EXPO_PUBLIC_KEEPSNAPPY_MODE
|
|
33
|
-
(process.env.EXPO_PUBLIC_KEEPSNAPPY_AUTO
|
|
31
|
+
export const MODE: KeepSnappyMode = resolveMode(
|
|
32
|
+
(process.env.EXPO_PUBLIC_KEEPSNAPPY_MODE),
|
|
33
|
+
(process.env.EXPO_PUBLIC_KEEPSNAPPY_AUTO),
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_TTL_S = 7200;
|
|
@@ -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
|
),
|
package/js/sdk/artifacts.ts
CHANGED
|
@@ -54,7 +54,7 @@ export const ARTIFACT_REQUEST_TIMEOUT_MS = 15_000;
|
|
|
54
54
|
export const ARTIFACT_PUT_TIMEOUT_MS = 60_000;
|
|
55
55
|
// One deadline per flush, started at the flush link (core.ts flushOnce) and shared by the lines push, the clock
|
|
56
56
|
// report and the artifact upload (this file's ARTIFACT_UPLOAD_DEADLINE_MS is that deadline). The end report gets its
|
|
57
|
-
// own request timeout after it, so `
|
|
57
|
+
// own request timeout after it, so `keepsnappy.flushed` is raised at most ARTIFACT_FLUSH_WORST_CASE_MS after the link.
|
|
58
58
|
// The e2e driver's wait for it is derived from this.
|
|
59
59
|
export const ARTIFACT_FLUSH_WORST_CASE_MS = ARTIFACT_UPLOAD_DEADLINE_MS + ARTIFACT_REQUEST_TIMEOUT_MS;
|
|
60
60
|
// Longest pause between two attempts at one file. SDK choice: half the Retry-After cap below, so a failing file never
|
|
@@ -324,8 +324,8 @@ export const createArtifactUploader = (deps: ArtifactUploaderDeps) => {
|
|
|
324
324
|
method: 'POST',
|
|
325
325
|
headers: {
|
|
326
326
|
'content-type': 'application/json',
|
|
327
|
-
'x-
|
|
328
|
-
'x-
|
|
327
|
+
'x-keepsnappy-key': deps.key,
|
|
328
|
+
'x-keepsnappy-run-token': token,
|
|
329
329
|
},
|
|
330
330
|
body: JSON.stringify(body),
|
|
331
331
|
...(controller ? { signal: controller.signal } : {}),
|
package/js/sdk/boot.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// keepsnappy SDK entry. withKeepSnappy (metro.js) adds this module to the graph and
|
|
2
2
|
// runs it after InitializeCore, before the app's entry module. The React hook
|
|
3
3
|
// itself was installed earlier by hook.polyfill.js (unless dormant).
|
|
4
4
|
//
|
|
5
5
|
// The measuring SDK (active.ts and everything it imports) is required only when
|
|
6
6
|
// this process measures, so a dormant process never evaluates it.
|
|
7
7
|
|
|
8
|
-
import { type Decision,
|
|
8
|
+
import { type Decision, KEEPSNAPPY_ENABLED, resolveDecision } from './activation';
|
|
9
9
|
|
|
10
|
-
if (
|
|
10
|
+
if (KEEPSNAPPY_ENABLED) {
|
|
11
11
|
try {
|
|
12
12
|
const decision: Decision = resolveDecision();
|
|
13
13
|
if (decision.active) {
|
|
@@ -16,6 +16,6 @@ if (PERF_LAB_ENABLED) {
|
|
|
16
16
|
(require('./dormant') as typeof import('./dormant')).startDormant(decision);
|
|
17
17
|
}
|
|
18
18
|
} catch (error) {
|
|
19
|
-
globalThis.console.log(`
|
|
19
|
+
globalThis.console.log(`KEEPSNAPPY boot failed ${String(error)}`);
|
|
20
20
|
}
|
|
21
21
|
}
|
package/js/sdk/causes.ts
CHANGED
|
@@ -743,7 +743,7 @@ const wrapDeviceEmitter = (emitter: { emit: AnyFn } | undefined) => {
|
|
|
743
743
|
// Responses of a request run under the cause of the code that sent it.
|
|
744
744
|
const wrapXhr = (proto: Record<string, unknown> | undefined) => {
|
|
745
745
|
if (!proto || typeof proto.send !== 'function') return;
|
|
746
|
-
const CAUSE = '
|
|
746
|
+
const CAUSE = '__keepsnappyCause';
|
|
747
747
|
const send = proto.send as AnyFn;
|
|
748
748
|
proto.send = function (this: Record<string, unknown>, ...args: unknown[]) {
|
|
749
749
|
this[CAUSE] = captured();
|
|
@@ -880,7 +880,7 @@ export const installCauses = (deps: CauseDeps = {}) => {
|
|
|
880
880
|
| { addListener?: (type: string, fn: (event: RawEvent) => void) => unknown }
|
|
881
881
|
| undefined;
|
|
882
882
|
raw?.addListener?.('*', onRawEvent);
|
|
883
|
-
g.
|
|
883
|
+
g.__KEEPSNAPPY_STORES__ = { writer: storeWriter };
|
|
884
884
|
causeStats.installed = true;
|
|
885
885
|
} catch {
|
|
886
886
|
causeStats.errors++;
|