@callstack/react-native-brownfield 5.3.0 → 6.0.0

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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/android/build.gradle +1 -0
  3. package/android/consumer-rules.pro +6 -0
  4. package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformance.kt +384 -0
  5. package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformanceModule.kt +15 -0
  6. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt +8 -5
  7. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +78 -23
  8. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfieldPackage.kt +1 -0
  9. package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +89 -3
  10. package/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt +2 -0
  11. package/android/src/test/java/com/callstack/reactnativebrownfield/PerformanceMetricsTest.kt +54 -0
  12. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +59 -15
  13. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +6 -8
  14. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +6 -4
  15. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/RNBundleTaskStubs.kt +27 -0
  16. package/ios/BrownfieldDisplayMetrics.swift +265 -0
  17. package/ios/BrownfieldPerformanceBridge.h +8 -0
  18. package/ios/BrownfieldPerformanceBridge.mm +103 -0
  19. package/ios/Expo/ExpoHostRuntime.swift +58 -14
  20. package/ios/Expo/ExpoUpdatesStartup.swift +49 -0
  21. package/ios/JSBundleLoadObserver.swift +56 -19
  22. package/ios/JSBundleTimingObserver.h +26 -0
  23. package/ios/JSBundleTimingObserver.m +140 -0
  24. package/ios/ReactHostPreloading.swift +7 -3
  25. package/ios/ReactNativeBrownfield.swift +82 -14
  26. package/ios/ReactNativeBrownfield.xcodeproj/project.pbxproj +16 -0
  27. package/ios/Vanilla/ReactNativeHostRuntime.swift +1 -1
  28. package/ios/Views/ReactNativeView.swift +21 -3
  29. package/ios/Views/ReactNativeViewController.swift +44 -42
  30. package/lib/commonjs/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  31. package/lib/commonjs/index.js +1 -1
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/module/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  34. package/lib/module/index.js +1 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/typescript/commonjs/src/index.d.ts +7 -0
  37. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  38. package/lib/typescript/module/src/index.d.ts +7 -0
  39. package/lib/typescript/module/src/index.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  42. package/src/index.ts +13 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @callstack/react-native-brownfield
2
2
 
3
+ ## 6.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#494](https://github.com/callstack/react-native-brownfield/pull/494) [`d28810c`](https://github.com/callstack/react-native-brownfield/commit/d28810caa32f727d56915d8e8d9276a82560b231) Thanks [@hurali97](https://github.com/hurali97)! - Add Android startup, TTID/TTFD, and JS/UI responsiveness metrics. Android's
8
+ `OnJSBundleLoaded` callback now receives `JSBundleTimings` instead of a Boolean.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies []:
13
+ - @callstack/brownfield-cli@6.0.0
14
+
3
15
  ## 5.3.0
4
16
 
5
17
  ### Minor Changes
@@ -128,4 +128,5 @@ dependencies {
128
128
  //noinspection GradleDynamicVersion
129
129
  implementation "com.facebook.react:react-native:+"
130
130
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
131
+ testImplementation "junit:junit:4.13.2"
131
132
  }
@@ -1,6 +1,12 @@
1
1
  -keep class com.callstack.reactnativebrownfield.ReactNativeBrownfield { *; }
2
2
  -keep class com.callstack.reactnativebrownfield.ReactNativeBrownfield$Companion { *; }
3
3
  -keep interface com.callstack.reactnativebrownfield.OnJSBundleLoaded { *; }
4
+ -keep interface com.callstack.reactnativebrownfield.OnDisplayMetrics { *; }
5
+ -keep class com.callstack.reactnativebrownfield.JSBundleTimings { *; }
6
+ -keep class com.callstack.reactnativebrownfield.BrownfieldDisplayMetrics { *; }
7
+ -keep class com.callstack.reactnativebrownfield.BrownfieldDisplayInterval { *; }
8
+ -keep class com.callstack.reactnativebrownfield.BrownfieldThreadMetrics { *; }
9
+ -keep class com.callstack.reactnativebrownfield.BrownfieldPerformanceModule { *; }
4
10
  -keep interface com.callstack.reactnativebrownfield.OnMessageListener { *; }
