@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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/ReactNativePerfStats.podspec +30 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +130 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +8 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/PerfStatsInitProvider.kt +43 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStats.kt +514 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativeperfstats/ReactNativePerfStatsPackage.kt +24 -0
  12. package/ios/ReactNativePerfStats.swift +391 -0
  13. package/lib/module/ReactNativePerfStats.nitro.js +4 -0
  14. package/lib/module/ReactNativePerfStats.nitro.js.map +1 -0
  15. package/lib/module/index.js +6 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/ReactNativePerfStats.nitro.d.ts +51 -0
  20. package/lib/typescript/src/ReactNativePerfStats.nitro.d.ts.map +1 -0
  21. package/lib/typescript/src/index.d.ts +4 -0
  22. package/lib/typescript/src/index.d.ts.map +1 -0
  23. package/nitro.json +17 -0
  24. package/nitrogen/generated/android/c++/JHybridReactNativePerfStatsSpec.cpp +83 -0
  25. package/nitrogen/generated/android/c++/JHybridReactNativePerfStatsSpec.hpp +69 -0
  26. package/nitrogen/generated/android/c++/JPerfSample.hpp +65 -0
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/HybridReactNativePerfStatsSpec.kt +74 -0
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/PerfSample.kt +44 -0
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeperfstats/reactnativeperfstatsOnLoad.kt +35 -0
  30. package/nitrogen/generated/android/reactnativeperfstats+autolinking.cmake +81 -0
  31. package/nitrogen/generated/android/reactnativeperfstats+autolinking.gradle +27 -0
  32. package/nitrogen/generated/android/reactnativeperfstatsOnLoad.cpp +44 -0
  33. package/nitrogen/generated/android/reactnativeperfstatsOnLoad.hpp +25 -0
  34. package/nitrogen/generated/ios/ReactNativePerfStats+autolinking.rb +60 -0
  35. package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Bridge.cpp +49 -0
  36. package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Bridge.hpp +122 -0
  37. package/nitrogen/generated/ios/ReactNativePerfStats-Swift-Cxx-Umbrella.hpp +47 -0
  38. package/nitrogen/generated/ios/ReactNativePerfStatsAutolinking.mm +33 -0
  39. package/nitrogen/generated/ios/ReactNativePerfStatsAutolinking.swift +25 -0
  40. package/nitrogen/generated/ios/c++/HybridReactNativePerfStatsSpecSwift.cpp +11 -0
  41. package/nitrogen/generated/ios/c++/HybridReactNativePerfStatsSpecSwift.hpp +102 -0
  42. package/nitrogen/generated/ios/swift/Func_void_PerfSample.swift +47 -0
  43. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  44. package/nitrogen/generated/ios/swift/HybridReactNativePerfStatsSpec.swift +60 -0
  45. package/nitrogen/generated/ios/swift/HybridReactNativePerfStatsSpec_cxx.swift +182 -0
  46. package/nitrogen/generated/ios/swift/PerfSample.swift +58 -0
  47. package/nitrogen/generated/shared/c++/HybridReactNativePerfStatsSpec.cpp +25 -0
  48. package/nitrogen/generated/shared/c++/HybridReactNativePerfStatsSpec.hpp +68 -0
  49. package/nitrogen/generated/shared/c++/PerfSample.hpp +83 -0
  50. package/package.json +169 -0
  51. package/src/ReactNativePerfStats.nitro.ts +54 -0
  52. 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
+ }
@@ -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
+