@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,514 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeperfstats
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.app.Application
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.PixelFormat
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.HandlerThread
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.os.SystemClock
|
|
14
|
+
import android.util.TypedValue
|
|
15
|
+
import android.view.Gravity
|
|
16
|
+
import android.view.MotionEvent
|
|
17
|
+
import android.view.WindowManager
|
|
18
|
+
import android.widget.TextView
|
|
19
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
20
|
+
import com.margelo.nitro.NitroModules
|
|
21
|
+
import com.margelo.nitro.core.Promise
|
|
22
|
+
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
23
|
+
import java.io.BufferedReader
|
|
24
|
+
import java.io.FileReader
|
|
25
|
+
import java.util.Locale
|
|
26
|
+
|
|
27
|
+
private const val TAG = "PerfStats"
|
|
28
|
+
private const val MIN_INTERVAL_MS = 200L
|
|
29
|
+
// Standard Android USER_HZ. If a device differs the absolute CPU% scales
|
|
30
|
+
// accordingly, but values stay self-consistent across samples.
|
|
31
|
+
private const val CLOCK_TICKS_PER_SECOND = 100L
|
|
32
|
+
|
|
33
|
+
// Anomaly logging thresholds. We only emit a warn after the metric has
|
|
34
|
+
// stayed over the threshold for SUSTAIN samples in a row, to skip
|
|
35
|
+
// transient spikes (e.g. JS startup, GC). After firing we throttle for
|
|
36
|
+
// COOLDOWN_MS to avoid flooding native-logger.
|
|
37
|
+
private const val CPU_ANOMALY_PCT = 150.0
|
|
38
|
+
// 800 MiB.
|
|
39
|
+
private const val RSS_ANOMALY_BYTES = 838_860_800.0
|
|
40
|
+
private const val ANOMALY_SUSTAIN_SAMPLES = 5
|
|
41
|
+
private const val ANOMALY_COOLDOWN_MS = 30_000L
|
|
42
|
+
|
|
43
|
+
@DoNotStrip
|
|
44
|
+
class ReactNativePerfStats : HybridReactNativePerfStatsSpec() {
|
|
45
|
+
|
|
46
|
+
override fun start(intervalMs: Double) {
|
|
47
|
+
Sampler.start(intervalMs.toLong().coerceAtLeast(MIN_INTERVAL_MS))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun stop() {
|
|
51
|
+
Sampler.stop()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun showOverlay() {
|
|
55
|
+
Overlay.show()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun hideOverlay() {
|
|
59
|
+
Overlay.hide()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun sample(): Promise<PerfSample> {
|
|
63
|
+
return Promise.async {
|
|
64
|
+
Sampler.takeSample()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- Sampler ----------------------------------------------------------
|
|
70
|
+
//
|
|
71
|
+
// Singleton state shared across all HybridObject instances. The
|
|
72
|
+
// HandlerThread runs the polling loop off the main / native-modules queue,
|
|
73
|
+
// so JS-thread freezes do not stop overlay updates.
|
|
74
|
+
|
|
75
|
+
private object Sampler {
|
|
76
|
+
// The HandlerThread is created lazily on start() and quit on stop() so
|
|
77
|
+
// an idle process doesn't keep a worker thread (~512KB stack + VM
|
|
78
|
+
// overhead) alive forever. Recreated on next start().
|
|
79
|
+
//
|
|
80
|
+
// schedulerLock guards `handlerThread`, `handler`, `running`, `generation`
|
|
81
|
+
// and `intervalMs` together — start/stop must transition all of them
|
|
82
|
+
// atomically, otherwise a start() lambda queued on the (now-quitting)
|
|
83
|
+
// looper could resurrect `running=true` after stop() and strand the
|
|
84
|
+
// sampler with no live handler.
|
|
85
|
+
private val schedulerLock = Any()
|
|
86
|
+
private var handlerThread: HandlerThread? = null
|
|
87
|
+
private var handler: Handler? = null
|
|
88
|
+
// Bumped on every stop(); any in-flight tick whose generation no longer
|
|
89
|
+
// matches drops itself instead of rescheduling on a stale handler.
|
|
90
|
+
private var generation = 0L
|
|
91
|
+
private var running = false
|
|
92
|
+
private var intervalMs: Long = 1000L
|
|
93
|
+
|
|
94
|
+
private val lock = Any()
|
|
95
|
+
@Volatile private var lastCpuTicks: Long = -1L
|
|
96
|
+
@Volatile private var lastMonoNs: Long = -1L
|
|
97
|
+
|
|
98
|
+
// Anomaly state lives on the sampler HandlerThread (single consumer),
|
|
99
|
+
// so no synchronization is needed. lastLogMs intentionally persists
|
|
100
|
+
// across stop()/start() cycles to keep the cooldown honest if the
|
|
101
|
+
// caller toggles the sampler rapidly.
|
|
102
|
+
private var cpuOverCount = 0
|
|
103
|
+
private var rssOverCount = 0
|
|
104
|
+
private var lastCpuLogMs = 0L
|
|
105
|
+
private var lastRssLogMs = 0L
|
|
106
|
+
|
|
107
|
+
fun start(intervalMsNew: Long) {
|
|
108
|
+
synchronized(schedulerLock) {
|
|
109
|
+
intervalMs = intervalMsNew
|
|
110
|
+
if (running) return
|
|
111
|
+
if (handler == null) {
|
|
112
|
+
val ht = HandlerThread("PerfStatsSampler").apply { start() }
|
|
113
|
+
handlerThread = ht
|
|
114
|
+
handler = Handler(ht.looper)
|
|
115
|
+
}
|
|
116
|
+
running = true
|
|
117
|
+
scheduleTick(generation, handler!!)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fun stop() {
|
|
122
|
+
synchronized(schedulerLock) {
|
|
123
|
+
generation++
|
|
124
|
+
running = false
|
|
125
|
+
handler?.removeCallbacksAndMessages(null)
|
|
126
|
+
// quitSafely lets in-flight tick body finish; the generation
|
|
127
|
+
// check below prevents that body from rescheduling itself.
|
|
128
|
+
handlerThread?.quitSafely()
|
|
129
|
+
handlerThread = null
|
|
130
|
+
handler = null
|
|
131
|
+
}
|
|
132
|
+
Overlay.hide()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Caller must hold schedulerLock; [h] is the handler captured at start time. */
|
|
136
|
+
private fun scheduleTick(genAtStart: Long, h: Handler) {
|
|
137
|
+
h.post(object : Runnable {
|
|
138
|
+
override fun run() {
|
|
139
|
+
val active = synchronized(schedulerLock) {
|
|
140
|
+
genAtStart == generation && running
|
|
141
|
+
}
|
|
142
|
+
if (!active) return
|
|
143
|
+
val sample = takeSample()
|
|
144
|
+
Overlay.update(sample)
|
|
145
|
+
checkAnomalyAndLog(sample)
|
|
146
|
+
synchronized(schedulerLock) {
|
|
147
|
+
if (genAtStart != generation || !running) return
|
|
148
|
+
handler?.postDelayed(this, intervalMs)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Emits a warn to native-logger when CPU or RSS has stayed over its
|
|
156
|
+
* threshold for [ANOMALY_SUSTAIN_SAMPLES] consecutive samples. Only
|
|
157
|
+
* called from the periodic sampler tick; one-off `sample()` calls do
|
|
158
|
+
* not trip this path. Each metric tracks its own counter and cooldown.
|
|
159
|
+
*/
|
|
160
|
+
private fun checkAnomalyAndLog(s: PerfSample) {
|
|
161
|
+
val nowMs = SystemClock.uptimeMillis()
|
|
162
|
+
|
|
163
|
+
if (s.cpu >= CPU_ANOMALY_PCT) {
|
|
164
|
+
cpuOverCount++
|
|
165
|
+
if (cpuOverCount >= ANOMALY_SUSTAIN_SAMPLES &&
|
|
166
|
+
nowMs - lastCpuLogMs >= ANOMALY_COOLDOWN_MS
|
|
167
|
+
) {
|
|
168
|
+
OneKeyLog.warn(
|
|
169
|
+
TAG,
|
|
170
|
+
String.format(
|
|
171
|
+
Locale.US,
|
|
172
|
+
"Sustained high CPU: %.1f%% over %d samples",
|
|
173
|
+
s.cpu, cpuOverCount,
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
lastCpuLogMs = nowMs
|
|
177
|
+
cpuOverCount = 0
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
cpuOverCount = 0
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (s.rss >= RSS_ANOMALY_BYTES) {
|
|
184
|
+
rssOverCount++
|
|
185
|
+
if (rssOverCount >= ANOMALY_SUSTAIN_SAMPLES &&
|
|
186
|
+
nowMs - lastRssLogMs >= ANOMALY_COOLDOWN_MS
|
|
187
|
+
) {
|
|
188
|
+
val mb = s.rss / 1024.0 / 1024.0
|
|
189
|
+
OneKeyLog.warn(
|
|
190
|
+
TAG,
|
|
191
|
+
String.format(
|
|
192
|
+
Locale.US,
|
|
193
|
+
"Sustained high RSS: %.1f MB over %d samples",
|
|
194
|
+
mb, rssOverCount,
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
lastRssLogMs = nowMs
|
|
198
|
+
rssOverCount = 0
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
rssOverCount = 0
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fun takeSample(): PerfSample {
|
|
206
|
+
val nowMonoNs = System.nanoTime()
|
|
207
|
+
val cpuTicks = readProcessCpuTicks()
|
|
208
|
+
val rssBytes = readResidentBytes()
|
|
209
|
+
val nowWallMs = System.currentTimeMillis().toDouble()
|
|
210
|
+
|
|
211
|
+
var cpuPct = 0.0
|
|
212
|
+
synchronized(lock) {
|
|
213
|
+
val prevCpu = lastCpuTicks
|
|
214
|
+
val prevMono = lastMonoNs
|
|
215
|
+
if (cpuTicks != null && prevCpu >= 0 && prevMono > 0) {
|
|
216
|
+
val dTicks = cpuTicks - prevCpu
|
|
217
|
+
val dWallNs = nowMonoNs - prevMono
|
|
218
|
+
if (dWallNs > 0 && dTicks >= 0) {
|
|
219
|
+
val cpuSec = dTicks.toDouble() / CLOCK_TICKS_PER_SECOND
|
|
220
|
+
val wallSec = dWallNs.toDouble() / 1_000_000_000.0
|
|
221
|
+
cpuPct = (cpuSec / wallSec) * 100.0
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (cpuTicks != null) {
|
|
225
|
+
lastCpuTicks = cpuTicks
|
|
226
|
+
lastMonoNs = nowMonoNs
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return PerfSample(
|
|
231
|
+
cpu = cpuPct,
|
|
232
|
+
rss = rssBytes.toDouble(),
|
|
233
|
+
timestamp = nowWallMs,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun readProcessCpuTicks(): Long? {
|
|
238
|
+
return try {
|
|
239
|
+
BufferedReader(FileReader("/proc/self/stat")).use { reader ->
|
|
240
|
+
val line = reader.readLine() ?: return null
|
|
241
|
+
// Field 2 (comm) is in parens and may itself contain spaces,
|
|
242
|
+
// so split everything *after* the last ')'.
|
|
243
|
+
val rparen = line.lastIndexOf(')')
|
|
244
|
+
if (rparen < 0 || rparen + 2 >= line.length) return null
|
|
245
|
+
val tail = line.substring(rparen + 2).split(" ")
|
|
246
|
+
if (tail.size < 13) return null
|
|
247
|
+
tail[11].toLong() + tail[12].toLong()
|
|
248
|
+
}
|
|
249
|
+
} catch (e: Exception) {
|
|
250
|
+
OneKeyLog.warn(TAG, "Failed to read /proc/self/stat: ${e.message}")
|
|
251
|
+
null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Cache page size; sysconf is not free, and the value never changes.
|
|
256
|
+
// Pixel 9 Pro Fold's 16K-page system image is why we don't hard-code 4096.
|
|
257
|
+
private val pageSize: Long by lazy {
|
|
258
|
+
try {
|
|
259
|
+
android.system.Os.sysconf(android.system.OsConstants._SC_PAGESIZE)
|
|
260
|
+
} catch (_: Throwable) {
|
|
261
|
+
4096L
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Reads /proc/self/statm field 2 (resident pages) and converts to bytes.
|
|
267
|
+
* One readLine, ~50 byte allocation; vs /proc/self/status which scans
|
|
268
|
+
* ~25 lines until VmRSS — at higher polling rates statm produces ~20x
|
|
269
|
+
* less GC pressure for an equivalent value.
|
|
270
|
+
*/
|
|
271
|
+
private fun readResidentBytes(): Long {
|
|
272
|
+
return try {
|
|
273
|
+
val line = BufferedReader(FileReader("/proc/self/statm")).use { it.readLine() }
|
|
274
|
+
?: return 0L
|
|
275
|
+
val parts = line.split(" ")
|
|
276
|
+
if (parts.size < 2) return 0L
|
|
277
|
+
parts[1].toLong() * pageSize
|
|
278
|
+
} catch (e: Exception) {
|
|
279
|
+
OneKeyLog.warn(TAG, "Failed to read /proc/self/statm: ${e.message}")
|
|
280
|
+
0L
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- Overlay ----------------------------------------------------------
|
|
286
|
+
//
|
|
287
|
+
// Attaches a TextView via WindowManager.addView using
|
|
288
|
+
// TYPE_APPLICATION_ABOVE_SUB_PANEL (z=1005). This sits above:
|
|
289
|
+
// - The activity's main window (TYPE_APPLICATION = 2)
|
|
290
|
+
// - PANEL / SUB_PANEL (1000 / 1002)
|
|
291
|
+
// - ATTACHED_DIALOG used by RN's Modal (1003)
|
|
292
|
+
// and below system-level windows (Toast = 2005, OVERLAY = 2038).
|
|
293
|
+
// Tied to the current Activity's window token, so it stays inside the app
|
|
294
|
+
// (no SYSTEM_ALERT_WINDOW permission required) and is removed automatically
|
|
295
|
+
// when the Activity is paused/destroyed.
|
|
296
|
+
//
|
|
297
|
+
// `bootstrap(app)` is invoked by `PerfStatsInitProvider` during process
|
|
298
|
+
// startup, which guarantees the launcher Activity's onResumed event is
|
|
299
|
+
// captured. Without this hook, JS code calling showOverlay() after the
|
|
300
|
+
// React tree mounts would arrive too late and currentActivity would stay
|
|
301
|
+
// null indefinitely.
|
|
302
|
+
|
|
303
|
+
internal object Overlay : Application.ActivityLifecycleCallbacks {
|
|
304
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
305
|
+
|
|
306
|
+
@Volatile private var registered = false
|
|
307
|
+
@Volatile private var visible = false
|
|
308
|
+
@Volatile private var currentActivity: Activity? = null
|
|
309
|
+
@Volatile private var overlayView: TextView? = null
|
|
310
|
+
@Volatile private var attachedToWindowManager = false
|
|
311
|
+
@Volatile private var lastSample: PerfSample? = null
|
|
312
|
+
|
|
313
|
+
// Persist drag position across Activity transitions
|
|
314
|
+
@Volatile private var posX: Int = -1
|
|
315
|
+
@Volatile private var posY: Int = -1
|
|
316
|
+
|
|
317
|
+
/** Called from PerfStatsInitProvider at process start so we never miss
|
|
318
|
+
* the launcher Activity's onResumed event. */
|
|
319
|
+
fun bootstrap(app: Application) {
|
|
320
|
+
if (registered) return
|
|
321
|
+
app.registerActivityLifecycleCallbacks(this)
|
|
322
|
+
registered = true
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fun show() {
|
|
326
|
+
visible = true
|
|
327
|
+
mainHandler.post {
|
|
328
|
+
ensureRegistered()
|
|
329
|
+
attachIfPossible()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fun hide() {
|
|
334
|
+
visible = false
|
|
335
|
+
mainHandler.post { detach() }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Coalesce updates: if the main thread is blocked, we don't want N
|
|
340
|
+
* setText calls piling up only to all fire when it unblocks. removing
|
|
341
|
+
* any pending instance and posting a fresh one guarantees at most one
|
|
342
|
+
* pending update at a time, and it always reads the latest sample.
|
|
343
|
+
*/
|
|
344
|
+
private val updateRunnable = Runnable {
|
|
345
|
+
val s = lastSample ?: return@Runnable
|
|
346
|
+
overlayView?.text = renderText(s)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fun update(sample: PerfSample) {
|
|
350
|
+
lastSample = sample
|
|
351
|
+
if (!visible) return
|
|
352
|
+
mainHandler.removeCallbacks(updateRunnable)
|
|
353
|
+
mainHandler.post(updateRunnable)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Late-init fallback if bootstrap() somehow didn't run. */
|
|
357
|
+
private fun ensureRegistered() {
|
|
358
|
+
if (registered) return
|
|
359
|
+
val ctx = NitroModules.applicationContext
|
|
360
|
+
if (ctx == null) {
|
|
361
|
+
OneKeyLog.warn(TAG, "applicationContext is null at showOverlay()")
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
val app = ctx.applicationContext as? Application
|
|
365
|
+
if (app == null) {
|
|
366
|
+
OneKeyLog.warn(TAG, "applicationContext is not Application")
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
app.registerActivityLifecycleCallbacks(this)
|
|
370
|
+
registered = true
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private fun attachIfPossible() {
|
|
374
|
+
val activity = currentActivity ?: return
|
|
375
|
+
if (overlayView != null) return
|
|
376
|
+
|
|
377
|
+
val wm = activity.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
|
|
378
|
+
if (wm == null) {
|
|
379
|
+
OneKeyLog.warn(TAG, "WindowManager unavailable on current Activity")
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
val token = activity.window?.decorView?.windowToken
|
|
384
|
+
if (token == null) {
|
|
385
|
+
// decorView may not have a token before the first frame draws.
|
|
386
|
+
// Defer to next onActivityResumed which will retry.
|
|
387
|
+
OneKeyLog.warn(TAG, "Activity window token is null; deferring overlay")
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
val tv = createOverlay(activity)
|
|
392
|
+
val params = WindowManager.LayoutParams(
|
|
393
|
+
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
394
|
+
WindowManager.LayoutParams.WRAP_CONTENT,
|
|
395
|
+
WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL,
|
|
396
|
+
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
397
|
+
or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
|
398
|
+
or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
|
399
|
+
PixelFormat.TRANSLUCENT,
|
|
400
|
+
).apply {
|
|
401
|
+
this.token = token
|
|
402
|
+
gravity = Gravity.TOP or Gravity.START
|
|
403
|
+
x = if (posX >= 0) posX else dp(activity, 30)
|
|
404
|
+
y = if (posY >= 0) posY else dp(activity, 100)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
wm.addView(tv, params)
|
|
409
|
+
attachedToWindowManager = true
|
|
410
|
+
overlayView = tv
|
|
411
|
+
attachDragListener(tv)
|
|
412
|
+
lastSample?.let { tv.text = renderText(it) }
|
|
413
|
+
} catch (e: Exception) {
|
|
414
|
+
OneKeyLog.warn(TAG, "Failed to addView overlay: ${e.message}")
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private fun detach() {
|
|
419
|
+
val view = overlayView ?: return
|
|
420
|
+
if (attachedToWindowManager) {
|
|
421
|
+
try {
|
|
422
|
+
val wm = currentActivity?.getSystemService(Context.WINDOW_SERVICE)
|
|
423
|
+
as? WindowManager
|
|
424
|
+
wm?.removeView(view)
|
|
425
|
+
} catch (e: Exception) {
|
|
426
|
+
OneKeyLog.warn(TAG, "Failed to removeView overlay: ${e.message}")
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
attachedToWindowManager = false
|
|
430
|
+
overlayView = null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private fun createOverlay(activity: Activity): TextView {
|
|
434
|
+
return TextView(activity).apply {
|
|
435
|
+
setTextColor(Color.WHITE)
|
|
436
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
|
|
437
|
+
setBackgroundColor(0xB0000000.toInt())
|
|
438
|
+
setPadding(
|
|
439
|
+
dp(activity, 12),
|
|
440
|
+
dp(activity, 8),
|
|
441
|
+
dp(activity, 12),
|
|
442
|
+
dp(activity, 8),
|
|
443
|
+
)
|
|
444
|
+
gravity = Gravity.START
|
|
445
|
+
// Min width prevents re-layout when digit count changes
|
|
446
|
+
minWidth = dp(activity, 110)
|
|
447
|
+
text = "CPU: --\nRAM: --"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
452
|
+
private fun attachDragListener(view: TextView) {
|
|
453
|
+
var dX = 0f
|
|
454
|
+
var dY = 0f
|
|
455
|
+
view.setOnTouchListener { v, event ->
|
|
456
|
+
val params = (v.layoutParams as? WindowManager.LayoutParams)
|
|
457
|
+
?: return@setOnTouchListener false
|
|
458
|
+
when (event.action) {
|
|
459
|
+
MotionEvent.ACTION_DOWN -> {
|
|
460
|
+
dX = event.rawX - params.x
|
|
461
|
+
dY = event.rawY - params.y
|
|
462
|
+
true
|
|
463
|
+
}
|
|
464
|
+
MotionEvent.ACTION_MOVE -> {
|
|
465
|
+
val newX = (event.rawX - dX).toInt()
|
|
466
|
+
val newY = (event.rawY - dY).toInt()
|
|
467
|
+
params.x = newX
|
|
468
|
+
params.y = newY
|
|
469
|
+
posX = newX
|
|
470
|
+
posY = newY
|
|
471
|
+
val wm = v.context.getSystemService(Context.WINDOW_SERVICE)
|
|
472
|
+
as? WindowManager
|
|
473
|
+
try { wm?.updateViewLayout(v, params) } catch (_: Exception) {}
|
|
474
|
+
true
|
|
475
|
+
}
|
|
476
|
+
else -> false
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private fun renderText(s: PerfSample): String {
|
|
482
|
+
val cpu = if (s.cpu > 0) String.format(Locale.US, "%.1f%%", s.cpu) else "--"
|
|
483
|
+
val mb = s.rss / 1024.0 / 1024.0
|
|
484
|
+
val mem = if (mb > 0) String.format(Locale.US, "%.1f MB", mb) else "--"
|
|
485
|
+
return "CPU: $cpu\nRAM: $mem"
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private fun dp(activity: Activity, v: Int): Int =
|
|
489
|
+
(v * activity.resources.displayMetrics.density).toInt()
|
|
490
|
+
|
|
491
|
+
// Application.ActivityLifecycleCallbacks ----------------------------
|
|
492
|
+
|
|
493
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
|
494
|
+
override fun onActivityStarted(activity: Activity) {}
|
|
495
|
+
override fun onActivityResumed(activity: Activity) {
|
|
496
|
+
currentActivity = activity
|
|
497
|
+
if (visible && overlayView == null) {
|
|
498
|
+
mainHandler.post { attachIfPossible() }
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
override fun onActivityPaused(activity: Activity) {
|
|
502
|
+
if (currentActivity === activity) {
|
|
503
|
+
mainHandler.post { detach() }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
override fun onActivityStopped(activity: Activity) {}
|
|
507
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
508
|
+
override fun onActivityDestroyed(activity: Activity) {
|
|
509
|
+
if (currentActivity === activity) {
|
|
510
|
+
mainHandler.post { detach() }
|
|
511
|
+
currentActivity = null
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStatsPackage.kt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativeperfstats
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class ReactNativePerfStatsPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
14
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
init {
|
|
19
|
+
System.loadLibrary("reactnativeperfstats")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|