5
11
  -keep class com.callstack.reactnativebrownfield.ReactNativeFragment { *; }
6
12
  -keep class com.callstack.reactnativebrownfield.ReactNativeFragment$Companion { *; }
@@ -0,0 +1,384 @@
1
+ package com.callstack.reactnativebrownfield
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.os.SystemClock
6
+ import android.view.Choreographer
7
+ import android.view.View
8
+ import com.facebook.react.ReactHost
9
+ import com.facebook.react.ReactInstanceEventListener
10
+ import com.facebook.react.bridge.ReactContext
11
+ import com.facebook.react.bridge.ReactMarker
12
+ import com.facebook.react.bridge.ReactMarkerConstants
13
+ import java.util.Collections
14
+ import java.util.UUID
15
+
16
+ class JSBundleTimings @JvmOverloads constructor(
17
+ val jsBundleLoadTime: Double?,
18
+ val jsBundleEvaluationTime: Double?,
19
+ timeline: List<Map<String, Any>> = emptyList()
20
+ ) {
21
+ val timeline: List<Map<String, Any>> = Collections.unmodifiableList(
22
+ timeline.map { Collections.unmodifiableMap(HashMap(it)) }
23
+ )
24
+ }
25
+
26
+ fun interface OnDisplayMetrics {
27
+ fun onMetrics(metrics: BrownfieldDisplayMetrics)
28
+ }
29
+
30
+ data class BrownfieldThreadMetrics(
31
+ val fps: Double?,
32
+ val busyRatio: Double?,
33
+ val sampledMs: Double,
34
+ val frameCount: Int
35
+ )
36
+
37
+ data class BrownfieldDisplayInterval(
38
+ val duration: Double,
39
+ val jsThread: BrownfieldThreadMetrics,
40
+ val uiThread: BrownfieldThreadMetrics
41
+ )
42
+
43
+ data class BrownfieldDisplayMetrics(
44
+ val presentationID: String,
45
+ val moduleName: String,
46
+ val initialDisplay: BrownfieldDisplayInterval?,
47
+ val fullDisplay: BrownfieldDisplayInterval?,
48
+ val isFinal: Boolean,
49
+ val isCancelled: Boolean = false
50
+ )
51
+
52
+ internal class BrownfieldFrameSamples {
53
+ private var startNanos: Long? = null
54
+ private var lastNanos = 0L
55
+ private var budgetNanos = 1_000_000_000L / 60
56
+ private var frames = 0
57
+ private var busyNanos = 0L
58
+
59
+ @Synchronized
60
+ fun begin(nowNanos: Long) {
61
+ if (startNanos == null) {
62
+ startNanos = nowNanos
63
+ lastNanos = nowNanos
64
+ }
65
+ }
66
+
67
+ @Synchronized
68
+ fun record(nowNanos: Long, frameIntervalNanos: Long) {
69
+ if (startNanos == null) begin(nowNanos)
70
+ budgetNanos = frameIntervalNanos.coerceAtLeast(1_000_000_000L / 240)
71
+ busyNanos += (nowNanos - lastNanos - budgetNanos).coerceAtLeast(0)
72
+ frames += 1
73
+ lastNanos = nowNanos
74
+ }
75
+
76
+ @Synchronized
77
+ fun snapshot(nowNanos: Long): BrownfieldThreadMetrics {
78
+ val start = startNanos
79
+ if (start == null || nowNanos <= start) {
80
+ return BrownfieldThreadMetrics(null, null, 0.0, 0)
81
+ }
82
+ val elapsed = nowNanos - start
83
+ val unfinishedStall = (nowNanos - lastNanos - budgetNanos).coerceAtLeast(0)
84
+ return BrownfieldThreadMetrics(
85
+ fps = frames * 1_000_000_000.0 / elapsed,
86
+ busyRatio = ((busyNanos + unfinishedStall).toDouble() / elapsed).coerceAtMost(1.0),
87
+ sampledMs = elapsed / 1_000_000.0,
88
+ frameCount = frames
89
+ )
90
+ }
91
+ }
92
+
93
+ internal class BrownfieldFrameSampler {
94
+ private val samples = BrownfieldFrameSamples()
95
+ @Volatile private var stopped = false
96
+ private var choreographer: Choreographer? = null
97
+
98
+ private val callback = object : Choreographer.FrameCallback {
99
+ override fun doFrame(frameTimeNanos: Long) {
100
+ if (stopped) return
101
+ val now = SystemClock.elapsedRealtimeNanos()
102
+ val interval = ReactNativeBrownfield.displayFrameIntervalNanos()
103
+ samples.record(now, interval)
104
+ choreographer?.postFrameCallback(this)
105
+ }
106
+ }
107
+
108
+ fun startOnCurrentLooper() {
109
+ if (stopped || choreographer != null || Looper.myLooper() == null) return
110
+ samples.begin(SystemClock.elapsedRealtimeNanos())
111
+ choreographer = Choreographer.getInstance().also { it.postFrameCallback(callback) }
112
+ }
113
+
114
+ fun snapshot(nowNanos: Long) = samples.snapshot(nowNanos)
115
+
116
+ fun stop() {
117
+ stopped = true
118
+ choreographer?.removeFrameCallback(callback)
119
+ choreographer = null
120
+ }
121
+ }
122
+
123
+ internal object BrownfieldPresentationRegistry {
124
+ private val sessions = mutableMapOf<String, BrownfieldDisplaySession>()
125
+
126
+ @Synchronized fun add(session: BrownfieldDisplaySession) { sessions[session.id] = session }
127
+ @Synchronized fun remove(id: String) { sessions.remove(id) }
128
+ @Synchronized fun mark(id: String) { sessions[id]?.markFullyDisplayed() }
129
+ @Synchronized fun cancelAll() { sessions.values.toList().forEach { it.cancel() }; sessions.clear() }
130
+ }
131
+
132
+ internal class BrownfieldDisplaySession(
133
+ val moduleName: String,
134
+ private val waitForFullDisplay: Boolean,
135
+ collectThreadMetrics: Boolean,
136
+ callback: OnDisplayMetrics?
137
+ ) {
138
+ val id: String = UUID.randomUUID().toString()
139
+ private val main = Handler(Looper.getMainLooper())
140
+ private val startNanos = SystemClock.elapsedRealtimeNanos()
141
+ private var completion = callback
142
+ private val ui = if (collectThreadMetrics && callback != null) BrownfieldFrameSampler() else null
143
+ private val js = if (collectThreadMetrics && callback != null) BrownfieldFrameSampler() else null
144
+ private var initial: BrownfieldDisplayInterval? = null
145
+ private var marked = false
146
+ private var attached = false
147
+ private var resumed = true
148
+ @Volatile private var finished = callback == null
149
+ private var boundRoot: View? = null
150
+ private var attachmentListener: View.OnAttachStateChangeListener? = null
151
+ private var contextListener: ReactInstanceEventListener? = null
152
+
153
+ init {
154
+ if (!finished) {
155
+ BrownfieldPresentationRegistry.add(this)
156
+ main.post { if (!finished) ui?.startOnCurrentLooper() }
157
+ scheduleJS()
158
+ }
159
+ }
160
+
161
+ private fun scheduleJS() {
162
+ if (js == null || finished) return
163
+ val context = ReactNativeBrownfield.currentReactContext()
164
+ if (context != null) {
165
+ context.runOnJSQueueThread { if (!finished) js.startOnCurrentLooper() }
166
+ return
167
+ }
168
+ val host = ReactNativeBrownfield.shared.reactHost
169
+ val listener = object : ReactInstanceEventListener {
170
+ override fun onReactContextInitialized(context: ReactContext) {
171
+ host.removeReactInstanceEventListener(this)
172
+ contextListener = null
173
+ context.runOnJSQueueThread { if (!finished) js.startOnCurrentLooper() }
174
+ }
175
+ }
176
+ contextListener = listener
177
+ host.addReactInstanceEventListener(listener)
178
+ }
179
+
180
+ fun bind(root: View, requiresResume: Boolean) {
181
+ resumed = !requiresResume
182
+ val listener = object : View.OnAttachStateChangeListener {
183
+ override fun onViewAttachedToWindow(view: View) {
184
+ attached = true
185
+ main.post { maybeAppear(view) }
186
+ }
187
+ override fun onViewDetachedFromWindow(view: View) {
188
+ if (attached) cancel()
189
+ view.removeOnAttachStateChangeListener(this)
190
+ }
191
+ }
192
+ boundRoot = root
193
+ attachmentListener = listener
194
+ root.addOnAttachStateChangeListener(listener)
195
+ if (root.isAttachedToWindow) {
196
+ attached = true
197
+ main.post { maybeAppear(root) }
198
+ }
199
+ }
200
+
201
+ fun setResumed(value: Boolean) {
202
+ resumed = value
203
+ if (!value && attached && initial == null) cancel() else if (value) main.post { maybeAppear(null) }
204
+ }
205
+
206
+ private fun maybeAppear(root: View?) {
207
+ if (finished || !attached || !resumed || initial != null || (root != null && !root.isAttachedToWindow)) return
208
+ initial = interval()
209
+ if (!waitForFullDisplay || marked) complete()
210
+ }
211
+
212
+ fun markFullyDisplayed() {
213
+ main.post {
214
+ if (finished || marked) return@post
215
+ marked = true
216
+ if (initial != null) complete()
217
+ }
218
+ }
219
+
220
+ private fun interval(): BrownfieldDisplayInterval {
221
+ val now = SystemClock.elapsedRealtimeNanos()
222
+ val unavailable = BrownfieldThreadMetrics(null, null, 0.0, 0)
223
+ return BrownfieldDisplayInterval(
224
+ duration = (now - startNanos) / 1_000_000.0,
225
+ jsThread = js?.snapshot(now) ?: unavailable,
226
+ uiThread = ui?.snapshot(now) ?: unavailable
227
+ )
228
+ }
229
+
230
+ private fun complete() {
231
+ val first = initial ?: return
232
+ val full = if (waitForFullDisplay) interval() else first
233
+ finished = true
234
+ val callback = completion
235
+ completion = null
236
+ stop()
237
+ main.post { callback?.onMetrics(BrownfieldDisplayMetrics(id, moduleName, first, full, true)) }
238
+ }
239
+
240
+ fun cancel() {
241
+ main.post {
242
+ if (finished) return@post
243
+ finished = true
244
+ completion = null
245
+ stop()
246
+ }
247
+ }
248
+
249
+ private fun stop() {
250
+ BrownfieldPresentationRegistry.remove(id)
251
+ attachmentListener?.let { boundRoot?.removeOnAttachStateChangeListener(it) }
252
+ attachmentListener = null
253
+ boundRoot = null
254
+ contextListener?.let {
255
+ ReactNativeBrownfield.shared.reactHost.removeReactInstanceEventListener(it)
256
+ }
257
+ contextListener = null
258
+ ui?.stop()
259
+ js?.stop()
260
+ }
261
+ }
262
+
263
+ internal object AndroidJSBundleTimingObserver {
264
+ private val main = Handler(Looper.getMainLooper())
265
+ private var generation = 0
266
+ private var accumulator = JSBundleTimingAccumulator()
267
+ private var result: JSBundleTimings? = null
268
+ private val subscribers = mutableListOf<OnJSBundleLoaded>()
269
+ private var installed = false
270
+
271
+ private val markerListener = ReactMarker.MarkerListener { name, _, instanceKey ->
272
+ val now = SystemClock.elapsedRealtime()
273
+ synchronized(this) {
274
+ when (name) {
275
+ ReactMarkerConstants.DOWNLOAD_START -> accumulator.record(BundleMarker.DOWNLOAD_START, instanceKey, now)
276
+ ReactMarkerConstants.DOWNLOAD_END -> accumulator.record(BundleMarker.DOWNLOAD_END, instanceKey, now)
277
+ ReactMarkerConstants.RUN_JS_BUNDLE_START -> accumulator.record(BundleMarker.EVALUATION_START, instanceKey, now)
278
+ ReactMarkerConstants.RUN_JS_BUNDLE_END -> accumulator.record(BundleMarker.EVALUATION_END, instanceKey, now)
279
+ else -> Unit
280
+ }
281
+ }
282
+ }
283
+
284
+ @Synchronized fun install() {
285
+ if (!installed) {
286
+ installed = true
287
+ ReactMarker.addListener(markerListener)
288
+ }
289
+ }
290
+
291
+ @Synchronized fun beginGeneration(): Int {
292
+ generation += 1
293
+ accumulator = JSBundleTimingAccumulator()
294
+ result = null
295
+ subscribers.clear()
296
+ return generation
297
+ }
298
+
299
+ @Synchronized fun subscribe(callback: OnJSBundleLoaded?) {
300
+ if (callback == null) return
301
+ val cached = result
302
+ if (cached != null) main.post { callback(cached) } else subscribers.add(callback)
303
+ }
304
+
305
+ fun contextInitialized(context: ReactContext, expectedGeneration: Int) {
306
+ context.runOnJSQueueThread { main.post { complete(expectedGeneration) } }
307
+ }
308
+
309
+ @Synchronized private fun complete(expectedGeneration: Int) {
310
+ if (expectedGeneration != generation || result != null) return
311
+ val timings = accumulator.snapshot()
312
+ result = timings
313
+ val callbacks = subscribers.toList()
314
+ subscribers.clear()
315
+ callbacks.forEach { it(timings) }
316
+ }
317
+
318
+ @Synchronized fun reset() {
319
+ generation += 1
320
+ accumulator = JSBundleTimingAccumulator()
321
+ result = null
322
+ subscribers.clear()
323
+ }
324
+ }
325
+
326
+ internal enum class BundleMarker { DOWNLOAD_START, DOWNLOAD_END, EVALUATION_START, EVALUATION_END }
327
+
328
+ internal class JSBundleTimingAccumulator {
329
+ private data class Endpoint(
330
+ var start: Long? = null,
331
+ var stop: Long? = null,
332
+ var ambiguous: Boolean = false
333
+ )
334
+
335
+ private val download = Endpoint()
336
+ private val evaluation = Endpoint()
337
+
338
+ fun record(marker: BundleMarker, instanceKey: Int, timestampMs: Long) {
339
+ val endpoint = when (marker) {
340
+ BundleMarker.DOWNLOAD_START, BundleMarker.DOWNLOAD_END -> download
341
+ BundleMarker.EVALUATION_START, BundleMarker.EVALUATION_END -> {
342
+ if (instanceKey != 1) return
343
+ evaluation
344
+ }
345
+ }
346
+ when (marker) {
347
+ BundleMarker.DOWNLOAD_START, BundleMarker.EVALUATION_START -> {
348
+ if (endpoint.start != null || endpoint.stop != null) endpoint.ambiguous = true
349
+ else endpoint.start = timestampMs
350
+ }
351
+ BundleMarker.DOWNLOAD_END, BundleMarker.EVALUATION_END -> {
352
+ if (endpoint.start == null || endpoint.stop != null) endpoint.ambiguous = true
353
+ else endpoint.stop = timestampMs
354
+ }
355
+ }
356
+ }
357
+
358
+ fun snapshot(): JSBundleTimings {
359
+ fun valid(endpoint: Endpoint): Pair<Long, Long>? {
360
+ val start = endpoint.start ?: return null
361
+ val stop = endpoint.stop ?: return null
362
+ return if (!endpoint.ambiguous && stop >= start) start to stop else null
363
+ }
364
+ val downloadPair = valid(download)
365
+ val evaluationPair = valid(evaluation)
366
+ val timeline = listOfNotNull(
367
+ downloadPair?.let { (start, stop) -> mapOf("tag" to "DOWNLOAD", "startMs" to start, "stopMs" to stop) },
368
+ evaluationPair?.let { (start, stop) -> mapOf("tag" to "RUN_JS_BUNDLE", "startMs" to start, "stopMs" to stop) }
369
+ ).sortedBy { it["startMs"] as Long }
370
+ return JSBundleTimings(
371
+ downloadPair?.let { (it.second - it.first).toDouble() },
372
+ evaluationPair?.let { (it.second - it.first).toDouble() },
373
+ timeline
374
+ )
375
+ }
376
+ }
377
+
378
+ internal fun ReactHost.installBrownfieldPerformanceStop() {
379
+ val stop = {
380
+ AndroidJSBundleTimingObserver.reset()
381
+ BrownfieldPresentationRegistry.cancelAll()
382
+ }
383
+ addBeforeDestroyListener(stop)
384
+ }
@@ -0,0 +1,15 @@
1
+ package com.callstack.reactnativebrownfield
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
5
+ import com.facebook.react.bridge.ReactMethod
6
+
7
+ class BrownfieldPerformanceModule(reactContext: ReactApplicationContext) :
8
+ ReactContextBaseJavaModule(reactContext) {
9
+ override fun getName() = "BrownfieldPerformance"
10
+
11
+ @ReactMethod
12
+ fun markFullyDisplayed(presentationID: String) {
13
+ BrownfieldPresentationRegistry.mark(presentationID)
14
+ }
15
+ }
@@ -8,14 +8,14 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
8
8
 
