@keepsnappy/react-native 0.0.0-stage → 0.1.0-alpha.11

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.
@@ -0,0 +1,64 @@
1
+ #import "PerfLabHermes.h"
2
+
3
+ #include <fstream>
4
+ #include <sstream>
5
+
6
+ #include <hermes/hermes.h>
7
+ #include <jsi/jsi.h>
8
+
9
+ namespace fh = facebook::hermes;
10
+ namespace jsi = facebook::jsi;
11
+
12
+ static fh::IHermesRootAPI *rootAPI() {
13
+ return jsi::castInterface<fh::IHermesRootAPI>(fh::makeHermesRootAPI());
14
+ }
15
+
16
+ @implementation PerfLabHermes
17
+
18
+ + (BOOL)startWithHz:(double)hz {
19
+ auto *api = rootAPI();
20
+ if (api == nullptr) {
21
+ return NO;
22
+ }
23
+ api->enableSamplingProfiler(hz);
24
+ return YES;
25
+ }
26
+
27
+ + (BOOL)stop {
28
+ auto *api = rootAPI();
29
+ if (api == nullptr) {
30
+ return NO;
31
+ }
32
+ api->disableSamplingProfiler();
33
+ return YES;
34
+ }
35
+
36
+ + (nullable NSDictionary<NSString *, id> *)dumpRuntime:(void *)runtime toPath:(NSString *)path {
37
+ std::string file = path.UTF8String;
38
+ NSString *format = nil;
39
+ auto *jsiRuntime = static_cast<jsi::Runtime *>(runtime);
40
+ auto *ihermes = jsiRuntime != nullptr ? jsi::castInterface<fh::IHermes>(jsiRuntime) : nullptr;
41
+ if (ihermes != nullptr) {
42
+ std::ofstream out(file, std::ios::out | std::ios::trunc);
43
+ if (!out) {
44
+ return nil;
45
+ }
46
+ ihermes->sampledTraceToStreamInDevToolsFormat(out);
47
+ format = @"cpuprofile";
48
+ } else {
49
+ auto *api = rootAPI();
50
+ if (api == nullptr) {
51
+ return nil;
52
+ }
53
+ api->dumpSampledTraceToFile(file);
54
+ format = @"hermes-trace-events";
55
+ }
56
+ NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
57
+ return @{
58
+ @"path" : path,
59
+ @"bytes" : attributes[NSFileSize] ?: @0,
60
+ @"format" : format,
61
+ };
62
+ }
63
+
64
+ @end
@@ -0,0 +1,371 @@
1
+ import CryptoKit
2
+ import ExpoModulesCore
3
+ import Foundation
4
+ import UIKit
5
+ import os
6
+
7
+ private let pointsOfInterest = OSLog(subsystem: "com.gigaverse.perflab", category: .pointsOfInterest)
8
+ private let logger = OSLog(subsystem: "com.gigaverse.perflab", category: "log")
9
+
10
+ // os_log truncates long messages, so long payloads go out in chunks.
11
+ private let logChunk = 800
12
+
13
+ // Created by Expo at app start whether or not the SDK measures: nothing here may
14
+ // do work before a function is called (a dormant build calls activationRead only).
15
+ public class PerfLabModule: Module {
16
+ private lazy var logQueue = DispatchQueue(label: "com.gigaverse.perflab.log")
17
+ private var logHandle: FileHandle?
18
+ private var spoolHandle: FileHandle?
19
+ private var samplerCreated = false
20
+ private lazy var sampler: PerfLabSampler = {
21
+ self.samplerCreated = true
22
+ return PerfLabSampler { [weak self] line in self?.writeLog(line) }
23
+ }()
24
+
25
+ public func definition() -> ModuleDefinition {
26
+ Name("PerfLab")
27
+
28
+ // Signpost names must be static, so the window name travels in the message.
29
+ Function("beginAsyncSection") { (name: String, cookie: Int) in
30
+ os_signpost(.begin, log: pointsOfInterest, name: "perf-lab", signpostID: OSSignpostID(UInt64(cookie)), "%{public}s", name)
31
+ }
32
+
33
+ Function("endAsyncSection") { (name: String, cookie: Int) in
34
+ os_signpost(.end, log: pointsOfInterest, name: "perf-lab", signpostID: OSSignpostID(UInt64(cookie)), "%{public}s", name)
35
+ }
36
+
37
+ Function("setCounter") { (name: String, value: Double) in
38
+ os_signpost(.event, log: pointsOfInterest, name: "perf-lab-counter", "%{public}s=%f", name, value)
39
+ }
40
+
41
+ Function("isTracing") { () -> Bool in
42
+ pointsOfInterest.signpostsEnabled
43
+ }
44
+
45
+ Function("log") { (message: String) in
46
+ self.writeLog(message)
47
+ }
48
+
49
+ Function("samplerConfigure") { (mode: String, intervalMs: Double) -> [String: Any] in
50
+ self.sampler.configure(mode: mode, intervalMs: intervalMs)
51
+ }
52
+
53
+ Function("samplerSample") { (reason: String) -> [String: Any]? in
54
+ self.sampler.sample(reason: reason)
55
+ }
56
+
57
+ Function("samplerRequest") { (reason: String) in
58
+ self.sampler.request(reason: reason)
59
+ }
60
+
61
+ Function("samplerTake") { (reason: String) -> [String: Any]? in
62
+ self.sampler.take(reason: reason)
63
+ }
64
+
65
+ Function("samplerRecent") { (sinceT: Double) -> [[String: Any]] in
66
+ self.sampler.recent(since: sinceT)
67
+ }
68
+
69
+ Function("samplerNow") { () -> Double in
70
+ perfLabMonotonicMs()
71
+ }
72
+
73
+ OnDestroy {
74
+ if self.samplerCreated {
75
+ _ = self.sampler.configure(mode: "off", intervalMs: 0)
76
+ }
77
+ }
78
+
79
+ // Activation of a dormant build (JSON written by the SDK, read once at startup by the
80
+ // hook polyfill). Plain file read: no directory is created when there is none.
81
+ Function("activationRead") { () -> String? in
82
+ guard let file = Self.fileIfPresent("activation.json") else { return nil }
83
+ return try? String(contentsOf: file, encoding: .utf8)
84
+ }
85
+
86
+ Function("activationWrite") { (json: String) -> Bool in
87
+ guard let dir = Self.outputDir() else { return false }
88
+ do {
89
+ try json.write(to: dir.appendingPathComponent("activation.json"), atomically: true, encoding: .utf8)
90
+ return true
91
+ } catch {
92
+ return false
93
+ }
94
+ }
95
+
96
+ // os_log only, never the log file: a dormant build's answer to `dump=1`.
97
+ Function("osLog") { (message: String) in
98
+ Self.emit(message)
99
+ }
100
+
101
+ // Push journal (perf-lab/push-spool.jsonl): append is queued, read and replace wait for
102
+ // the queue so they see every earlier append.
103
+ Function("spoolAppend") { (record: String) in
104
+ self.logQueue.async {
105
+ if self.spoolHandle == nil, let dir = Self.outputDir() {
106
+ let file = dir.appendingPathComponent("push-spool.jsonl")
107
+ if !FileManager.default.fileExists(atPath: file.path) {
108
+ FileManager.default.createFile(atPath: file.path, contents: nil)
109
+ }
110
+ self.spoolHandle = try? FileHandle(forWritingTo: file)
111
+ _ = try? self.spoolHandle?.seekToEnd()
112
+ }
113
+ if let data = (record + "\n").data(using: .utf8) {
114
+ try? self.spoolHandle?.write(contentsOf: data)
115
+ }
116
+ }
117
+ }
118
+
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 `perflab.flushed`, label ok / error / off.
121
+ // Only ever created on request in a measuring process; replaced by the next flush.
122
+ Function("flushSignal") { (status: String, detail: String) in
123
+ Task { @MainActor in
124
+ Self.showSignal(tag: Self.flushSignalTag, id: "perflab.flushed", label: status, value: detail)
125
+ }
126
+ }
127
+
128
+ // The answer to a driver command (GC before the action): the same kind of element, id `perflab.ack`, label `gc:<n> ok`.
129
+ Function("ackSignal") { (text: String) in
130
+ Task { @MainActor in
131
+ Self.showSignal(tag: Self.ackSignalTag, id: "perflab.ack", label: text, value: text)
132
+ }
133
+ }
134
+
135
+ Function("spoolRead") { () -> String? in
136
+ self.logQueue.sync {
137
+ guard let file = Self.fileIfPresent("push-spool.jsonl") else { return nil }
138
+ return Self.readTail(file, max: Self.spoolReadMax)
139
+ }
140
+ }
141
+
142
+ Function("spoolReplace") { (content: String) -> Bool in
143
+ self.logQueue.sync {
144
+ try? self.spoolHandle?.close()
145
+ self.spoolHandle = nil
146
+ guard let dir = Self.outputDir() else { return false }
147
+ let file = dir.appendingPathComponent("push-spool.jsonl")
148
+ if content.isEmpty {
149
+ try? FileManager.default.removeItem(at: file)
150
+ return true
151
+ }
152
+ do {
153
+ try content.write(to: file, atomically: true, encoding: .utf8)
154
+ return true
155
+ } catch {
156
+ return false
157
+ }
158
+ }
159
+ }
160
+
161
+ // performance.now() in Hermes on iOS is steady_clock = mach_absolute_time (stops in sleep);
162
+ // the snapshot lets a run map it onto wall time and the trace clock.
163
+ Function("clockSnapshot") { () -> [String: Double] in
164
+ var info = mach_timebase_info_data_t()
165
+ mach_timebase_info(&info)
166
+ let machMs = Double(mach_absolute_time()) * Double(info.numer) / Double(info.denom) / 1_000_000
167
+ let continuousMs = Double(mach_continuous_time()) * Double(info.numer) / Double(info.denom) / 1_000_000
168
+ return [
169
+ "machAbsoluteMs": machMs,
170
+ "machContinuousMs": continuousMs,
171
+ "systemUptimeMs": ProcessInfo.processInfo.systemUptime * 1000,
172
+ "wallMs": Date().timeIntervalSince1970 * 1000,
173
+ "thermalState": Double(ProcessInfo.processInfo.thermalState.rawValue),
174
+ "lowPowerMode": ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0,
175
+ ]
176
+ }
177
+
178
+ // Documents/perf-lab is pulled with `xcrun devicectl device copy from --domain-type appDataContainer`.
179
+ Function("writeText") { (fileName: String, content: String) -> String? in
180
+ guard let dir = Self.outputDir() else { return nil }
181
+ let file = dir.appendingPathComponent(fileName.replacingOccurrences(of: "/", with: "_"))
182
+ do {
183
+ try content.write(to: file, atomically: true, encoding: .utf8)
184
+ return file.path
185
+ } catch {
186
+ return nil
187
+ }
188
+ }
189
+
190
+ // Hermes sampling profiler (PerfLabHermes.mm). Sync functions run on the JS
191
+ // thread, which the per-runtime dump needs.
192
+ Function("cpuProfileStart") { (hz: Double) -> Bool in
193
+ PerfLabHermes.start(withHz: hz)
194
+ }
195
+
196
+ Function("cpuProfileStop") { () -> Bool in
197
+ PerfLabHermes.stop()
198
+ }
199
+
200
+ // A raw profile file of a run (plan 40 §3.6): gzip, sha256 of the gzip, PUT to the
201
+ // signed URL the API handed out. Called at flush only, never inside a measured window.
202
+ // status 0: no HTTP answer (missing file, network error, timeout).
203
+ // The file is memory-mapped (clean pages, not app memory) and checked against the API's cap
204
+ // before it is read; only the gzip is built in memory.
205
+ // NOT YET COMPILED: the size check and the mapped read (night of 2026-10-14).
206
+ AsyncFunction("artifactUpload") { (fileName: String, url: String, headers: [String: String], timeoutMs: Double, promise: Promise) in
207
+ guard let file = Self.fileIfPresent(fileName.replacingOccurrences(of: "/", with: "_")) else {
208
+ promise.resolve(["status": 0, "error": "file missing"])
209
+ return
210
+ }
211
+ let size = ((try? FileManager.default.attributesOfItem(atPath: file.path))?[.size] as? NSNumber)?.int64Value ?? 0
212
+ if size > Self.artifactMaxBytes {
213
+ promise.resolve(["status": 413, "error": "file is \(size) bytes, over \(Self.artifactMaxBytes)"])
214
+ return
215
+ }
216
+ guard let raw = try? Data(contentsOf: file, options: .alwaysMapped) else {
217
+ promise.resolve(["status": 0, "error": "file missing"])
218
+ return
219
+ }
220
+ guard let target = URL(string: url), let gz = Self.gzip(raw) else {
221
+ promise.resolve(["status": 0, "bytes": raw.count, "error": "gzip or url failed"])
222
+ return
223
+ }
224
+ let sha = SHA256.hash(data: gz).map { String(format: "%02x", $0) }.joined()
225
+ var request = URLRequest(url: target, timeoutInterval: max(1, timeoutMs / 1000))
226
+ request.httpMethod = "PUT"
227
+ for (key, value) in headers {
228
+ request.setValue(value, forHTTPHeaderField: key)
229
+ }
230
+ URLSession.shared.uploadTask(with: request, from: gz) { _, response, error in
231
+ var out: [String: Any] = [
232
+ "status": (response as? HTTPURLResponse)?.statusCode ?? 0,
233
+ "bytes": raw.count,
234
+ "gzBytes": gz.count,
235
+ "sha256": sha,
236
+ ]
237
+ if let error {
238
+ out["error"] = error.localizedDescription
239
+ }
240
+ promise.resolve(out)
241
+ }.resume()
242
+ }
243
+
244
+ Function("cpuProfileDump") { (fileName: String) -> [String: Any]? in
245
+ guard let dir = Self.outputDir() else { return nil }
246
+ let path = dir.appendingPathComponent(fileName.replacingOccurrences(of: "/", with: "_")).path
247
+ guard let runtime = try? self.appContext?.runtime else {
248
+ return PerfLabHermes.dumpRuntime(nil, toPath: path)
249
+ }
250
+ return runtime.withUnsafePointee { pointer in
251
+ PerfLabHermes.dumpRuntime(pointer, toPath: path)
252
+ }
253
+ }
254
+ }
255
+
256
+ // The API parses at most 64 MiB of a raw profile file (MAX_RAW_BYTES in apps/api cpu-worker.ts).
257
+ private static let artifactMaxBytes: Int64 = 64 * 1024 * 1024
258
+
259
+ private static func outputDir() -> URL? {
260
+ guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
261
+ let dir = docs.appendingPathComponent("perf-lab", isDirectory: true)
262
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
263
+ return dir
264
+ }
265
+
266
+ // RFC 1952 around Foundation's zlib codec, which writes raw DEFLATE (RFC 1951).
267
+ private static func gzip(_ data: Data) -> Data? {
268
+ guard let deflated = try? (data as NSData).compressed(using: .zlib) as Data else { return nil }
269
+ var out = Data([0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff])
270
+ out.append(deflated)
271
+ var crc = crc32(data).littleEndian
272
+ var size = UInt32(truncatingIfNeeded: data.count).littleEndian
273
+ withUnsafeBytes(of: &crc) { out.append(contentsOf: $0) }
274
+ withUnsafeBytes(of: &size) { out.append(contentsOf: $0) }
275
+ return out
276
+ }
277
+
278
+ private static let crcTable: [UInt32] = (0..<256).map { n -> UInt32 in
279
+ var c = UInt32(n)
280
+ for _ in 0..<8 {
281
+ c = (c & 1) != 0 ? 0xEDB8_8320 ^ (c >> 1) : c >> 1
282
+ }
283
+ return c
284
+ }
285
+
286
+ private static func crc32(_ data: Data) -> UInt32 {
287
+ var c: UInt32 = 0xFFFF_FFFF
288
+ data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
289
+ for byte in buffer {
290
+ c = crcTable[Int((c ^ UInt32(byte)) & 0xFF)] ^ (c >> 8)
291
+ }
292
+ }
293
+ return c ^ 0xFFFF_FFFF
294
+ }
295
+
296
+ private static let flushSignalTag = 0x9E7F_1A5
297
+ private static let ackSignalTag = 0x9E7F_1A6
298
+
299
+ @MainActor
300
+ private static func showSignal(tag: Int, id: String, label status: String, value detail: String) {
301
+ let windows = UIApplication.shared.connectedScenes
302
+ .compactMap { $0 as? UIWindowScene }
303
+ .flatMap { $0.windows }
304
+ guard let window = windows.first(where: { $0.isKeyWindow }) ?? windows.first else { return }
305
+ window.viewWithTag(tag)?.removeFromSuperview()
306
+ let label = UILabel(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
307
+ label.tag = tag
308
+ label.text = status
309
+ label.textColor = .clear
310
+ label.backgroundColor = .clear
311
+ label.isUserInteractionEnabled = false
312
+ label.isAccessibilityElement = true
313
+ label.accessibilityIdentifier = id
314
+ label.accessibilityLabel = status
315
+ label.accessibilityValue = detail
316
+ window.addSubview(label)
317
+ }
318
+
319
+ // The SDK keeps the journal near 2x its queue cap (16 MB); a larger file is read from its tail.
320
+ private static let spoolReadMax: UInt64 = 16 * 1024 * 1024
321
+
322
+ private static func readTail(_ file: URL, max: UInt64) -> String? {
323
+ guard let handle = try? FileHandle(forReadingFrom: file) else { return nil }
324
+ defer { try? handle.close() }
325
+ guard let size = try? handle.seekToEnd() else { return nil }
326
+ let start = size > max ? size - max : 0
327
+ guard (try? handle.seek(toOffset: start)) != nil else { return nil }
328
+ let data = (try? handle.readToEnd()) ?? Data()
329
+ let text = String(decoding: data, as: UTF8.self)
330
+ if start == 0 { return text }
331
+ guard let newline = text.firstIndex(of: "\n") else { return "" }
332
+ return String(text[text.index(after: newline)...])
333
+ }
334
+
335
+ private static func fileIfPresent(_ name: String) -> URL? {
336
+ guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
337
+ let file = docs.appendingPathComponent("perf-lab", isDirectory: true).appendingPathComponent(name)
338
+ return FileManager.default.fileExists(atPath: file.path) ? file : nil
339
+ }
340
+
341
+ private static func emit(_ message: String) {
342
+ var start = message.startIndex
343
+ while start < message.endIndex {
344
+ let end = message.index(start, offsetBy: logChunk, limitedBy: message.endIndex) ?? message.endIndex
345
+ os_log("%{public}s", log: logger, type: .info, String(message[start..<end]))
346
+ start = end
347
+ }
348
+ }
349
+
350
+ private func writeLog(_ message: String) {
351
+ Self.emit(message)
352
+ appendToLogFile(message)
353
+ }
354
+
355
+ private func appendToLogFile(_ message: String) {
356
+ let line = "\(Date().timeIntervalSince1970 * 1000) \(message)\n"
357
+ logQueue.async {
358
+ if self.logHandle == nil, let dir = Self.outputDir() {
359
+ let file = dir.appendingPathComponent("log.txt")
360
+ if !FileManager.default.fileExists(atPath: file.path) {
361
+ FileManager.default.createFile(atPath: file.path, contents: nil)
362
+ }
363
+ self.logHandle = try? FileHandle(forWritingTo: file)
364
+ _ = try? self.logHandle?.seekToEnd()
365
+ }
366
+ if let data = line.data(using: .utf8) {
367
+ try? self.logHandle?.write(contentsOf: data)
368
+ }
369
+ }
370
+ }
371
+ }