@onekeyfe/react-native-perf-stats 3.0.35 → 3.0.36
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
package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/PerfStatsInitProvider.kt
CHANGED
|
@@ -7,19 +7,26 @@ import android.database.Cursor
|
|
|
7
7
|
import android.net.Uri
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Auto-initialises [Overlay] before
|
|
10
|
+
* Auto-initialises [Overlay] and [MemoryWarningCenter] before
|
|
11
|
+
* Application.onCreate().
|
|
11
12
|
*
|
|
12
13
|
* ContentProvider.onCreate() runs between Application.attachBaseContext()
|
|
13
14
|
* and Application.onCreate(), so registering ActivityLifecycleCallbacks
|
|
14
|
-
* here guarantees
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* and ComponentCallbacks2 here guarantees:
|
|
16
|
+
* - we capture the launcher Activity's first onResumed event (Overlay),
|
|
17
|
+
* - we are subscribed before the system can fire its first low-memory
|
|
18
|
+
* callback (MemoryWarningCenter).
|
|
19
|
+
*
|
|
20
|
+
* Without this hook, JS code calling showOverlay() / addMemoryWarningListener
|
|
21
|
+
* after the React tree mounts would arrive after early-boot memory
|
|
22
|
+
* pressure events and would miss them.
|
|
17
23
|
*/
|
|
18
24
|
class PerfStatsInitProvider : ContentProvider() {
|
|
19
25
|
|
|
20
26
|
override fun onCreate(): Boolean {
|
|
21
27
|
val app = context?.applicationContext as? Application ?: return true
|
|
22
28
|
Overlay.bootstrap(app)
|
|
29
|
+
MemoryWarningCenter.bootstrap(app)
|
|
23
30
|
return true
|
|
24
31
|
}
|
|
25
32
|
|
package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStats.kt
CHANGED
|
@@ -3,7 +3,10 @@ package com.margelo.nitro.reactnativeperfstats
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.app.Activity
|
|
5
5
|
import android.app.Application
|
|
6
|
+
import android.content.ComponentCallbacks
|
|
7
|
+
import android.content.ComponentCallbacks2
|
|
6
8
|
import android.content.Context
|
|
9
|
+
import android.content.res.Configuration
|
|
7
10
|
import android.graphics.Color
|
|
8
11
|
import android.graphics.PixelFormat
|
|
9
12
|
import android.os.Bundle
|
|
@@ -18,6 +21,8 @@ import android.view.MotionEvent
|
|
|
18
21
|
import android.view.WindowManager
|
|
19
22
|
import android.widget.TextView
|
|
20
23
|
import java.util.concurrent.atomic.AtomicInteger
|
|
24
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
25
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
21
26
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
22
27
|
import com.margelo.nitro.NitroModules
|
|
23
28
|
import com.margelo.nitro.core.Promise
|
|
@@ -28,6 +33,10 @@ import java.util.Locale
|
|
|
28
33
|
|
|
29
34
|
private const val TAG = "PerfStats"
|
|
30
35
|
private const val MIN_INTERVAL_MS = 200L
|
|
36
|
+
// 24 h. `Double.POSITIVE_INFINITY.toLong()` saturates to Long.MAX_VALUE
|
|
37
|
+
// and `postDelayed(_, Long.MAX_VALUE)` would silently never fire again,
|
|
38
|
+
// so we cap before handing to the scheduler. Mirrors iOS kMaxIntervalMs.
|
|
39
|
+
private const val MAX_INTERVAL_MS = 86_400_000L
|
|
31
40
|
// Standard Android USER_HZ. If a device differs the absolute CPU% scales
|
|
32
41
|
// accordingly, but values stay self-consistent across samples.
|
|
33
42
|
private const val CLOCK_TICKS_PER_SECOND = 100L
|
|
@@ -56,7 +65,11 @@ private const val JS_FPS_HINT_TTL_MS = 2_000L
|
|
|
56
65
|
class ReactNativePerfStats : HybridReactNativePerfStatsSpec() {
|
|
57
66
|
|
|
58
67
|
override fun start(intervalMs: Double) {
|
|
59
|
-
|
|
68
|
+
// Drop NaN/Inf at the JS↔native boundary: `Double.NaN.toLong()`
|
|
69
|
+
// returns 0 (then clamps to MIN), but `Infinity.toLong()` saturates
|
|
70
|
+
// to Long.MAX_VALUE and would freeze the sampler indefinitely.
|
|
71
|
+
val safe = if (intervalMs.isFinite()) intervalMs.toLong() else 1000L
|
|
72
|
+
Sampler.start(safe.coerceIn(MIN_INTERVAL_MS, MAX_INTERVAL_MS))
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
override fun stop() {
|
|
@@ -73,13 +86,35 @@ class ReactNativePerfStats : HybridReactNativePerfStatsSpec() {
|
|
|
73
86
|
|
|
74
87
|
override fun sample(): Promise<PerfSample> {
|
|
75
88
|
return Promise.async {
|
|
76
|
-
|
|
89
|
+
// recordBaseline=false: one-shot reads must not write the
|
|
90
|
+
// CPU baseline used by the periodic tick. See takeSample.
|
|
91
|
+
Sampler.takeSample(recordBaseline = false)
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
override fun setJsFpsHint(fps: Double) {
|
|
96
|
+
// Drop NaN/Inf at the boundary; JsFpsHolder caches the value and
|
|
97
|
+
// the overlay would happily render "inf fps" otherwise.
|
|
98
|
+
if (!fps.isFinite()) return
|
|
81
99
|
JsFpsHolder.set(fps)
|
|
82
100
|
}
|
|
101
|
+
|
|
102
|
+
override fun addMemoryWarningListener(
|
|
103
|
+
callback: (MemoryWarningEvent) -> Unit,
|
|
104
|
+
): Double {
|
|
105
|
+
return MemoryWarningCenter.add(callback).toDouble()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override fun removeMemoryWarningListener(id: Double) {
|
|
109
|
+
MemoryWarningCenter.remove(id.toLong())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun cleanupNativeCaches() {
|
|
113
|
+
// Same hint that the LOW / CRITICAL warning path runs. ART may
|
|
114
|
+
// ignore Runtime.gc() at low pressure, but the cost of asking
|
|
115
|
+
// is negligible.
|
|
116
|
+
MemoryWarningCenter.performNativeCleanup()
|
|
117
|
+
}
|
|
83
118
|
}
|
|
84
119
|
|
|
85
120
|
// ---- Sampler ----------------------------------------------------------
|
|
@@ -130,8 +165,35 @@ private object Sampler {
|
|
|
130
165
|
|
|
131
166
|
fun start(intervalMsNew: Long) {
|
|
132
167
|
synchronized(schedulerLock) {
|
|
168
|
+
val prevInterval = intervalMs
|
|
133
169
|
intervalMs = intervalMsNew
|
|
134
|
-
if (running)
|
|
170
|
+
if (running) {
|
|
171
|
+
// Mirror iOS dispatch_source schedule() on a running timer:
|
|
172
|
+
// a fresh start() with a new interval re-paces the loop
|
|
173
|
+
// immediately rather than waiting for the old period to
|
|
174
|
+
// elapse. Bump generation so the in-flight tick (if any)
|
|
175
|
+
// self-rejects before rescheduling on the stale cadence.
|
|
176
|
+
if (prevInterval != intervalMsNew) {
|
|
177
|
+
generation++
|
|
178
|
+
handler?.removeCallbacksAndMessages(null)
|
|
179
|
+
scheduleTick(generation, handler!!)
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
// Cold-start path: reset CPU baseline now, before any tick is
|
|
184
|
+
// scheduled. Doing it here rather than in stop() is the only
|
|
185
|
+
// race-free option — stop()'s `quitSafely` doesn't synchronously
|
|
186
|
+
// drain a tick whose `active` check already passed, so a
|
|
187
|
+
// reset in stop() can be silently undone by that in-flight
|
|
188
|
+
// tick writing its captured cpuTicks back into lastCpuTicks.
|
|
189
|
+
// takeSample() called from one-shot sample() (recordBaseline
|
|
190
|
+
// = false) also wouldn't be able to clobber this, so the
|
|
191
|
+
// first periodic tick after start() always begins from a
|
|
192
|
+
// clean slate.
|
|
193
|
+
synchronized(lock) {
|
|
194
|
+
lastCpuTicks = -1L
|
|
195
|
+
lastMonoNs = -1L
|
|
196
|
+
}
|
|
135
197
|
if (handler == null) {
|
|
136
198
|
val ht = HandlerThread("PerfStatsSampler").apply { start() }
|
|
137
199
|
handlerThread = ht
|
|
@@ -155,6 +217,11 @@ private object Sampler {
|
|
|
155
217
|
handler = null
|
|
156
218
|
lastUiFps = 0.0
|
|
157
219
|
}
|
|
220
|
+
// No CPU baseline reset here on purpose: an in-flight tick that
|
|
221
|
+
// already passed the `active` check would race with us and write
|
|
222
|
+
// its captured cpuTicks back, undoing the reset. start() resets
|
|
223
|
+
// on its cold-start path instead, where no sampler thread is
|
|
224
|
+
// running yet.
|
|
158
225
|
UiFpsMonitor.stop()
|
|
159
226
|
Overlay.hide()
|
|
160
227
|
}
|
|
@@ -275,7 +342,19 @@ private object Sampler {
|
|
|
275
342
|
}
|
|
276
343
|
}
|
|
277
344
|
|
|
278
|
-
|
|
345
|
+
/** Public alias used by [MemoryWarningCenter] to attach an RSS reading
|
|
346
|
+
* to each emitted event. Safe to call from any thread. */
|
|
347
|
+
fun residentBytesPublic(): Long = readResidentBytes()
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param recordBaseline true for periodic ticks (the next sample's
|
|
351
|
+
* delta is computed against this read). false for one-shot
|
|
352
|
+
* sample() calls so they don't pollute the periodic CPU% baseline
|
|
353
|
+
* — between stop() and start(), or between two periodic ticks,
|
|
354
|
+
* a sample() would otherwise insert a new baseline that the next
|
|
355
|
+
* periodic tick reads, producing a CPU% covering the gap.
|
|
356
|
+
*/
|
|
357
|
+
fun takeSample(recordBaseline: Boolean = true): PerfSample {
|
|
279
358
|
val nowMonoNs = System.nanoTime()
|
|
280
359
|
val cpuTicks = readProcessCpuTicks()
|
|
281
360
|
val rssBytes = readResidentBytes()
|
|
@@ -294,7 +373,7 @@ private object Sampler {
|
|
|
294
373
|
cpuPct = (cpuSec / wallSec) * 100.0
|
|
295
374
|
}
|
|
296
375
|
}
|
|
297
|
-
if (cpuTicks != null) {
|
|
376
|
+
if (recordBaseline && cpuTicks != null) {
|
|
298
377
|
lastCpuTicks = cpuTicks
|
|
299
378
|
lastMonoNs = nowMonoNs
|
|
300
379
|
}
|
|
@@ -384,24 +463,29 @@ private object UiFpsMonitor {
|
|
|
384
463
|
}
|
|
385
464
|
|
|
386
465
|
fun start() {
|
|
387
|
-
|
|
388
|
-
|
|
466
|
+
// Idempotency check runs inside the main-thread lambda so two
|
|
467
|
+
// concurrent start() calls can't both pass the gate and post the
|
|
468
|
+
// callback twice — Choreographer happily enqueues the same instance
|
|
469
|
+
// multiple times, which would double (then exponentially grow) the
|
|
470
|
+
// frame count and repost cadence.
|
|
389
471
|
mainHandler.post {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (!registered) return@post
|
|
472
|
+
if (registered) return@post
|
|
473
|
+
registered = true
|
|
393
474
|
Choreographer.getInstance().postFrameCallback(callback)
|
|
394
475
|
}
|
|
395
476
|
}
|
|
396
477
|
|
|
397
478
|
fun stop() {
|
|
398
|
-
|
|
399
|
-
|
|
479
|
+
// Mirror start(): serialize the toggle on the main looper so a
|
|
480
|
+
// start/stop pair posted from a non-main thread can't interleave
|
|
481
|
+
// out of order.
|
|
400
482
|
mainHandler.post {
|
|
483
|
+
if (!registered) return@post
|
|
484
|
+
registered = false
|
|
401
485
|
Choreographer.getInstance().removeFrameCallback(callback)
|
|
486
|
+
frameCounter.set(0)
|
|
487
|
+
lastReadMonoNs = -1L
|
|
402
488
|
}
|
|
403
|
-
frameCounter.set(0)
|
|
404
|
-
lastReadMonoNs = -1L
|
|
405
489
|
}
|
|
406
490
|
|
|
407
491
|
/**
|
|
@@ -430,20 +514,21 @@ private object UiFpsMonitor {
|
|
|
430
514
|
// stopped or has not yet booted.
|
|
431
515
|
|
|
432
516
|
private object JsFpsHolder {
|
|
433
|
-
|
|
434
|
-
|
|
517
|
+
// Pair holds (fps, lastSetMonoNs). Stored together so a concurrent
|
|
518
|
+
// reader can't observe a new timestamp paired with a stale fps (or
|
|
519
|
+
// vice versa) — which two independent @Volatile fields would allow.
|
|
520
|
+
private val state = AtomicReference(Pair(0.0, -1L))
|
|
435
521
|
|
|
436
522
|
fun set(fps: Double) {
|
|
437
|
-
|
|
438
|
-
lastSetMonoNs = System.nanoTime()
|
|
523
|
+
state.set(Pair(fps, System.nanoTime()))
|
|
439
524
|
}
|
|
440
525
|
|
|
441
526
|
fun read(nowMonoNs: Long): Double {
|
|
442
|
-
val last =
|
|
527
|
+
val (fps, last) = state.get()
|
|
443
528
|
if (last < 0) return 0.0
|
|
444
529
|
val ageMs = (nowMonoNs - last) / 1_000_000L
|
|
445
530
|
if (ageMs > JS_FPS_HINT_TTL_MS) return 0.0
|
|
446
|
-
return
|
|
531
|
+
return fps
|
|
447
532
|
}
|
|
448
533
|
}
|
|
449
534
|
|
|
@@ -488,11 +573,27 @@ internal object Overlay : Application.ActivityLifecycleCallbacks {
|
|
|
488
573
|
@Volatile private var posX: Int = -1
|
|
489
574
|
@Volatile private var posY: Int = -1
|
|
490
575
|
|
|
576
|
+
// Configuration callback for rotation / split-screen / foldable
|
|
577
|
+
// size changes. Activities declaring android:configChanges (a common
|
|
578
|
+
// optimization in RN apps) don't re-create on rotation, so
|
|
579
|
+
// attachIfPossible's posted clamp never re-runs — handle it here.
|
|
580
|
+
private val configCallbacks = object : ComponentCallbacks {
|
|
581
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
582
|
+
val view = overlayView ?: return
|
|
583
|
+
mainHandler.post { clampOverlayToWindow(view) }
|
|
584
|
+
}
|
|
585
|
+
override fun onLowMemory() {
|
|
586
|
+
// No-op. Memory pressure is handled by MemoryWarningCenter
|
|
587
|
+
// via ComponentCallbacks2 on a separate registration.
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
491
591
|
/** Called from PerfStatsInitProvider at process start so we never miss
|
|
492
592
|
* the launcher Activity's onResumed event. */
|
|
493
593
|
fun bootstrap(app: Application) {
|
|
494
594
|
if (registered) return
|
|
495
595
|
app.registerActivityLifecycleCallbacks(this)
|
|
596
|
+
app.registerComponentCallbacks(configCallbacks)
|
|
496
597
|
registered = true
|
|
497
598
|
}
|
|
498
599
|
|
|
@@ -541,6 +642,7 @@ internal object Overlay : Application.ActivityLifecycleCallbacks {
|
|
|
541
642
|
return
|
|
542
643
|
}
|
|
543
644
|
app.registerActivityLifecycleCallbacks(this)
|
|
645
|
+
app.registerComponentCallbacks(configCallbacks)
|
|
544
646
|
registered = true
|
|
545
647
|
}
|
|
546
648
|
|
|
@@ -584,11 +686,48 @@ internal object Overlay : Application.ActivityLifecycleCallbacks {
|
|
|
584
686
|
overlayView = tv
|
|
585
687
|
attachDragListener(tv)
|
|
586
688
|
lastSample?.let { tv.text = renderText(it) }
|
|
689
|
+
// posX/posY are persisted from previous orientations and
|
|
690
|
+
// sessions; after rotation or a fold/unfold the saved
|
|
691
|
+
// position can land off the new window. Posted after addView
|
|
692
|
+
// so tv.width/height are measured, then re-clamped once.
|
|
693
|
+
tv.post { clampOverlayToWindow(tv) }
|
|
587
694
|
} catch (e: Exception) {
|
|
588
695
|
OneKeyLog.warn(TAG, "Failed to addView overlay: ${e.message}")
|
|
589
696
|
}
|
|
590
697
|
}
|
|
591
698
|
|
|
699
|
+
/** Preferred window size for clamping: the host Activity's decorView
|
|
700
|
+
* (correct under split-screen / foldable, where the Activity owns
|
|
701
|
+
* only part of the physical display), falling back to displayMetrics
|
|
702
|
+
* if the Activity / decorView isn't ready yet. */
|
|
703
|
+
private fun resolveWindowSize(view: TextView): Pair<Int, Int> {
|
|
704
|
+
val activity = view.context as? Activity
|
|
705
|
+
val decor = activity?.window?.decorView
|
|
706
|
+
val metrics = view.context.resources.displayMetrics
|
|
707
|
+
val w = decor?.width?.takeIf { it > 0 } ?: metrics.widthPixels
|
|
708
|
+
val h = decor?.height?.takeIf { it > 0 } ?: metrics.heightPixels
|
|
709
|
+
return w to h
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Re-clamps the overlay's current params to the host window. Called
|
|
713
|
+
* after addView (when posX/posY may have come from a different
|
|
714
|
+
* orientation) and could be reused on configuration changes. */
|
|
715
|
+
private fun clampOverlayToWindow(tv: TextView) {
|
|
716
|
+
val (winW, winH) = resolveWindowSize(tv)
|
|
717
|
+
val maxX = (winW - tv.width).coerceAtLeast(0)
|
|
718
|
+
val maxY = (winH - tv.height).coerceAtLeast(0)
|
|
719
|
+
val curParams = (tv.layoutParams as? WindowManager.LayoutParams) ?: return
|
|
720
|
+
val newX = curParams.x.coerceIn(0, maxX)
|
|
721
|
+
val newY = curParams.y.coerceIn(0, maxY)
|
|
722
|
+
if (newX == curParams.x && newY == curParams.y) return
|
|
723
|
+
curParams.x = newX
|
|
724
|
+
curParams.y = newY
|
|
725
|
+
posX = newX
|
|
726
|
+
posY = newY
|
|
727
|
+
val wm = tv.context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
|
|
728
|
+
try { wm?.updateViewLayout(tv, curParams) } catch (_: Exception) {}
|
|
729
|
+
}
|
|
730
|
+
|
|
592
731
|
private fun detach() {
|
|
593
732
|
val view = overlayView ?: return
|
|
594
733
|
if (attachedToWindowManager) {
|
|
@@ -640,17 +779,34 @@ internal object Overlay : Application.ActivityLifecycleCallbacks {
|
|
|
640
779
|
true
|
|
641
780
|
}
|
|
642
781
|
MotionEvent.ACTION_MOVE -> {
|
|
782
|
+
// Clamp to the host Activity window — without this the
|
|
783
|
+
// overlay can be dragged off-screen and stays
|
|
784
|
+
// unreachable, since hit-testing follows params.x/y.
|
|
785
|
+
// resolveWindowSize uses decorView dimensions so
|
|
786
|
+
// split-screen / foldable sessions don't let the
|
|
787
|
+
// overlay wander into another app's pane (where the
|
|
788
|
+
// overlay's window token can't receive touches).
|
|
643
789
|
val newX = (event.rawX - dX).toInt()
|
|
644
790
|
val newY = (event.rawY - dY).toInt()
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
791
|
+
val (winW, winH) = resolveWindowSize(v as TextView)
|
|
792
|
+
val maxX = (winW - v.width).coerceAtLeast(0)
|
|
793
|
+
val maxY = (winH - v.height).coerceAtLeast(0)
|
|
794
|
+
val clampedX = newX.coerceIn(0, maxX)
|
|
795
|
+
val clampedY = newY.coerceIn(0, maxY)
|
|
796
|
+
params.x = clampedX
|
|
797
|
+
params.y = clampedY
|
|
798
|
+
posX = clampedX
|
|
799
|
+
posY = clampedY
|
|
649
800
|
val wm = v.context.getSystemService(Context.WINDOW_SERVICE)
|
|
650
801
|
as? WindowManager
|
|
651
802
|
try { wm?.updateViewLayout(v, params) } catch (_: Exception) {}
|
|
652
803
|
true
|
|
653
804
|
}
|
|
805
|
+
// Terminate the gesture cleanly so the touch sequence
|
|
806
|
+
// we claimed in ACTION_DOWN doesn't leak to whatever is
|
|
807
|
+
// below the overlay window on the up-stroke.
|
|
808
|
+
MotionEvent.ACTION_UP,
|
|
809
|
+
MotionEvent.ACTION_CANCEL -> true
|
|
654
810
|
else -> false
|
|
655
811
|
}
|
|
656
812
|
}
|
|
@@ -692,3 +848,156 @@ internal object Overlay : Application.ActivityLifecycleCallbacks {
|
|
|
692
848
|
}
|
|
693
849
|
}
|
|
694
850
|
}
|
|
851
|
+
|
|
852
|
+
// ---- MemoryWarningCenter ---------------------------------------------
|
|
853
|
+
//
|
|
854
|
+
// Bridges Android's ComponentCallbacks2 callbacks to Nitro listeners.
|
|
855
|
+
//
|
|
856
|
+
// Levels are normalised to match iOS:
|
|
857
|
+
// - TRIM_MEMORY_RUNNING_MODERATE / TRIM_MEMORY_RUNNING_LOW -> "low"
|
|
858
|
+
// - TRIM_MEMORY_RUNNING_CRITICAL -> "critical"
|
|
859
|
+
// - onLowMemory() (deprecated, fires alongside CRITICAL or
|
|
860
|
+
// standalone on older devices) -> "critical"
|
|
861
|
+
// - TRIM_MEMORY_UI_HIDDEN and TRIM_MEMORY_BACKGROUND/MODERATE/COMPLETE
|
|
862
|
+
// are ignored. UI_HIDDEN is a backgrounding signal, not memory
|
|
863
|
+
// pressure; the BACKGROUND tier is too late to be useful for a
|
|
864
|
+
// foreground-pressure response (process is already targeted for
|
|
865
|
+
// LRU eviction). Subscribers who care about backgrounding can use
|
|
866
|
+
// AppState directly.
|
|
867
|
+
//
|
|
868
|
+
// Registration is process-wide via `Application.registerComponentCallbacks`,
|
|
869
|
+
// triggered lazily by [PerfStatsInitProvider] or the first `add()` call.
|
|
870
|
+
// Listeners are invoked on the main thread (the thread the callbacks
|
|
871
|
+
// fire on). Nitro's dispatcher hops back to the JS thread before the JS
|
|
872
|
+
// closure runs.
|
|
873
|
+
//
|
|
874
|
+
// We coalesce duplicate critical events that arrive within
|
|
875
|
+
// [DEDUP_WINDOW_MS]: Android often fires onLowMemory() right after
|
|
876
|
+
// TRIM_MEMORY_RUNNING_CRITICAL, and we don't want JS to receive two
|
|
877
|
+
// back-to-back cache-purge signals.
|
|
878
|
+
|
|
879
|
+
internal object MemoryWarningCenter : ComponentCallbacks2 {
|
|
880
|
+
private const val MEMORY_TAG = "PerfStats.Memory"
|
|
881
|
+
private const val DEDUP_WINDOW_MS = 500L
|
|
882
|
+
|
|
883
|
+
private val lock = Any()
|
|
884
|
+
private val listeners = LinkedHashMap<Long, (MemoryWarningEvent) -> Unit>()
|
|
885
|
+
private val nextId = AtomicLong(1)
|
|
886
|
+
@Volatile private var registered = false
|
|
887
|
+
private val lastEmitTimestamp = AtomicLong(0L)
|
|
888
|
+
|
|
889
|
+
/** Called from [PerfStatsInitProvider] at process start so we never
|
|
890
|
+
* miss the first memory-pressure event on Android 14+ where the
|
|
891
|
+
* system may fire one shortly after Application onCreate. */
|
|
892
|
+
fun bootstrap(app: Application) {
|
|
893
|
+
ensureRegistered(app)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
fun add(callback: (MemoryWarningEvent) -> Unit): Long {
|
|
897
|
+
val id = nextId.getAndIncrement()
|
|
898
|
+
synchronized(lock) { listeners[id] = callback }
|
|
899
|
+
// Late-init fallback: PerfStatsInitProvider normally registers
|
|
900
|
+
// us, but defend against test harnesses or stripped manifests.
|
|
901
|
+
ensureRegistered(null)
|
|
902
|
+
return id
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
fun remove(id: Long) {
|
|
906
|
+
synchronized(lock) { listeners.remove(id) }
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private fun ensureRegistered(appHint: Application?) {
|
|
910
|
+
if (registered) return
|
|
911
|
+
val app = appHint
|
|
912
|
+
?: (NitroModules.applicationContext as? Application)
|
|
913
|
+
?: run {
|
|
914
|
+
OneKeyLog.warn(MEMORY_TAG, "applicationContext is null; skipping registration")
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
synchronized(lock) {
|
|
918
|
+
if (registered) return
|
|
919
|
+
app.registerComponentCallbacks(this)
|
|
920
|
+
registered = true
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
override fun onTrimMemory(level: Int) {
|
|
925
|
+
val normalised = when (level) {
|
|
926
|
+
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
|
|
927
|
+
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> MemoryWarningLevel.LOW
|
|
928
|
+
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> MemoryWarningLevel.CRITICAL
|
|
929
|
+
else -> return // background / UI_HIDDEN — see header comment
|
|
930
|
+
}
|
|
931
|
+
emit(normalised, "onTrimMemory($level)")
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
override fun onLowMemory() {
|
|
935
|
+
emit(MemoryWarningLevel.CRITICAL, "onLowMemory")
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
939
|
+
// No-op. We only implement ComponentCallbacks2 for the memory
|
|
940
|
+
// callbacks above; the configuration channel is unused.
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private fun emit(level: MemoryWarningLevel, source: String) {
|
|
944
|
+
val nowMs = System.currentTimeMillis()
|
|
945
|
+
if (level == MemoryWarningLevel.CRITICAL) {
|
|
946
|
+
// Suppress: TRIM_MEMORY_RUNNING_CRITICAL + onLowMemory() often
|
|
947
|
+
// arrive within tens of ms of each other; one cache purge is
|
|
948
|
+
// enough. ComponentCallbacks2 fires on the main thread in
|
|
949
|
+
// practice, but we use CAS so the read/check/write window is
|
|
950
|
+
// closed even if a future Android version posts these from a
|
|
951
|
+
// worker thread.
|
|
952
|
+
val prev = lastEmitTimestamp.get()
|
|
953
|
+
if (nowMs - prev < DEDUP_WINDOW_MS) return
|
|
954
|
+
if (!lastEmitTimestamp.compareAndSet(prev, nowMs)) return
|
|
955
|
+
}
|
|
956
|
+
val rssBytes = Sampler.residentBytesPublic()
|
|
957
|
+
val event = MemoryWarningEvent(
|
|
958
|
+
level = level,
|
|
959
|
+
rss = rssBytes.toDouble(),
|
|
960
|
+
timestamp = nowMs.toDouble(),
|
|
961
|
+
)
|
|
962
|
+
OneKeyLog.warn(
|
|
963
|
+
MEMORY_TAG,
|
|
964
|
+
String.format(
|
|
965
|
+
Locale.US,
|
|
966
|
+
"Memory warning received (%s) from %s, RSS=%.1f MB",
|
|
967
|
+
level.name.lowercase(Locale.US), source, rssBytes / 1024.0 / 1024.0,
|
|
968
|
+
),
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
// Native cleanup before notifying JS subscribers. On Android the
|
|
972
|
+
// analogues of iOS's URLCache / malloc_pressure are far weaker:
|
|
973
|
+
// - Dalvik/ART has no equivalent of malloc_zone_pressure_relief;
|
|
974
|
+
// `System.gc()` is a hint only, the runtime decides whether to
|
|
975
|
+
// honour it. We still call it because under TRIM_MEMORY_RUNNING_*
|
|
976
|
+
// the runtime is more likely to act on the hint.
|
|
977
|
+
// - HTTP caches live inside each OkHttpClient and there's no
|
|
978
|
+
// process-wide shared instance to drop, so we leave that to JS-
|
|
979
|
+
// side subscribers that own their clients.
|
|
980
|
+
// - WebView cache clearing needs a WebView instance; defer to JS.
|
|
981
|
+
//
|
|
982
|
+
// Fires on both LOW and CRITICAL: iOS triggers cleanup on every
|
|
983
|
+
// warning (there's only one level), and JS handlers reasonably
|
|
984
|
+
// assume "native already attempted a reclaim before this event"
|
|
985
|
+
// across platforms. Runtime.gc() is a hint anyway, so even when
|
|
986
|
+
// ART ignores it on LOW the cost is negligible.
|
|
987
|
+
performNativeCleanup()
|
|
988
|
+
|
|
989
|
+
val snapshot = synchronized(lock) { listeners.values.toList() }
|
|
990
|
+
for (cb in snapshot) cb(event)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
internal fun performNativeCleanup() {
|
|
994
|
+
// Hint to ART; safe to call from main, returns quickly. ART may
|
|
995
|
+
// ignore under low pressure but under TRIM_MEMORY_RUNNING_CRITICAL
|
|
996
|
+
// it generally promotes to a concurrent collection.
|
|
997
|
+
try {
|
|
998
|
+
Runtime.getRuntime().gc()
|
|
999
|
+
} catch (e: Throwable) {
|
|
1000
|
+
OneKeyLog.warn(MEMORY_TAG, "Runtime.gc() failed: ${e.message}")
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|