9
9
  class ReactDelegateWrapper(
10
10
  private val activity: ComponentActivity?,
11
- resolvedReactHost: ReactHost?,
12
- moduleName: String,
13
- launchOptions: Bundle?
11
+ private val resolvedReactHost: ReactHost?,
12
+ private val moduleName: String,
13
+ internal val brownfieldLaunchOptions: Bundle?
14
14
  ) : ReactDelegate(
15
15
  activity = activity!!,
16
16
  resolvedReactHost,
17
17
  appKey = moduleName,
18
- launchOptions = launchOptions,
18
+ launchOptions = brownfieldLaunchOptions,
19
19
  ) {
20
20
  private lateinit var hardwareBackHandler: () -> Unit
21
21
  private val backBtnHandler = DefaultHardwareBackBtnHandler {
@@ -33,4 +33,7 @@ class ReactDelegateWrapper(
33
33
  fun onReactHostResume() {
34
34
  super.reactHost?.onHostResume(activity, backBtnHandler)
35
35
  }
36
- }
36
+
37
+ internal fun withLaunchOptions(launchOptions: Bundle?): ReactDelegateWrapper =
38
+ ReactDelegateWrapper(activity, resolvedReactHost, moduleName, launchOptions)
39
+ }
@@ -2,6 +2,7 @@ package com.callstack.reactnativebrownfield
2
2
 
3
3
  import android.app.Application
4
4
  import android.os.Bundle
5
+ import android.view.WindowManager
5
6
  import android.widget.FrameLayout
6
7
  import androidx.activity.OnBackPressedCallback
7
8
  import androidx.fragment.app.FragmentActivity
@@ -20,7 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList
20
21
  import java.util.concurrent.atomic.AtomicBoolean
21
22
 
22
23
  fun interface OnJSBundleLoaded {
23
- operator fun invoke(initialized: Boolean)
24
+ operator fun invoke(timings: JSBundleTimings)
24
25
  }
25
26
 
26
27
  fun interface OnMessageListener {
@@ -34,6 +35,7 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
34
35
  private lateinit var instance: ReactNativeBrownfield
35
36
  private val initialized = AtomicBoolean()
36
37
  private val nativeLibsLoaded = AtomicBoolean()
38
+ private lateinit var applicationContext: Application
37
39
 
38
40
  @JvmStatic
39
41
  val shared: ReactNativeBrownfield get() = instance
@@ -62,7 +64,9 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
62
64
  reactHost: ReactHost,
63
65
  onJSBundleLoaded: OnJSBundleLoaded? = null
64
66
  ) {
67
+ applicationContext = application
65
68
  if (!initialized.getAndSet(true)) {
69
+ AndroidJSBundleTimingObserver.install()
66
70
  loadNativeLibs(application)
67
71
  installAndPreload(reactHost, onJSBundleLoaded)
68
72
  } else {
@@ -76,14 +80,16 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
76
80
  onJSBundleLoaded: OnJSBundleLoaded? = null,
77
81
  reactHostFactory: () -> ReactHost
78
82
  ) {
83
+ applicationContext = application
79
84
  if (!initialized.getAndSet(true)) {
85
+ AndroidJSBundleTimingObserver.install()
80
86
  loadNativeLibs(application)
81
87
  installAndPreload(reactHostFactory(), onJSBundleLoaded)
82
88
  } else {
83
89
  invokeWhenJSBundleLoaded(onJSBundleLoaded)
84
90
  }
85
91
  }
86
-
92
+
87
93
  @JvmStatic
88
94
  @JvmOverloads
89
95
  fun initialize(
@@ -91,7 +97,9 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
91
97
  options: HashMap<String, Any>,
92
98
  onJSBundleLoaded: OnJSBundleLoaded? = null
93
99
  ) {
100
+ applicationContext = application
94
101
  if (!initialized.getAndSet(true)) {
102
+ AndroidJSBundleTimingObserver.install()
95
103
  loadNativeLibs(application)
96
104
  val reactHost = getDefaultReactHost(
97
105
  context = application,
@@ -123,11 +131,11 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
123
131
  initialize(application, options, onJSBundleLoaded)
124
132
  }
125
133
 
126
- private fun preloadReactNative(callback: ((Boolean) -> Unit)) {
134
+ private fun preloadReactNative(generation: Int) {
127
135
  shared.reactHost.addReactInstanceEventListener(object :
128
136
  ReactInstanceEventListener {
129
137
  override fun onReactContextInitialized(context: ReactContext) {
130
- callback(true)
138
+ AndroidJSBundleTimingObserver.contextInitialized(context, generation)
131
139
  shared.reactHost.removeReactInstanceEventListener(this)
132
140
  }
133
141
  })
@@ -135,10 +143,12 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
135
143
  }
136
144
 
137
145
  private fun installAndPreload(reactHost: ReactHost, onJSBundleLoaded: OnJSBundleLoaded?) {
146
+ AndroidJSBundleTimingObserver.install()
147
+ val generation = AndroidJSBundleTimingObserver.beginGeneration()
138
148
  instance = ReactNativeBrownfield(reactHost)
139
- preloadReactNative {
140
- onJSBundleLoaded?.invoke(true)
141
- }
149
+ reactHost.installBrownfieldPerformanceStop()
150
+ AndroidJSBundleTimingObserver.subscribe(onJSBundleLoaded)
151
+ preloadReactNative(generation)
142
152
  }
143
153
 
144
154
  private fun invokeWhenJSBundleLoaded(onJSBundleLoaded: OnJSBundleLoaded?) {
@@ -146,18 +156,21 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
146
156
  return
147
157
  }
148
158
 
149
- val reactHost = instance.reactHost
150
- reactHost.currentReactContext?.let {
151
- onJSBundleLoaded.invoke(true)
152
- return
153
- }
159
+ AndroidJSBundleTimingObserver.subscribe(onJSBundleLoaded)
160
+ }
154
161
 
155
- reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener {
156
- override fun onReactContextInitialized(context: ReactContext) {
157
- onJSBundleLoaded.invoke(true)
158
- reactHost.removeReactInstanceEventListener(this)
159
- }
160
- })
162
+ internal fun currentReactContext(): ReactContext? =
163
+ if (::instance.isInitialized) instance.reactHost.currentReactContext else null
164
+
165
+ internal fun displayFrameIntervalNanos(): Long {
166
+ if (!::applicationContext.isInitialized) return 1_000_000_000L / 60
167
+ @Suppress("DEPRECATION")
168
+ val refreshRate = applicationContext
169
+ .getSystemService(WindowManager::class.java)
170
+ ?.defaultDisplay
171
+ ?.refreshRate
172
+ ?.takeIf { it > 0f } ?: 60f
173
+ return (1_000_000_000.0 / refreshRate).toLong()
161
174
  }
162
175
  }
163
176
 
@@ -215,10 +228,47 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
215
228
  reactDelegate: ReactDelegateWrapper?,
216
229
  launchOptions: Bundle?,
217
230
  lifecycleOwner: LifecycleOwner?,
231
+ ): FrameLayout {
232
+ return createViewInternal(
233
+ activity, moduleName, reactDelegate, launchOptions, lifecycleOwner, null, false
234
+ )
235
+ }
236
+
237
+ @JvmOverloads
238
+ fun createView(
239
+ activity: FragmentActivity?,
240
+ moduleName: String,
241
+ reactDelegate: ReactDelegateWrapper? = null,
242
+ launchOptions: Bundle? = null,
243
+ waitForFullDisplay: Boolean,
244
+ collectThreadMetrics: Boolean = false,
245
+ onMetrics: OnDisplayMetrics? = null,
246
+ ): FrameLayout {
247
+ val session = BrownfieldDisplaySession(
248
+ moduleName, waitForFullDisplay, collectThreadMetrics, onMetrics
249
+ )
250
+ val props = Bundle(launchOptions ?: Bundle()).apply {
251
+ putString("brownfieldPresentationID", session.id)
252
+ }
253
+ return createViewInternal(activity, moduleName, reactDelegate, props, activity, session, false)
254
+ }
255
+
256
+ internal fun createViewInternal(
257
+ activity: FragmentActivity?,
258
+ moduleName: String,
259
+ reactDelegate: ReactDelegateWrapper?,
260
+ launchOptions: Bundle?,
261
+ lifecycleOwner: LifecycleOwner?,
262
+ session: BrownfieldDisplaySession?,
263
+ requiresResume: Boolean,
218
264
  ): FrameLayout {
219
265
  val reactHost = shared.reactHost
220
- val resolvedDelegate =
221
- reactDelegate ?: ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions)
266
+ val resolvedDelegate = when {
267
+ reactDelegate == null -> ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions)
268
+ session != null && reactDelegate.brownfieldLaunchOptions !== launchOptions ->
269
+ reactDelegate.withLaunchOptions(launchOptions)
270
+ else -> reactDelegate
271
+ }
222
272
 
