@onekeyfe/react-native-perf-stats 3.0.35 → 3.0.37
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/android/src/main/java/com/margelo/nitro/reactnativeperfstats/PerfStatsInitProvider.kt +11 -4
- package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStats.kt +333 -24
- package/ios/ReactNativePerfStats.swift +360 -15
- package/lib/module/index.js +77 -5
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/ReactNativePerfStats.nitro.d.ts +109 -1
- package/lib/typescript/src/ReactNativePerfStats.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +31 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_MemoryWarningEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JHybridReactNativePerfStatsSpec.cpp +24 -0
- package/nitrogen/generated/android/c++/JHybridReactNativePerfStatsSpec.hpp +3 -0
- package/nitrogen/generated/android/c++/JMemoryWarningEvent.hpp +66 -0
- package/nitrogen/generated/android/c++/JMemoryWarningLevel.hpp +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/Func_void_MemoryWarningEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/HybridReactNativePerfStatsSpec.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/MemoryWarningEvent.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/MemoryWarningLevel.kt +21 -0
- package/nitrogen/generated/android/reactnativeperfstatsOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Bridge.hpp +37 -0
- package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Umbrella.hpp +7 -0
- package/nitrogen/generated/ios/c++/HybridReactNativePerfStatsSpecSwift.hpp +27 -0
- package/nitrogen/generated/ios/swift/Func_void_MemoryWarningEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridReactNativePerfStatsSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridReactNativePerfStatsSpec_cxx.swift +39 -0
- package/nitrogen/generated/ios/swift/MemoryWarningEvent.swift +58 -0
- package/nitrogen/generated/ios/swift/MemoryWarningLevel.swift +40 -0
- package/nitrogen/generated/shared/c++/HybridReactNativePerfStatsSpec.cpp +3 -0
- package/nitrogen/generated/shared/c++/HybridReactNativePerfStatsSpec.hpp +7 -0
- package/nitrogen/generated/shared/c++/MemoryWarningEvent.hpp +84 -0
- package/nitrogen/generated/shared/c++/MemoryWarningLevel.hpp +76 -0
- package/package.json +1 -1
- package/src/ReactNativePerfStats.nitro.ts +114 -1
- package/src/index.tsx +90 -5
|
@@ -2,9 +2,16 @@ import NitroModules
|
|
|
2
2
|
import ReactNativeNativeLogger
|
|
3
3
|
import Darwin
|
|
4
4
|
import UIKit
|
|
5
|
+
import WebKit
|
|
6
|
+
// URLCache and Date come transitively from UIKit. malloc_zone_pressure_relief
|
|
7
|
+
// lives in <malloc/malloc.h>, which is reached via `import Darwin`.
|
|
8
|
+
import Foundation
|
|
5
9
|
|
|
6
10
|
private let kTag = "PerfStats"
|
|
7
11
|
private let kMinIntervalMs: Double = 200
|
|
12
|
+
// 24 h. The interval feeds DispatchSource.schedule(deadline: .now() + .milliseconds(Int(ms))),
|
|
13
|
+
// and Int(Double) traps on values that don't fit in Int — cap before the cast.
|
|
14
|
+
private let kMaxIntervalMs: Double = 86_400_000
|
|
8
15
|
|
|
9
16
|
// Anomaly logging thresholds. We only emit a warn after the metric has
|
|
10
17
|
// stayed over the threshold for kAnomalySustainSamples in a row, to
|
|
@@ -26,7 +33,12 @@ private let kJsFpsHintTtlSec: Double = 2.0
|
|
|
26
33
|
class ReactNativePerfStats: HybridReactNativePerfStatsSpec {
|
|
27
34
|
|
|
28
35
|
func start(intervalMs: Double) throws {
|
|
29
|
-
|
|
36
|
+
// Sanitise the JS-supplied value: NaN/Inf would trap on Int(...)
|
|
37
|
+
// inside DispatchSource scheduling, and max(NaN, x) returns NaN
|
|
38
|
+
// in Swift so the lower-bound clamp below can't catch it.
|
|
39
|
+
let finite = intervalMs.isFinite ? intervalMs : 1000.0
|
|
40
|
+
let clamped = min(max(finite, kMinIntervalMs), kMaxIntervalMs)
|
|
41
|
+
Sampler.shared.start(intervalMs: clamped)
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
func stop() throws {
|
|
@@ -43,13 +55,38 @@ class ReactNativePerfStats: HybridReactNativePerfStatsSpec {
|
|
|
43
55
|
|
|
44
56
|
func sample() throws -> Promise<PerfSample> {
|
|
45
57
|
return Promise.async {
|
|
46
|
-
|
|
58
|
+
// recordBaseline=false: one-shot reads must not write the
|
|
59
|
+
// CPU baseline used by the periodic tick. See takeSample.
|
|
60
|
+
return Sampler.shared.takeSample(recordBaseline: false)
|
|
47
61
|
}
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
func setJsFpsHint(fps: Double) throws {
|
|
65
|
+
// Drop NaN/Inf at the boundary; JsFpsHolder caches the value and
|
|
66
|
+
// the overlay would happily render "inf fps" otherwise.
|
|
67
|
+
guard fps.isFinite else { return }
|
|
51
68
|
JsFpsHolder.shared.set(fps: fps)
|
|
52
69
|
}
|
|
70
|
+
|
|
71
|
+
func addMemoryWarningListener(callback: @escaping (MemoryWarningEvent) -> Void) throws -> Double {
|
|
72
|
+
return Double(MemoryWarningCenter.shared.add(callback: callback))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func removeMemoryWarningListener(id: Double) throws {
|
|
76
|
+
// Int(exactly:) returns nil for NaN/Inf/non-integral/out-of-range
|
|
77
|
+
// values; plain Int(NaN) traps. We treat any of those as "unknown
|
|
78
|
+
// id", consistent with the doc note that removal of unknown ids
|
|
79
|
+
// is a no-op.
|
|
80
|
+
guard let intId = Int(exactly: id) else { return }
|
|
81
|
+
MemoryWarningCenter.shared.remove(id: intId)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func cleanupNativeCaches() throws {
|
|
85
|
+
// Caller-initiated: pass rssBefore=nil so the bg reclaim-delta
|
|
86
|
+
// log is skipped (the OS-warning path is the only one that
|
|
87
|
+
// wants that "before vs after" line; on-demand callers don't).
|
|
88
|
+
MemoryWarningCenter.shared.performNativeCleanup()
|
|
89
|
+
}
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
// MARK: - Sampler
|
|
@@ -82,9 +119,9 @@ private final class Sampler {
|
|
|
82
119
|
private var lastUiFpsLogSec: Double = 0
|
|
83
120
|
private var lastJsFpsLogSec: Double = 0
|
|
84
121
|
|
|
85
|
-
// Last UI FPS computed by the periodic timer.
|
|
86
|
-
//
|
|
87
|
-
//
|
|
122
|
+
// Last UI FPS computed by the periodic timer. Guarded by `lock`:
|
|
123
|
+
// the periodic tick writes from the sampler queue while takeSample()
|
|
124
|
+
// may read from Nitro's worker queue when called via Promise.async.
|
|
88
125
|
private var lastUiFps: Double = 0
|
|
89
126
|
|
|
90
127
|
func start(intervalMs ms: Double) {
|
|
@@ -104,6 +141,16 @@ private final class Sampler {
|
|
|
104
141
|
}
|
|
105
142
|
return
|
|
106
143
|
}
|
|
144
|
+
// Cold-start path: reset CPU baseline now, before scheduling
|
|
145
|
+
// the timer. Doing it here rather than in stop() also stops
|
|
146
|
+
// sample() (Promise.async on a worker queue, not this serial
|
|
147
|
+
// queue) from writing a fresh baseline between stop() and
|
|
148
|
+
// start(). One-shot sample() now passes recordBaseline=false
|
|
149
|
+
// and the periodic tick is the only writer.
|
|
150
|
+
self.lock.lock()
|
|
151
|
+
self.lastCpuSec = -1
|
|
152
|
+
self.lastMonoSec = -1
|
|
153
|
+
self.lock.unlock()
|
|
107
154
|
self.running = true
|
|
108
155
|
let t = DispatchSource.makeTimerSource(queue: self.queue)
|
|
109
156
|
t.schedule(
|
|
@@ -114,7 +161,10 @@ private final class Sampler {
|
|
|
114
161
|
guard let self = self, self.running else { return }
|
|
115
162
|
// Refresh cached UI FPS *before* takeSample reads it, so
|
|
116
163
|
// the value covers exactly the just-elapsed interval.
|
|
117
|
-
|
|
164
|
+
let newUiFps = UiFpsMonitor.shared.readAndReset(nowSec: self.monotonicSec())
|
|
165
|
+
self.lock.lock()
|
|
166
|
+
self.lastUiFps = newUiFps
|
|
167
|
+
self.lock.unlock()
|
|
118
168
|
let s = self.takeSample()
|
|
119
169
|
Overlay.shared.update(sample: s)
|
|
120
170
|
self.checkAnomalyAndLog(sample: s)
|
|
@@ -131,19 +181,29 @@ private final class Sampler {
|
|
|
131
181
|
self.running = false
|
|
132
182
|
self.timer?.cancel()
|
|
133
183
|
self.timer = nil
|
|
184
|
+
self.lock.lock()
|
|
134
185
|
self.lastUiFps = 0
|
|
186
|
+
self.lock.unlock()
|
|
135
187
|
}
|
|
136
188
|
UiFpsMonitor.shared.stop()
|
|
137
189
|
Overlay.shared.hide()
|
|
138
190
|
}
|
|
139
191
|
|
|
140
|
-
|
|
192
|
+
/// - parameter recordBaseline: `true` for the periodic timer (the
|
|
193
|
+
/// next sample's CPU delta is computed against this read); `false`
|
|
194
|
+
/// for one-shot `sample()` calls so they don't pollute the periodic
|
|
195
|
+
/// CPU% baseline. A one-shot call between two periodic ticks (or
|
|
196
|
+
/// between stop() and start()) would otherwise insert a new
|
|
197
|
+
/// baseline that the next periodic tick reads, producing a CPU%
|
|
198
|
+
/// that spans the gap.
|
|
199
|
+
func takeSample(recordBaseline: Bool = true) -> PerfSample {
|
|
141
200
|
let nowMono = monotonicSec()
|
|
142
201
|
let nowCpu = processCpuSec()
|
|
143
202
|
let rssBytes = residentBytes()
|
|
144
203
|
let nowWallMs = Date().timeIntervalSince1970 * 1000.0
|
|
145
204
|
|
|
146
205
|
var cpuPct: Double = 0
|
|
206
|
+
var uiFps: Double = 0
|
|
147
207
|
lock.lock()
|
|
148
208
|
if nowCpu >= 0 && lastCpuSec >= 0 && lastMonoSec > 0 {
|
|
149
209
|
let dCpu = nowCpu - lastCpuSec
|
|
@@ -152,16 +212,17 @@ private final class Sampler {
|
|
|
152
212
|
cpuPct = (dCpu / dWall) * 100.0
|
|
153
213
|
}
|
|
154
214
|
}
|
|
155
|
-
if nowCpu >= 0 {
|
|
215
|
+
if recordBaseline && nowCpu >= 0 {
|
|
156
216
|
lastCpuSec = nowCpu
|
|
157
217
|
lastMonoSec = nowMono
|
|
158
218
|
}
|
|
219
|
+
uiFps = lastUiFps
|
|
159
220
|
lock.unlock()
|
|
160
221
|
|
|
161
222
|
return PerfSample(
|
|
162
223
|
cpu: cpuPct,
|
|
163
224
|
rss: Double(rssBytes),
|
|
164
|
-
uiFps:
|
|
225
|
+
uiFps: uiFps,
|
|
165
226
|
jsFps: JsFpsHolder.shared.read(nowSec: nowMono),
|
|
166
227
|
timestamp: nowWallMs
|
|
167
228
|
)
|
|
@@ -270,6 +331,24 @@ private final class Sampler {
|
|
|
270
331
|
return Double(ts.tv_sec) + Double(ts.tv_nsec) / 1_000_000_000.0
|
|
271
332
|
}
|
|
272
333
|
|
|
334
|
+
/// Public alias used by `MemoryWarningCenter` to attach an RSS reading
|
|
335
|
+
/// to each emitted event. Reads `phys_footprint` (or `resident_size`
|
|
336
|
+
/// as a fallback) on the calling thread; safe to call from main.
|
|
337
|
+
func residentBytesPublic() -> UInt64 {
|
|
338
|
+
return residentBytes()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Primary path is `phys_footprint` (TASK_VM_INFO) — the same metric
|
|
342
|
+
// iOS jetsam uses for pressure decisions; it includes IOKit
|
|
343
|
+
// accounting and dirty pages credited to this process. The fallback
|
|
344
|
+
// reads `resident_size` (MACH_TASK_BASIC_INFO), which is the raw
|
|
345
|
+
// resident-page count and is semantically smaller and noisier.
|
|
346
|
+
//
|
|
347
|
+
// The two values are NOT interchangeable: if TASK_VM_INFO ever fails
|
|
348
|
+
// mid-run, live readings will step DOWN on the next sample without
|
|
349
|
+
// the underlying memory state having actually changed. TASK_VM_INFO
|
|
350
|
+
// is well supported on iOS 12+, so this is largely defensive — but
|
|
351
|
+
// the discontinuity is worth knowing about when reading the logs.
|
|
273
352
|
private func residentBytes() -> UInt64 {
|
|
274
353
|
var vmInfo = task_vm_info_data_t()
|
|
275
354
|
var count = mach_msg_type_number_t(
|
|
@@ -442,23 +521,60 @@ private final class OverlayPassthroughWindow: UIWindow {
|
|
|
442
521
|
}
|
|
443
522
|
}
|
|
444
523
|
|
|
524
|
+
/// Host VC for the overlay window. Exists only to give us a
|
|
525
|
+
/// `viewWillTransition(to:with:)` hook so the label can be re-clamped
|
|
526
|
+
/// after rotation / split-screen size changes. Without this, after a
|
|
527
|
+
/// rotation the label's center could land outside the new parent.bounds
|
|
528
|
+
/// and the user would have to drag it back before the pan-clamp kicks in.
|
|
529
|
+
private final class OverlayHostViewController: UIViewController {
|
|
530
|
+
var onTransitionComplete: (() -> Void)?
|
|
531
|
+
|
|
532
|
+
override func viewWillTransition(
|
|
533
|
+
to size: CGSize,
|
|
534
|
+
with coordinator: UIViewControllerTransitionCoordinator
|
|
535
|
+
) {
|
|
536
|
+
super.viewWillTransition(to: size, with: coordinator)
|
|
537
|
+
coordinator.animate(alongsideTransition: nil) { [weak self] _ in
|
|
538
|
+
self?.onTransitionComplete?()
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
445
543
|
private final class Overlay: NSObject {
|
|
446
544
|
static let shared = Overlay()
|
|
447
545
|
|
|
448
|
-
private override init() {
|
|
546
|
+
private override init() {
|
|
547
|
+
super.init()
|
|
548
|
+
// Process-wide observer for scene teardown (iPadOS / Vision Pro
|
|
549
|
+
// multi-window, Mac Catalyst). When the user closes the scene
|
|
550
|
+
// hosting the overlay, our UIWindow ends up referencing a
|
|
551
|
+
// disconnected UIWindowScene — subsequent isHidden / rootVC
|
|
552
|
+
// mutations are at best no-ops and at worst crash on certain
|
|
553
|
+
// iOS releases. Detach references so the next show() can attach
|
|
554
|
+
// cleanly to whichever scene is foreground.
|
|
555
|
+
NotificationCenter.default.addObserver(
|
|
556
|
+
self,
|
|
557
|
+
selector: #selector(handleSceneDisconnect(_:)),
|
|
558
|
+
name: UIScene.didDisconnectNotification,
|
|
559
|
+
object: nil
|
|
560
|
+
)
|
|
561
|
+
}
|
|
449
562
|
|
|
450
563
|
private var label: UILabel?
|
|
451
564
|
private var overlayWindow: UIWindow?
|
|
452
|
-
private var visible = false
|
|
453
565
|
|
|
454
566
|
// `_lastSample` is written by the Sampler timer thread and read by the
|
|
455
567
|
// main thread in attach() and inside the coalesced update closure.
|
|
456
568
|
// Optional<struct> is not atomic in Swift, so guard with a lock to avoid
|
|
457
|
-
// torn reads / undefined behaviour. `_updatePending`
|
|
458
|
-
//
|
|
569
|
+
// torn reads / undefined behaviour. `_updatePending` and `_visible` join
|
|
570
|
+
// the same lock: _updatePending coalesces overlay refreshes; _visible is
|
|
571
|
+
// toggled by show()/hide() on the caller's thread but read in update()'s
|
|
572
|
+
// main-thread closure, so snapshotting it under the existing lock keeps
|
|
573
|
+
// the read race-free without a second lock.
|
|
459
574
|
private let sampleLock = NSLock()
|
|
460
575
|
private var _lastSample: PerfSample?
|
|
461
576
|
private var _updatePending = false
|
|
577
|
+
private var _visible = false
|
|
462
578
|
|
|
463
579
|
private var lastSample: PerfSample? {
|
|
464
580
|
get {
|
|
@@ -471,6 +587,17 @@ private final class Overlay: NSObject {
|
|
|
471
587
|
}
|
|
472
588
|
}
|
|
473
589
|
|
|
590
|
+
private var visible: Bool {
|
|
591
|
+
get {
|
|
592
|
+
sampleLock.lock(); defer { sampleLock.unlock() }
|
|
593
|
+
return _visible
|
|
594
|
+
}
|
|
595
|
+
set {
|
|
596
|
+
sampleLock.lock(); defer { sampleLock.unlock() }
|
|
597
|
+
_visible = newValue
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
474
601
|
func show() {
|
|
475
602
|
visible = true
|
|
476
603
|
DispatchQueue.main.async { [weak self] in self?.attach() }
|
|
@@ -500,8 +627,9 @@ private final class Overlay: NSObject {
|
|
|
500
627
|
self.sampleLock.lock()
|
|
501
628
|
self._updatePending = false
|
|
502
629
|
let snapshot = self._lastSample
|
|
630
|
+
let isVisible = self._visible
|
|
503
631
|
self.sampleLock.unlock()
|
|
504
|
-
guard
|
|
632
|
+
guard isVisible, let snapshot = snapshot else { return }
|
|
505
633
|
self.label?.text = self.renderText(snapshot)
|
|
506
634
|
}
|
|
507
635
|
}
|
|
@@ -516,8 +644,13 @@ private final class Overlay: NSObject {
|
|
|
516
644
|
// Dedicated host VC keeps the window's view hierarchy minimal —
|
|
517
645
|
// the empty UIView serves only as the parent for the label and
|
|
518
646
|
// as the hitTest sentinel checked by OverlayPassthroughWindow.
|
|
519
|
-
|
|
647
|
+
// The subclass overrides viewWillTransition so the label gets
|
|
648
|
+
// re-clamped after rotation / split-screen size changes.
|
|
649
|
+
let host = OverlayHostViewController()
|
|
520
650
|
host.view.backgroundColor = .clear
|
|
651
|
+
host.onTransitionComplete = { [weak self] in
|
|
652
|
+
self?.clampLabelToBounds()
|
|
653
|
+
}
|
|
521
654
|
|
|
522
655
|
let window = OverlayPassthroughWindow(windowScene: scene)
|
|
523
656
|
window.frame = scene.coordinateSpace.bounds
|
|
@@ -580,6 +713,35 @@ private final class Overlay: NSObject {
|
|
|
580
713
|
gesture.setTranslation(.zero, in: parent)
|
|
581
714
|
}
|
|
582
715
|
|
|
716
|
+
/// Re-clamp the label to its parent.bounds. Called after rotation /
|
|
717
|
+
/// size-class change so the persisted center doesn't leave the
|
|
718
|
+
/// label half-off-screen until the next pan triggers handlePan.
|
|
719
|
+
private func clampLabelToBounds() {
|
|
720
|
+
guard let lbl = label, let parent = lbl.superview else { return }
|
|
721
|
+
let parentW = parent.bounds.size.width
|
|
722
|
+
let parentH = parent.bounds.size.height
|
|
723
|
+
if parentW <= 0 || parentH <= 0 { return }
|
|
724
|
+
let halfW = lbl.bounds.size.width / 2
|
|
725
|
+
let halfH = lbl.bounds.size.height / 2
|
|
726
|
+
lbl.center = CGPoint(
|
|
727
|
+
x: max(halfW, min(parentW - halfW, lbl.center.x)),
|
|
728
|
+
y: max(halfH, min(parentH - halfH, lbl.center.y))
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/// Fires when a UIWindowScene disconnects (multi-window close on
|
|
733
|
+
/// iPad / Vision / Mac Catalyst). If it's the scene our window is
|
|
734
|
+
/// attached to, drop our references so the next show() rebuilds
|
|
735
|
+
/// against the new foreground scene. UIScene notifications post on
|
|
736
|
+
/// the main thread, so this matches the threading invariants of
|
|
737
|
+
/// attach/detach.
|
|
738
|
+
@objc private func handleSceneDisconnect(_ note: Notification) {
|
|
739
|
+
guard let scene = note.object as? UIWindowScene else { return }
|
|
740
|
+
if overlayWindow?.windowScene === scene {
|
|
741
|
+
detach()
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
583
745
|
private func renderText(_ s: PerfSample) -> String {
|
|
584
746
|
let cpuStr = s.cpu > 0 ? String(format: "%.1f%%", s.cpu) : "--"
|
|
585
747
|
let mb = s.rss / 1024.0 / 1024.0
|
|
@@ -605,3 +767,186 @@ private final class Overlay: NSObject {
|
|
|
605
767
|
return fallback
|
|
606
768
|
}
|
|
607
769
|
}
|
|
770
|
+
|
|
771
|
+
// MARK: - MemoryWarningCenter
|
|
772
|
+
//
|
|
773
|
+
// Bridges UIKit's memory-warning notification to Nitro callbacks.
|
|
774
|
+
//
|
|
775
|
+
// iOS only emits one level — `UIApplicationDidReceiveMemoryWarningNotification`
|
|
776
|
+
// — which we map to `critical`. There is no `low` analog on iOS, by design
|
|
777
|
+
// (`MemoryWarningEvent.level` is normalised across platforms).
|
|
778
|
+
//
|
|
779
|
+
// The observer is registered lazily on the first `add(callback:)` and kept
|
|
780
|
+
// for the process lifetime. iOS guarantees a single NotificationCenter
|
|
781
|
+
// callback per notification, so cost is negligible. Callbacks fire on the
|
|
782
|
+
// main thread because that's the queue UIApplication posts on; Nitro's
|
|
783
|
+
// dispatcher will hop back to the JS thread.
|
|
784
|
+
|
|
785
|
+
/// 500ms cleanup throttle. UIApplication usually coalesces repeated
|
|
786
|
+
/// didReceiveMemoryWarning posts but doesn't guarantee it; without a
|
|
787
|
+
/// gate, a burst would re-run URLCache.removeAllCachedResponses
|
|
788
|
+
/// (no-op), WK clear (kicks off a redundant async run) and a 100-500ms
|
|
789
|
+
/// malloc zone walk back-to-back. Mirrors Android's emit() dedupe
|
|
790
|
+
/// window in MemoryWarningCenter.kt.
|
|
791
|
+
private let kCleanupDedupSec: Double = 0.5
|
|
792
|
+
|
|
793
|
+
/// `malloc_zone_pressure_relief(nil, 0)` walks every registered libmalloc
|
|
794
|
+
/// zone and can block 100-500ms on an 800MB+ heap. Hosted off-main on a
|
|
795
|
+
/// userInitiated queue so the OS memory-warning observer chain returns
|
|
796
|
+
/// promptly. File-scope private so all MemoryWarningCenter calls share
|
|
797
|
+
/// the queue.
|
|
798
|
+
private let perfStatsCleanupQueue = DispatchQueue(
|
|
799
|
+
label: "io.onekey.perfstats.cleanup",
|
|
800
|
+
qos: .userInitiated
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
private final class MemoryWarningCenter: NSObject {
|
|
804
|
+
static let shared = MemoryWarningCenter()
|
|
805
|
+
|
|
806
|
+
private override init() { super.init() }
|
|
807
|
+
|
|
808
|
+
private let lock = NSLock()
|
|
809
|
+
private var listeners: [Int: (MemoryWarningEvent) -> Void] = [:]
|
|
810
|
+
private var nextId: Int = 1
|
|
811
|
+
private var observed = false
|
|
812
|
+
/// Monotonic seconds of the last cleanup run, guarded by `lock`.
|
|
813
|
+
/// Survives the process lifetime to honour the dedup window across
|
|
814
|
+
/// rapid back-to-back memory warnings.
|
|
815
|
+
private var lastCleanupSec: Double = 0
|
|
816
|
+
|
|
817
|
+
func add(callback: @escaping (MemoryWarningEvent) -> Void) -> Int {
|
|
818
|
+
lock.lock()
|
|
819
|
+
let id = nextId
|
|
820
|
+
nextId += 1
|
|
821
|
+
listeners[id] = callback
|
|
822
|
+
if !observed {
|
|
823
|
+
// Register synchronously inside the lock so a memory warning
|
|
824
|
+
// posted between `add` returning and the observer being attached
|
|
825
|
+
// cannot slip past the caller. NotificationCenter.addObserver
|
|
826
|
+
// has no main-thread requirement; the selector is dispatched on
|
|
827
|
+
// whichever thread posts the notification (UIApplication posts
|
|
828
|
+
// on main, so handleWarning still lands there).
|
|
829
|
+
NotificationCenter.default.addObserver(
|
|
830
|
+
self,
|
|
831
|
+
selector: #selector(self.handleWarning),
|
|
832
|
+
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
833
|
+
object: nil
|
|
834
|
+
)
|
|
835
|
+
observed = true
|
|
836
|
+
}
|
|
837
|
+
lock.unlock()
|
|
838
|
+
return id
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
func remove(id: Int) {
|
|
842
|
+
lock.lock()
|
|
843
|
+
listeners.removeValue(forKey: id)
|
|
844
|
+
lock.unlock()
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
@objc private func handleWarning() {
|
|
848
|
+
let rssBefore = Sampler.shared.residentBytesPublic()
|
|
849
|
+
let event = MemoryWarningEvent(
|
|
850
|
+
level: .critical,
|
|
851
|
+
rss: Double(rssBefore),
|
|
852
|
+
timestamp: Date().timeIntervalSince1970 * 1000.0
|
|
853
|
+
)
|
|
854
|
+
OneKeyLog.warn(kTag, String(
|
|
855
|
+
format: "Memory warning received (critical), RSS=%.1f MB",
|
|
856
|
+
event.rss / 1024.0 / 1024.0
|
|
857
|
+
))
|
|
858
|
+
|
|
859
|
+
// Throttle native cleanup but never the JS notification: the OS
|
|
860
|
+
// can post a second warning within milliseconds of the first
|
|
861
|
+
// (UIApplication "usually coalesces" — not guaranteed), and the
|
|
862
|
+
// second one's URLCache + WK + malloc relief would all be
|
|
863
|
+
// redundant work. JS handlers still get every signal because
|
|
864
|
+
// they may want to know the pressure persists even after a
|
|
865
|
+
// skipped cleanup.
|
|
866
|
+
let nowSec = monotonicSec()
|
|
867
|
+
lock.lock()
|
|
868
|
+
let shouldClean = nowSec - lastCleanupSec >= kCleanupDedupSec
|
|
869
|
+
if shouldClean { lastCleanupSec = nowSec }
|
|
870
|
+
lock.unlock()
|
|
871
|
+
|
|
872
|
+
if shouldClean {
|
|
873
|
+
performNativeCleanup(rssBefore: rssBefore)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
lock.lock()
|
|
877
|
+
let snapshot = Array(listeners.values)
|
|
878
|
+
lock.unlock()
|
|
879
|
+
for cb in snapshot { cb(event) }
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/// Three reclaim paths, ordered from cheap-and-safe to system-level:
|
|
883
|
+
///
|
|
884
|
+
/// 1. `URLCache.shared.removeAllCachedResponses()` — drops the
|
|
885
|
+
/// process-wide HTTP response cache (CFNetwork). Empirically the
|
|
886
|
+
/// largest single non-WebView pool, often 50–200 MB. **Note:**
|
|
887
|
+
/// this is process-wide, so any code path relying on URLCache
|
|
888
|
+
/// for offline content (some image libraries, default
|
|
889
|
+
/// URLSessionConfiguration) loses its cached responses; the
|
|
890
|
+
/// next request goes to network. Synchronous, ~few ms.
|
|
891
|
+
/// 2. `WKWebsiteDataStore.removeData(...)` for HTTP caches and the
|
|
892
|
+
/// AppCache/ServiceWorker store. Excludes cookies / localStorage
|
|
893
|
+
/// / IndexedDB so in-WebView auth survives. **AppCache and
|
|
894
|
+
/// Service Worker registrations ARE in scope** — PWAs that rely
|
|
895
|
+
/// on a Service Worker for offline fallback will lose that until
|
|
896
|
+
/// the SW re-registers on next navigation. Asynchronous, runs on
|
|
897
|
+
/// a WebKit private queue; the reclaim is not visible in the
|
|
898
|
+
/// rssBefore/rssAfter delta logged below.
|
|
899
|
+
/// 3. `malloc_zone_pressure_relief(nil, 0)` — asks libmalloc to
|
|
900
|
+
/// walk every registered zone and return free pages to the
|
|
901
|
+
/// kernel. This is the only API that actually drops
|
|
902
|
+
/// `phys_footprint`; the previous two reduce allocator usage but
|
|
903
|
+
/// pages stay mapped until pressure relief runs. **Can block
|
|
904
|
+
/// 100-500ms on a heap of 800MB+**, so it's dispatched to a
|
|
905
|
+
/// background queue.
|
|
906
|
+
///
|
|
907
|
+
/// - parameter rssBefore: baseline for the reclaim-delta log. Pass
|
|
908
|
+
/// non-nil from the memory-warning path so the bg-dispatched
|
|
909
|
+
/// `OneKeyLog.warn` shows "before/after"; pass nil for caller-
|
|
910
|
+
/// initiated cleanups (`cleanupNativeCaches`) to skip the log.
|
|
911
|
+
func performNativeCleanup(rssBefore: UInt64? = nil) {
|
|
912
|
+
URLCache.shared.removeAllCachedResponses()
|
|
913
|
+
|
|
914
|
+
let cacheTypes: Set<String> = [
|
|
915
|
+
WKWebsiteDataTypeMemoryCache,
|
|
916
|
+
WKWebsiteDataTypeDiskCache,
|
|
917
|
+
WKWebsiteDataTypeOfflineWebApplicationCache,
|
|
918
|
+
]
|
|
919
|
+
WKWebsiteDataStore.default().removeData(
|
|
920
|
+
ofTypes: cacheTypes,
|
|
921
|
+
modifiedSince: Date(timeIntervalSince1970: 0),
|
|
922
|
+
completionHandler: {}
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
perfStatsCleanupQueue.async {
|
|
926
|
+
malloc_zone_pressure_relief(nil, 0)
|
|
927
|
+
guard let before = rssBefore else { return }
|
|
928
|
+
let rssAfter = Sampler.shared.residentBytesPublic()
|
|
929
|
+
let deltaMB = (Double(before) - Double(rssAfter)) / 1024.0 / 1024.0
|
|
930
|
+
if deltaMB > 0.5 {
|
|
931
|
+
OneKeyLog.warn(kTag, String(
|
|
932
|
+
format: "Native cleanup reclaimed %.1f MB (RSS %.1f → %.1f, " +
|
|
933
|
+
"excludes WK async reclaim which completes later)",
|
|
934
|
+
deltaMB,
|
|
935
|
+
Double(before) / 1024.0 / 1024.0,
|
|
936
|
+
Double(rssAfter) / 1024.0 / 1024.0
|
|
937
|
+
))
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/// Local monotonic helper. `Sampler.monotonicSec` is private; rather
|
|
943
|
+
/// than widening its visibility for one caller, we inline the same
|
|
944
|
+
/// `clock_gettime(CLOCK_MONOTONIC)` lookup here.
|
|
945
|
+
private func monotonicSec() -> Double {
|
|
946
|
+
var ts = timespec()
|
|
947
|
+
if clock_gettime(CLOCK_MONOTONIC, &ts) != 0 {
|
|
948
|
+
return Date().timeIntervalSince1970
|
|
949
|
+
}
|
|
950
|
+
return Double(ts.tv_sec) + Double(ts.tv_nsec) / 1_000_000_000.0
|
|
951
|
+
}
|
|
952
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -24,6 +24,11 @@ let jsFpsIntervalId = null;
|
|
|
24
24
|
let jsFpsFrameCount = 0;
|
|
25
25
|
let jsFpsCurrentInterval = null;
|
|
26
26
|
|
|
27
|
+
// Module-level latch so forceGarbageCollection's "no GC binding" branch
|
|
28
|
+
// warns exactly once per JS realm. Avoids spamming the console when a
|
|
29
|
+
// memory-warning handler calls forceGarbageCollection on every event.
|
|
30
|
+
let forceGcMissingWarned = false;
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Start the JS-side FPS ticker. Normally invoked automatically by
|
|
29
34
|
* `ReactNativePerfStats.start` and stopped by `.stop`; exported as
|
|
@@ -35,19 +40,25 @@ let jsFpsCurrentInterval = null;
|
|
|
35
40
|
* interval is a no-op.
|
|
36
41
|
*/
|
|
37
42
|
export function startJsFpsTracker(reportIntervalMs = 1000) {
|
|
38
|
-
|
|
43
|
+
// Clamp before use: `setInterval(_, 0)` fires every event-loop tick and
|
|
44
|
+
// the per-second divide below would race away to Infinity; NaN/negative
|
|
45
|
+
// values are equally meaningless here. 100 ms is below one rAF frame
|
|
46
|
+
// on a 60 Hz display, so it's already finer than the data warrants;
|
|
47
|
+
// 60 s is an arbitrary upper sanity bound.
|
|
48
|
+
const safeInterval = Number.isFinite(reportIntervalMs) ? Math.max(100, Math.min(60_000, Math.trunc(reportIntervalMs))) : 1000;
|
|
49
|
+
if (jsFpsCurrentInterval === safeInterval) return;
|
|
39
50
|
stopJsFpsTracker();
|
|
40
|
-
jsFpsCurrentInterval =
|
|
51
|
+
jsFpsCurrentInterval = safeInterval;
|
|
41
52
|
const tick = () => {
|
|
42
53
|
jsFpsFrameCount += 1;
|
|
43
54
|
jsFpsRafId = requestAnimationFrame(tick);
|
|
44
55
|
};
|
|
45
56
|
jsFpsRafId = requestAnimationFrame(tick);
|
|
46
57
|
jsFpsIntervalId = setInterval(() => {
|
|
47
|
-
const fps = jsFpsFrameCount * 1000 /
|
|
58
|
+
const fps = jsFpsFrameCount * 1000 / safeInterval;
|
|
48
59
|
jsFpsFrameCount = 0;
|
|
49
60
|
nativeImpl.setJsFpsHint(fps);
|
|
50
|
-
},
|
|
61
|
+
}, safeInterval);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
/** Stop the JS-side FPS ticker. Idempotent. */
|
|
@@ -82,6 +93,67 @@ export const ReactNativePerfStats = {
|
|
|
82
93
|
showOverlay: () => nativeImpl.showOverlay(),
|
|
83
94
|
hideOverlay: () => nativeImpl.hideOverlay(),
|
|
84
95
|
sample: () => nativeImpl.sample(),
|
|
85
|
-
setJsFpsHint: fps => nativeImpl.setJsFpsHint(fps)
|
|
96
|
+
setJsFpsHint: fps => nativeImpl.setJsFpsHint(fps),
|
|
97
|
+
// Memory pressure is independent of the sampler — these stay active
|
|
98
|
+
// even after `stop()`. iOS only emits `level: 'critical'`.
|
|
99
|
+
addMemoryWarningListener: callback => nativeImpl.addMemoryWarningListener(callback),
|
|
100
|
+
removeMemoryWarningListener: id => nativeImpl.removeMemoryWarningListener(id),
|
|
101
|
+
/**
|
|
102
|
+
* Run the same native reclaim path the OS memory-warning observer
|
|
103
|
+
* triggers, on demand. Returns immediately (heavy work runs async
|
|
104
|
+
* on iOS). See the spec doc for what gets dropped on each platform.
|
|
105
|
+
*/
|
|
106
|
+
cleanupNativeCaches: () => nativeImpl.cleanupNativeCaches(),
|
|
107
|
+
/**
|
|
108
|
+
* Best-effort hint to the JS engine that now is a good time to GC.
|
|
109
|
+
*
|
|
110
|
+
* Hermes does not expose a public `collectGarbage` binding in production
|
|
111
|
+
* builds; the only stable JS-level entry point is the (undocumented)
|
|
112
|
+
* `HermesInternal.gc` property, which is present in some builds and
|
|
113
|
+
* absent in others. We feature-detect it and fall back to a no-op so
|
|
114
|
+
* callers never have to branch.
|
|
115
|
+
*
|
|
116
|
+
* Returns `true` only if a GC binding was both found AND invoked
|
|
117
|
+
* without throwing. A `false` return therefore covers three cases —
|
|
118
|
+
* binding missing (production Hermes is the common case), binding
|
|
119
|
+
* present but threw, and any unexpected failure. The first miss is
|
|
120
|
+
* logged once via `console.warn` so the caller knows it landed in the
|
|
121
|
+
* "no binding" branch; throws are logged on every occurrence because
|
|
122
|
+
* those are real errors.
|
|
123
|
+
*
|
|
124
|
+
* Cost: when honoured, Hermes does a stop-the-world collection that
|
|
125
|
+
* can take 100–500 ms — never call this on the hot path. Memory-warning
|
|
126
|
+
* handlers are the intended use case.
|
|
127
|
+
*/
|
|
128
|
+
forceGarbageCollection() {
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
130
|
+
const g = globalThis;
|
|
131
|
+
const hi = g?.HermesInternal;
|
|
132
|
+
if (hi && typeof hi.gc === 'function') {
|
|
133
|
+
try {
|
|
134
|
+
hi.gc();
|
|
135
|
+
return true;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.warn('[PerfStats] HermesInternal.gc threw:', e);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (typeof g?.gc === 'function') {
|
|
142
|
+
// V8-style binding (only present with --expose-gc; never in
|
|
143
|
+
// production Hermes, but harmless to try).
|
|
144
|
+
try {
|
|
145
|
+
g.gc();
|
|
146
|
+
return true;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.warn('[PerfStats] globalThis.gc threw:', e);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!forceGcMissingWarned) {
|
|
153
|
+
forceGcMissingWarned = true;
|
|
154
|
+
console.warn('[PerfStats] forceGarbageCollection: no GC binding found ' + '(HermesInternal.gc / globalThis.gc absent). Production Hermes ' + 'strips these; this warning fires once per JS realm.');
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
86
158
|
};
|
|
87
159
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NitroModules","nativeImpl","createHybridObject","jsFpsRafId","jsFpsIntervalId","jsFpsFrameCount","jsFpsCurrentInterval","startJsFpsTracker","reportIntervalMs","stopJsFpsTracker","tick","requestAnimationFrame","setInterval","fps","setJsFpsHint","cancelAnimationFrame","clearInterval","ReactNativePerfStats","start","intervalMs","stop","showOverlay","hideOverlay","sample"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;
|
|
1
|
+
{"version":3,"names":["NitroModules","nativeImpl","createHybridObject","jsFpsRafId","jsFpsIntervalId","jsFpsFrameCount","jsFpsCurrentInterval","forceGcMissingWarned","startJsFpsTracker","reportIntervalMs","safeInterval","Number","isFinite","Math","max","min","trunc","stopJsFpsTracker","tick","requestAnimationFrame","setInterval","fps","setJsFpsHint","cancelAnimationFrame","clearInterval","ReactNativePerfStats","start","intervalMs","stop","showOverlay","hideOverlay","sample","addMemoryWarningListener","callback","removeMemoryWarningListener","id","cleanupNativeCaches","forceGarbageCollection","g","globalThis","hi","HermesInternal","gc","e","console","warn"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
|