@onekeyfe/react-native-perf-stats 3.0.25
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/LICENSE +21 -0
- package/README.md +64 -0
- package/ReactNativePerfStats.podspec +30 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +130 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/PerfStatsInitProvider.kt +43 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStats.kt +514 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStatsPackage.kt +24 -0
- package/ios/ReactNativePerfStats.swift +391 -0
- package/lib/module/ReactNativePerfStats.nitro.js +4 -0
- package/lib/module/ReactNativePerfStats.nitro.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativePerfStats.nitro.d.ts +51 -0
- package/lib/typescript/src/ReactNativePerfStats.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JHybridReactNativePerfStatsSpec.cpp +83 -0
- package/nitrogen/generated/android/c++/JHybridReactNativePerfStatsSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JPerfSample.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/HybridReactNativePerfStatsSpec.kt +74 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/PerfSample.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/reactnativeperfstatsOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativeperfstats+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativeperfstats+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativeperfstatsOnLoad.cpp +44 -0
- package/nitrogen/generated/android/reactnativeperfstatsOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ReactNativePerfStats+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Bridge.hpp +122 -0
- package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Umbrella.hpp +47 -0
- package/nitrogen/generated/ios/ReactNativePerfStatsAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativePerfStatsAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridReactNativePerfStatsSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativePerfStatsSpecSwift.hpp +102 -0
- package/nitrogen/generated/ios/swift/Func_void_PerfSample.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativePerfStatsSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridReactNativePerfStatsSpec_cxx.swift +182 -0
- package/nitrogen/generated/ios/swift/PerfSample.swift +58 -0
- package/nitrogen/generated/shared/c++/HybridReactNativePerfStatsSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridReactNativePerfStatsSpec.hpp +68 -0
- package/nitrogen/generated/shared/c++/PerfSample.hpp +83 -0
- package/package.json +169 -0
- package/src/ReactNativePerfStats.nitro.ts +54 -0
- package/src/index.tsx +8 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import ReactNativeNativeLogger
|
|
3
|
+
import Darwin
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
private let kTag = "PerfStats"
|
|
7
|
+
private let kMinIntervalMs: Double = 200
|
|
8
|
+
|
|
9
|
+
// Anomaly logging thresholds. We only emit a warn after the metric has
|
|
10
|
+
// stayed over the threshold for kAnomalySustainSamples in a row, to
|
|
11
|
+
// skip transient spikes (e.g. JS startup, GC). After firing we throttle
|
|
12
|
+
// for kAnomalyCooldownSec to avoid flooding native-logger.
|
|
13
|
+
private let kCpuAnomalyPct: Double = 150.0
|
|
14
|
+
private let kRssAnomalyBytes: Double = 800.0 * 1024.0 * 1024.0
|
|
15
|
+
private let kAnomalySustainSamples: Int = 5
|
|
16
|
+
private let kAnomalyCooldownSec: Double = 30.0
|
|
17
|
+
|
|
18
|
+
class ReactNativePerfStats: HybridReactNativePerfStatsSpec {
|
|
19
|
+
|
|
20
|
+
func start(intervalMs: Double) throws {
|
|
21
|
+
Sampler.shared.start(intervalMs: max(intervalMs, kMinIntervalMs))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func stop() throws {
|
|
25
|
+
Sampler.shared.stop()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func showOverlay() throws {
|
|
29
|
+
Overlay.shared.show()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func hideOverlay() throws {
|
|
33
|
+
Overlay.shared.hide()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func sample() throws -> Promise<PerfSample> {
|
|
37
|
+
return Promise.async {
|
|
38
|
+
return Sampler.shared.takeSample()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Sampler
|
|
44
|
+
//
|
|
45
|
+
// Singleton; one timer fires on a private background queue regardless of
|
|
46
|
+
// how many HybridObject instances exist or which thread calls start(). This
|
|
47
|
+
// keeps overlay updates flowing even when the JS thread is blocked.
|
|
48
|
+
|
|
49
|
+
private final class Sampler {
|
|
50
|
+
static let shared = Sampler()
|
|
51
|
+
|
|
52
|
+
private let queue = DispatchQueue(label: "io.onekey.perfstats.sampler", qos: .utility)
|
|
53
|
+
private let lock = NSLock()
|
|
54
|
+
private var timer: DispatchSourceTimer?
|
|
55
|
+
private var running = false
|
|
56
|
+
private var intervalMs: Double = 1000
|
|
57
|
+
private var lastCpuSec: Double = -1
|
|
58
|
+
private var lastMonoSec: Double = -1
|
|
59
|
+
|
|
60
|
+
// Anomaly state lives on the sampler's serial dispatch queue, so no
|
|
61
|
+
// synchronization is needed. lastLogSec intentionally persists across
|
|
62
|
+
// stop()/start() cycles to keep the cooldown honest if the caller
|
|
63
|
+
// toggles the sampler rapidly.
|
|
64
|
+
private var cpuOverCount: Int = 0
|
|
65
|
+
private var rssOverCount: Int = 0
|
|
66
|
+
private var lastCpuLogSec: Double = 0
|
|
67
|
+
private var lastRssLogSec: Double = 0
|
|
68
|
+
|
|
69
|
+
func start(intervalMs ms: Double) {
|
|
70
|
+
queue.async { [weak self] in
|
|
71
|
+
guard let self = self else { return }
|
|
72
|
+
let prev = self.intervalMs
|
|
73
|
+
self.intervalMs = ms
|
|
74
|
+
if self.running, let t = self.timer {
|
|
75
|
+
// Skip reschedule when interval is unchanged — calling
|
|
76
|
+
// `schedule` on a running source resets the next deadline,
|
|
77
|
+
// which would briefly extend the gap unnecessarily.
|
|
78
|
+
if prev != ms {
|
|
79
|
+
t.schedule(
|
|
80
|
+
deadline: .now() + .milliseconds(Int(ms)),
|
|
81
|
+
repeating: .milliseconds(Int(ms))
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
self.running = true
|
|
87
|
+
let t = DispatchSource.makeTimerSource(queue: self.queue)
|
|
88
|
+
t.schedule(
|
|
89
|
+
deadline: .now() + .milliseconds(Int(ms)),
|
|
90
|
+
repeating: .milliseconds(Int(ms))
|
|
91
|
+
)
|
|
92
|
+
t.setEventHandler { [weak self] in
|
|
93
|
+
guard let self = self, self.running else { return }
|
|
94
|
+
let s = self.takeSample()
|
|
95
|
+
Overlay.shared.update(sample: s)
|
|
96
|
+
self.checkAnomalyAndLog(sample: s)
|
|
97
|
+
}
|
|
98
|
+
self.timer = t
|
|
99
|
+
t.resume()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func stop() {
|
|
104
|
+
queue.async { [weak self] in
|
|
105
|
+
guard let self = self else { return }
|
|
106
|
+
self.running = false
|
|
107
|
+
self.timer?.cancel()
|
|
108
|
+
self.timer = nil
|
|
109
|
+
}
|
|
110
|
+
Overlay.shared.hide()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func takeSample() -> PerfSample {
|
|
114
|
+
let nowMono = monotonicSec()
|
|
115
|
+
let nowCpu = processCpuSec()
|
|
116
|
+
let rssBytes = residentBytes()
|
|
117
|
+
let nowWallMs = Date().timeIntervalSince1970 * 1000.0
|
|
118
|
+
|
|
119
|
+
var cpuPct: Double = 0
|
|
120
|
+
lock.lock()
|
|
121
|
+
if nowCpu >= 0 && lastCpuSec >= 0 && lastMonoSec > 0 {
|
|
122
|
+
let dCpu = nowCpu - lastCpuSec
|
|
123
|
+
let dWall = nowMono - lastMonoSec
|
|
124
|
+
if dWall > 0 && dCpu >= 0 {
|
|
125
|
+
cpuPct = (dCpu / dWall) * 100.0
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if nowCpu >= 0 {
|
|
129
|
+
lastCpuSec = nowCpu
|
|
130
|
+
lastMonoSec = nowMono
|
|
131
|
+
}
|
|
132
|
+
lock.unlock()
|
|
133
|
+
|
|
134
|
+
return PerfSample(
|
|
135
|
+
cpu: cpuPct,
|
|
136
|
+
rss: Double(rssBytes),
|
|
137
|
+
timestamp: nowWallMs
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Emits a warn to native-logger when CPU or RSS has stayed over its
|
|
142
|
+
/// threshold for `kAnomalySustainSamples` consecutive samples. Only
|
|
143
|
+
/// called from the periodic timer; one-off `sample()` calls do not
|
|
144
|
+
/// trip this path. Each metric tracks its own counter and cooldown.
|
|
145
|
+
private func checkAnomalyAndLog(sample s: PerfSample) {
|
|
146
|
+
let nowSec = monotonicSec()
|
|
147
|
+
|
|
148
|
+
if s.cpu >= kCpuAnomalyPct {
|
|
149
|
+
cpuOverCount += 1
|
|
150
|
+
if cpuOverCount >= kAnomalySustainSamples,
|
|
151
|
+
nowSec - lastCpuLogSec >= kAnomalyCooldownSec {
|
|
152
|
+
OneKeyLog.warn(
|
|
153
|
+
kTag,
|
|
154
|
+
String(
|
|
155
|
+
format: "Sustained high CPU: %.1f%% over %d samples",
|
|
156
|
+
s.cpu, cpuOverCount
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
lastCpuLogSec = nowSec
|
|
160
|
+
cpuOverCount = 0
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
cpuOverCount = 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if s.rss >= kRssAnomalyBytes {
|
|
167
|
+
rssOverCount += 1
|
|
168
|
+
if rssOverCount >= kAnomalySustainSamples,
|
|
169
|
+
nowSec - lastRssLogSec >= kAnomalyCooldownSec {
|
|
170
|
+
let mb = s.rss / 1024.0 / 1024.0
|
|
171
|
+
OneKeyLog.warn(
|
|
172
|
+
kTag,
|
|
173
|
+
String(
|
|
174
|
+
format: "Sustained high RSS: %.1f MB over %d samples",
|
|
175
|
+
mb, rssOverCount
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
lastRssLogSec = nowSec
|
|
179
|
+
rssOverCount = 0
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
rssOverCount = 0
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func monotonicSec() -> Double {
|
|
187
|
+
var ts = timespec()
|
|
188
|
+
if clock_gettime(CLOCK_MONOTONIC, &ts) != 0 {
|
|
189
|
+
return Date().timeIntervalSince1970
|
|
190
|
+
}
|
|
191
|
+
return Double(ts.tv_sec) + Double(ts.tv_nsec) / 1_000_000_000.0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Aggregate CPU time consumed by all threads (live + terminated) of the
|
|
195
|
+
/// current process, in seconds. Returns -1 on failure.
|
|
196
|
+
private func processCpuSec() -> Double {
|
|
197
|
+
var ts = timespec()
|
|
198
|
+
if clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts) != 0 {
|
|
199
|
+
OneKeyLog.warn(kTag, "clock_gettime(CLOCK_PROCESS_CPUTIME_ID) failed: errno=\(errno)")
|
|
200
|
+
return -1
|
|
201
|
+
}
|
|
202
|
+
return Double(ts.tv_sec) + Double(ts.tv_nsec) / 1_000_000_000.0
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func residentBytes() -> UInt64 {
|
|
206
|
+
var vmInfo = task_vm_info_data_t()
|
|
207
|
+
var count = mach_msg_type_number_t(
|
|
208
|
+
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size
|
|
209
|
+
)
|
|
210
|
+
let kr = withUnsafeMutablePointer(to: &vmInfo) { ptr in
|
|
211
|
+
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in
|
|
212
|
+
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if kr == KERN_SUCCESS {
|
|
216
|
+
return UInt64(vmInfo.phys_footprint)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var basicInfo = mach_task_basic_info_data_t()
|
|
220
|
+
var basicCount = mach_msg_type_number_t(
|
|
221
|
+
MemoryLayout<mach_task_basic_info_data_t>.size / MemoryLayout<natural_t>.size
|
|
222
|
+
)
|
|
223
|
+
let basicKr = withUnsafeMutablePointer(to: &basicInfo) { ptr in
|
|
224
|
+
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(basicCount)) { intPtr in
|
|
225
|
+
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), intPtr, &basicCount)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if basicKr == KERN_SUCCESS {
|
|
229
|
+
return UInt64(basicInfo.resident_size)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
OneKeyLog.warn(kTag, "Failed to read resident memory; vmKr=\(kr) basicKr=\(basicKr)")
|
|
233
|
+
return 0
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// MARK: - Overlay
|
|
238
|
+
//
|
|
239
|
+
// Singleton UILabel attached to the current key UIWindow. Updates always
|
|
240
|
+
// dispatch to main. No floating-window permission needed; overlay only
|
|
241
|
+
// shows while the app is in the foreground.
|
|
242
|
+
//
|
|
243
|
+
// Inherits NSObject so UIPanGestureRecognizer's target/action selector
|
|
244
|
+
// dispatch resolves cleanly via Obj-C runtime.
|
|
245
|
+
|
|
246
|
+
private final class Overlay: NSObject {
|
|
247
|
+
static let shared = Overlay()
|
|
248
|
+
|
|
249
|
+
private override init() { super.init() }
|
|
250
|
+
|
|
251
|
+
private var label: UILabel?
|
|
252
|
+
private var visible = false
|
|
253
|
+
|
|
254
|
+
// `_lastSample` is written by the Sampler timer thread and read by the
|
|
255
|
+
// main thread in attach() and inside the coalesced update closure.
|
|
256
|
+
// Optional<struct> is not atomic in Swift, so guard with a lock to avoid
|
|
257
|
+
// torn reads / undefined behaviour. `_updatePending` is part of the same
|
|
258
|
+
// protected state to coalesce overlay refreshes.
|
|
259
|
+
private let sampleLock = NSLock()
|
|
260
|
+
private var _lastSample: PerfSample?
|
|
261
|
+
private var _updatePending = false
|
|
262
|
+
|
|
263
|
+
private var lastSample: PerfSample? {
|
|
264
|
+
get {
|
|
265
|
+
sampleLock.lock(); defer { sampleLock.unlock() }
|
|
266
|
+
return _lastSample
|
|
267
|
+
}
|
|
268
|
+
set {
|
|
269
|
+
sampleLock.lock(); defer { sampleLock.unlock() }
|
|
270
|
+
_lastSample = newValue
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func show() {
|
|
275
|
+
visible = true
|
|
276
|
+
DispatchQueue.main.async { [weak self] in self?.attach() }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func hide() {
|
|
280
|
+
visible = false
|
|
281
|
+
DispatchQueue.main.async { [weak self] in self?.detach() }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Coalesce updates: if the main thread is blocked, we don't want N
|
|
285
|
+
/// label.text writes piling up only to all fire when it unblocks. The
|
|
286
|
+
/// `_updatePending` flag (held under sampleLock) ensures at most one
|
|
287
|
+
/// pending dispatch at a time, and the closure always reads the
|
|
288
|
+
/// latest sample on main.
|
|
289
|
+
func update(sample: PerfSample) {
|
|
290
|
+
sampleLock.lock()
|
|
291
|
+
_lastSample = sample
|
|
292
|
+
let shouldPost = !_updatePending
|
|
293
|
+
if shouldPost { _updatePending = true }
|
|
294
|
+
sampleLock.unlock()
|
|
295
|
+
|
|
296
|
+
if !shouldPost { return }
|
|
297
|
+
|
|
298
|
+
DispatchQueue.main.async { [weak self] in
|
|
299
|
+
guard let self = self else { return }
|
|
300
|
+
self.sampleLock.lock()
|
|
301
|
+
self._updatePending = false
|
|
302
|
+
let snapshot = self._lastSample
|
|
303
|
+
self.sampleLock.unlock()
|
|
304
|
+
guard self.visible, let snapshot = snapshot else { return }
|
|
305
|
+
self.label?.text = self.renderText(snapshot)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private func attach() {
|
|
310
|
+
if label != nil { return }
|
|
311
|
+
guard let window = currentKeyWindow() else {
|
|
312
|
+
OneKeyLog.warn(kTag, "No key UIWindow available; overlay deferred")
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let lbl = UILabel(frame: CGRect(x: 30, y: 100, width: 160, height: 60))
|
|
317
|
+
lbl.backgroundColor = UIColor.black.withAlphaComponent(0.7)
|
|
318
|
+
lbl.layer.cornerRadius = 8
|
|
319
|
+
lbl.layer.masksToBounds = true
|
|
320
|
+
lbl.textColor = .white
|
|
321
|
+
lbl.font = .systemFont(ofSize: 13, weight: .bold)
|
|
322
|
+
lbl.textAlignment = .center
|
|
323
|
+
lbl.numberOfLines = 2
|
|
324
|
+
lbl.text = "CPU: --\nRAM: --"
|
|
325
|
+
lbl.isUserInteractionEnabled = true
|
|
326
|
+
|
|
327
|
+
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
328
|
+
lbl.addGestureRecognizer(pan)
|
|
329
|
+
|
|
330
|
+
window.addSubview(lbl)
|
|
331
|
+
label = lbl
|
|
332
|
+
|
|
333
|
+
if let s = lastSample {
|
|
334
|
+
lbl.text = renderText(s)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private func detach() {
|
|
339
|
+
label?.removeFromSuperview()
|
|
340
|
+
label = nil
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
344
|
+
guard let view = gesture.view, let parent = view.superview else { return }
|
|
345
|
+
let translation = gesture.translation(in: parent)
|
|
346
|
+
let newCenter = CGPoint(
|
|
347
|
+
x: view.center.x + translation.x,
|
|
348
|
+
y: view.center.y + translation.y
|
|
349
|
+
)
|
|
350
|
+
let halfW = view.bounds.size.width / 2
|
|
351
|
+
let halfH = view.bounds.size.height / 2
|
|
352
|
+
view.center = CGPoint(
|
|
353
|
+
x: max(halfW, min(parent.bounds.size.width - halfW, newCenter.x)),
|
|
354
|
+
y: max(halfH, min(parent.bounds.size.height - halfH, newCenter.y))
|
|
355
|
+
)
|
|
356
|
+
gesture.setTranslation(.zero, in: parent)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private func renderText(_ s: PerfSample) -> String {
|
|
360
|
+
let cpuStr = s.cpu > 0 ? String(format: "%.1f%%", s.cpu) : "--"
|
|
361
|
+
let mb = s.rss / 1024.0 / 1024.0
|
|
362
|
+
let memStr = mb > 0 ? String(format: "%.1f MB", mb) : "--"
|
|
363
|
+
return "CPU: \(cpuStr)\nRAM: \(memStr)"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private func currentKeyWindow() -> UIWindow? {
|
|
367
|
+
// iOS 15+ preferred path
|
|
368
|
+
if #available(iOS 15.0, *) {
|
|
369
|
+
for scene in UIApplication.shared.connectedScenes {
|
|
370
|
+
guard
|
|
371
|
+
let windowScene = scene as? UIWindowScene,
|
|
372
|
+
windowScene.activationState == .foregroundActive
|
|
373
|
+
else { continue }
|
|
374
|
+
if let kw = windowScene.keyWindow {
|
|
375
|
+
return kw
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Fallback: scan all connected scenes
|
|
380
|
+
for scene in UIApplication.shared.connectedScenes {
|
|
381
|
+
guard let windowScene = scene as? UIWindowScene else { continue }
|
|
382
|
+
if let kw = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
|
383
|
+
return kw
|
|
384
|
+
}
|
|
385
|
+
if let any = windowScene.windows.first {
|
|
386
|
+
return any
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return nil
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["ReactNativePerfStats.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
4
|
+
const ReactNativePerfStatsHybridObject = NitroModules.createHybridObject('ReactNativePerfStats');
|
|
5
|
+
export const ReactNativePerfStats = ReactNativePerfStatsHybridObject;
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NitroModules","ReactNativePerfStatsHybridObject","createHybridObject","ReactNativePerfStats"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAGzD,MAAMC,gCAAgC,GACpCD,YAAY,CAACE,kBAAkB,CAA2B,sBAAsB,CAAC;AAEnF,OAAO,MAAMC,oBAAoB,GAAGF,gCAAgC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
export interface PerfSample {
|
|
3
|
+
/**
|
|
4
|
+
* Process CPU usage as a percentage of one core, computed as
|
|
5
|
+
* `(deltaCpuTime / deltaWallTime) * 100` against the previous sample.
|
|
6
|
+
* Multi-core saturation can exceed 100. The first sample after process
|
|
7
|
+
* launch returns `0` because no baseline exists yet.
|
|
8
|
+
*/
|
|
9
|
+
cpu: number;
|
|
10
|
+
/** Resident set size in bytes. iOS: phys_footprint; Android: VmRSS. */
|
|
11
|
+
rss: number;
|
|
12
|
+
/** Wall-clock timestamp (ms since unix epoch) when the sample was taken. */
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ReactNativePerfStats extends HybridObject<{
|
|
16
|
+
ios: 'swift';
|
|
17
|
+
android: 'kotlin';
|
|
18
|
+
}> {
|
|
19
|
+
/**
|
|
20
|
+
* Start the singleton native sampler.
|
|
21
|
+
*
|
|
22
|
+
* - Runs on a dedicated background thread (Android: HandlerThread;
|
|
23
|
+
* iOS: dispatch source on a global queue), so it survives JS-thread
|
|
24
|
+
* blockages — the overlay keeps updating even when JS is frozen.
|
|
25
|
+
* - Idempotent: calling `start` again only updates the interval; it does
|
|
26
|
+
* not spawn additional timers.
|
|
27
|
+
*/
|
|
28
|
+
start(intervalMs: number): void;
|
|
29
|
+
/** Stop the sampler. Also hides the overlay if shown. No-op if not running. */
|
|
30
|
+
stop(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Show the floating overlay (CPU + RAM) drawn natively.
|
|
33
|
+
*
|
|
34
|
+
* - Android: TextView attached to the current Activity via
|
|
35
|
+
* `addContentView` — no floating-window permission required.
|
|
36
|
+
* - iOS: UILabel added to the key UIWindow.
|
|
37
|
+
* - Draggable on both platforms.
|
|
38
|
+
* - Idempotent. If the sampler is not running, the overlay shows "--"
|
|
39
|
+
* until `start` is called.
|
|
40
|
+
*/
|
|
41
|
+
showOverlay(): void;
|
|
42
|
+
/** Hide the floating overlay. The sampler keeps running. No-op if not shown. */
|
|
43
|
+
hideOverlay(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Take one CPU+RAM sample without affecting the overlay timer.
|
|
46
|
+
* Cheap (~1ms) and runs off the JS thread via Promise.async.
|
|
47
|
+
* Shares the same delta baseline as the overlay sampler.
|
|
48
|
+
*/
|
|
49
|
+
sample(): Promise<PerfSample>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=ReactNativePerfStats.nitro.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativePerfStats.nitro.d.ts","sourceRoot":"","sources":["../../../src/ReactNativePerfStats.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhC,+EAA+E;IAC/E,IAAI,IAAI,IAAI,CAAC;IAEb;;;;;;;;;OASG;IACH,WAAW,IAAI,IAAI,CAAC;IAEpB,gFAAgF;IAChF,WAAW,IAAI,IAAI,CAAC;IAEpB;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,IAAI,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAKrG,eAAO,MAAM,oBAAoB,0BAAmC,CAAC;AACrE,mBAAmB,8BAA8B,CAAC"}
|
package/nitro.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cxxNamespace": ["reactnativeperfstats"],
|
|
3
|
+
"ios": {
|
|
4
|
+
"iosModuleName": "ReactNativePerfStats"
|
|
5
|
+
},
|
|
6
|
+
"android": {
|
|
7
|
+
"androidNamespace": ["reactnativeperfstats"],
|
|
8
|
+
"androidCxxLibName": "reactnativeperfstats"
|
|
9
|
+
},
|
|
10
|
+
"autolinking": {
|
|
11
|
+
"ReactNativePerfStats": {
|
|
12
|
+
"swift": "ReactNativePerfStats",
|
|
13
|
+
"kotlin": "ReactNativePerfStats"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"ignorePaths": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JHybridReactNativePerfStatsSpec.cpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © 2026 Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#include "JHybridReactNativePerfStatsSpec.hpp"
|
|
9
|
+
|
|
10
|
+
// Forward declaration of `PerfSample` to properly resolve imports.
|
|
11
|
+
namespace margelo::nitro::reactnativeperfstats { struct PerfSample; }
|
|
12
|
+
|
|
13
|
+
#include "PerfSample.hpp"
|
|
14
|
+
#include <NitroModules/Promise.hpp>
|
|
15
|
+
#include <NitroModules/JPromise.hpp>
|
|
16
|
+
#include "JPerfSample.hpp"
|
|
17
|
+
|
|
18
|
+
namespace margelo::nitro::reactnativeperfstats {
|
|
19
|
+
|
|
20
|
+
jni::local_ref<JHybridReactNativePerfStatsSpec::jhybriddata> JHybridReactNativePerfStatsSpec::initHybrid(jni::alias_ref<jhybridobject> jThis) {
|
|
21
|
+
return makeCxxInstance(jThis);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
void JHybridReactNativePerfStatsSpec::registerNatives() {
|
|
25
|
+
registerHybrid({
|
|
26
|
+
makeNativeMethod("initHybrid", JHybridReactNativePerfStatsSpec::initHybrid),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
size_t JHybridReactNativePerfStatsSpec::getExternalMemorySize() noexcept {
|
|
31
|
+
static const auto method = javaClassStatic()->getMethod<jlong()>("getMemorySize");
|
|
32
|
+
return method(_javaPart);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void JHybridReactNativePerfStatsSpec::dispose() noexcept {
|
|
36
|
+
static const auto method = javaClassStatic()->getMethod<void()>("dispose");
|
|
37
|
+
method(_javaPart);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
std::string JHybridReactNativePerfStatsSpec::toString() {
|
|
41
|
+
static const auto method = javaClassStatic()->getMethod<jni::JString()>("toString");
|
|
42
|
+
auto javaString = method(_javaPart);
|
|
43
|
+
return javaString->toStdString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Properties
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
// Methods
|
|
50
|
+
void JHybridReactNativePerfStatsSpec::start(double intervalMs) {
|
|
51
|
+
static const auto method = javaClassStatic()->getMethod<void(double /* intervalMs */)>("start");
|
|
52
|
+
method(_javaPart, intervalMs);
|
|
53
|
+
}
|
|
54
|
+
void JHybridReactNativePerfStatsSpec::stop() {
|
|
55
|
+
static const auto method = javaClassStatic()->getMethod<void()>("stop");
|
|
56
|
+
method(_javaPart);
|
|
57
|
+
}
|
|
58
|
+
void JHybridReactNativePerfStatsSpec::showOverlay() {
|
|
59
|
+
static const auto method = javaClassStatic()->getMethod<void()>("showOverlay");
|
|
60
|
+
method(_javaPart);
|
|
61
|
+
}
|
|
62
|
+
void JHybridReactNativePerfStatsSpec::hideOverlay() {
|
|
63
|
+
static const auto method = javaClassStatic()->getMethod<void()>("hideOverlay");
|
|
64
|
+
method(_javaPart);
|
|
65
|
+
}
|
|
66
|
+
std::shared_ptr<Promise<PerfSample>> JHybridReactNativePerfStatsSpec::sample() {
|
|
67
|
+
static const auto method = javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>()>("sample");
|
|
68
|
+
auto __result = method(_javaPart);
|
|
69
|
+
return [&]() {
|
|
70
|
+
auto __promise = Promise<PerfSample>::create();
|
|
71
|
+
__result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
|
|
72
|
+
auto __result = jni::static_ref_cast<JPerfSample>(__boxedResult);
|
|
73
|
+
__promise->resolve(__result->toCpp());
|
|
74
|
+
});
|
|
75
|
+
__result->cthis()->addOnRejectedListener([=](const jni::alias_ref<jni::JThrowable>& __throwable) {
|
|
76
|
+
jni::JniException __jniError(__throwable);
|
|
77
|
+
__promise->reject(std::make_exception_ptr(__jniError));
|
|
78
|
+
});
|
|
79
|
+
return __promise;
|
|
80
|
+
}();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
} // namespace margelo::nitro::reactnativeperfstats
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// HybridReactNativePerfStatsSpec.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © 2026 Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <NitroModules/JHybridObject.hpp>
|
|
11
|
+
#include <fbjni/fbjni.h>
|
|
12
|
+
#include "HybridReactNativePerfStatsSpec.hpp"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
namespace margelo::nitro::reactnativeperfstats {
|
|
18
|
+
|
|
19
|
+
using namespace facebook;
|
|
20
|
+
|
|
21
|
+
class JHybridReactNativePerfStatsSpec: public jni::HybridClass<JHybridReactNativePerfStatsSpec, JHybridObject>,
|
|
22
|
+
public virtual HybridReactNativePerfStatsSpec {
|
|
23
|
+
public:
|
|
24
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/reactnativeperfstats/HybridReactNativePerfStatsSpec;";
|
|
25
|
+
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
|
|
26
|
+
static void registerNatives();
|
|
27
|
+
|
|
28
|
+
protected:
|
|
29
|
+
// C++ constructor (called from Java via `initHybrid()`)
|
|
30
|
+
explicit JHybridReactNativePerfStatsSpec(jni::alias_ref<jhybridobject> jThis) :
|
|
31
|
+
HybridObject(HybridReactNativePerfStatsSpec::TAG),
|
|
32
|
+
HybridBase(jThis),
|
|
33
|
+
_javaPart(jni::make_global(jThis)) {}
|
|
34
|
+
|
|
35
|
+
public:
|
|
36
|
+
~JHybridReactNativePerfStatsSpec() override {
|
|
37
|
+
// Hermes GC can destroy JS objects on a non-JNI Thread.
|
|
38
|
+
jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public:
|
|
42
|
+
size_t getExternalMemorySize() noexcept override;
|
|
43
|
+
void dispose() noexcept override;
|
|
44
|
+
std::string toString() override;
|
|
45
|
+
|
|
46
|
+
public:
|
|
47
|
+
inline const jni::global_ref<JHybridReactNativePerfStatsSpec::javaobject>& getJavaPart() const noexcept {
|
|
48
|
+
return _javaPart;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public:
|
|
52
|
+
// Properties
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
public:
|
|
56
|
+
// Methods
|
|
57
|
+
void start(double intervalMs) override;
|
|
58
|
+
void stop() override;
|
|
59
|
+
void showOverlay() override;
|
|
60
|
+
void hideOverlay() override;
|
|
61
|
+
std::shared_ptr<Promise<PerfSample>> sample() override;
|
|
62
|
+
|
|
63
|
+
private:
|
|
64
|
+
friend HybridBase;
|
|
65
|
+
using HybridBase::HybridBase;
|
|
66
|
+
jni::global_ref<JHybridReactNativePerfStatsSpec::javaobject> _javaPart;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
} // namespace margelo::nitro::reactnativeperfstats
|