223
273
  val backPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
224
274
  override fun handleOnBackPressed() {
@@ -248,24 +298,29 @@ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
248
298
  * In such a case, we set the lifeCycle observer.
249
299
  */
250
300
  if (reactDelegate == null) {
251
- lifecycleOwner?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
301
+ lifecycleOwner?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate, session))
252
302
  }
253
303
 
254
304
  resolvedDelegate.loadApp()
255
- return resolvedDelegate.reactRootView!!
305
+ return resolvedDelegate.reactRootView!!.also { session?.bind(it, requiresResume) }
256
306
  }
257
307
 
258
- private fun getLifeCycleObserver(reactDelegate: ReactDelegateWrapper): DefaultLifecycleObserver {
308
+ private fun getLifeCycleObserver(
309
+ reactDelegate: ReactDelegateWrapper,
310
+ session: BrownfieldDisplaySession?
311
+ ): DefaultLifecycleObserver {
259
312
  return object : DefaultLifecycleObserver {
260
313
  override fun onResume(owner: LifecycleOwner) {
261
314
  reactDelegate.onReactHostResume()
262
315
  }
263
316
 
264
317
  override fun onPause(owner: LifecycleOwner) {
318
+ session?.cancel()
265
319
  reactDelegate.onHostPause()
266
320
  }
267
321
 
268
322
  override fun onDestroy(owner: LifecycleOwner) {
323
+ session?.cancel()
269
324
  reactDelegate.onHostDestroy()
270
325
  owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
271
326
  }
@@ -17,6 +17,7 @@ class ReactNativeBrownfieldPackage : ReactPackage {
17
17
  override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
18
18
  val modules = ArrayList<NativeModule>()
19
19
  modules.add(ReactNativeBrownfieldModule(reactContext))
20
+ modules.add(BrownfieldPerformanceModule(reactContext))
20
21
  return modules
21
22
  }
22
23
  }