@callstack/react-native-brownfield 5.3.0 → 6.0.1

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 (97) hide show
  1. package/CHANGELOG.md +44 -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/gradle.properties +1 -1
  13. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +59 -15
  14. package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +41 -4
  15. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +6 -8
  16. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +6 -4
  17. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/RNBundleTaskStubs.kt +27 -0
  18. package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
  19. package/ios/BrownfieldDisplayMetrics.swift +265 -0
  20. package/ios/BrownfieldPerformanceBridge.h +8 -0
  21. package/ios/BrownfieldPerformanceBridge.mm +103 -0
  22. package/ios/Expo/ExpoHostRuntime.swift +60 -14
  23. package/ios/Expo/ExpoUpdatesStartup.swift +50 -0
  24. package/ios/JSBundleLoadObserver.swift +56 -19
  25. package/ios/JSBundleTimingObserver.h +26 -0
  26. package/ios/JSBundleTimingObserver.m +140 -0
  27. package/ios/ReactHostPreloading.swift +7 -3
  28. package/ios/ReactNativeBrownfield.swift +82 -14
  29. package/ios/ReactNativeBrownfield.xcodeproj/project.pbxproj +16 -0
  30. package/ios/Vanilla/ReactNativeHostRuntime.swift +1 -1
  31. package/ios/Views/ReactNativeView.swift +21 -3
  32. package/ios/Views/ReactNativeViewController.swift +44 -42
  33. package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
  34. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +2 -2
  35. package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  36. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  37. package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  38. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  39. package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  40. package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  41. package/lib/commonjs/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  42. package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  43. package/lib/commonjs/index.js +1 -1
  44. package/lib/commonjs/index.js.map +1 -1
  45. package/lib/commonjs/scaffold/index.js +1 -1
  46. package/lib/commonjs/scaffold/index.js.map +1 -1
  47. package/lib/commonjs/scaffold/projectFiles.js +1 -1
  48. package/lib/commonjs/scaffold/projectFiles.js.map +1 -1
  49. package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
  50. package/lib/module/expo-config-plugin/ios/podfileHelpers.js +2 -2
  51. package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
  52. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
  53. package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
  54. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
  55. package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
  56. package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  57. package/lib/module/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  58. package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  59. package/lib/module/index.js +1 -1
  60. package/lib/module/index.js.map +1 -1
  61. package/lib/module/scaffold/index.js +1 -1
  62. package/lib/module/scaffold/index.js.map +1 -1
  63. package/lib/module/scaffold/projectFiles.js +1 -1
  64. package/lib/module/scaffold/projectFiles.js.map +1 -1
  65. package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  66. package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  68. package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  70. package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/src/index.d.ts +7 -0
  72. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +2 -2
  75. package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -1
  76. package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
  77. package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
  78. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
  79. package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
  80. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
  81. package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
  82. package/lib/typescript/module/src/index.d.ts +7 -0
  83. package/lib/typescript/module/src/index.d.ts.map +1 -1
  84. package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
  85. package/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
  86. package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -1
  87. package/package.json +2 -2
  88. package/src/expo-config-plugin/android/utils/constants.ts +1 -1
  89. package/src/expo-config-plugin/ios/podfileHelpers.ts +47 -13
  90. package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
  91. package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
  92. package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
  93. package/src/expo-config-plugin/template/android/consumer-rules.pro +7 -0
  94. package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
  95. package/src/index.ts +13 -1
  96. package/src/scaffold/index.ts +60 -4
  97. package/src/scaffold/projectFiles.ts +5 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @callstack/react-native-brownfield
2
2
 
3
+ ## 6.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#487](https://github.com/callstack/react-native-brownfield/pull/487) [`79817d1`](https://github.com/callstack/react-native-brownfield/commit/79817d1a21b0cb28ae63d41e935c57af8e7db39f) Thanks [@KisaneNeko](https://github.com/KisaneNeko)! - Support Expo modules in React Native CLI apps. `brownfield package:*` no longer runs `expo prebuild` on projects with committed `android/` and `ios/` directories, which used to delete the brownfield library module. Prebuild now runs only when the native directory is missing or gitignored. If you commit the generated directories of an Expo app, run `npx expo prebuild` yourself after changing `app.json` or config plugins. `create-react-native-brownfield` now detects Expo and sets up the iOS framework to use the Expo runtime. On a re-run after adding Expo, it also raises an existing framework target to iOS 16.4.
8
+
9
+ `--use-prebuilt-expo` now also turns Expo's precompiled modules on or off for `pod install`, and turns on Swift explicit modules so packaging works in projects that still carry the old `SWIFT_ENABLE_EXPLICIT_MODULES = NO` workaround. It defaults to building Expo from source when the app's React Native version doesn't match the one Expo's precompiled modules were built for.
10
+
11
+ `package:ios` now copies the expo-image dependency XCFrameworks (SDWebImage and libavif) as real files instead of symlinks into `node_modules`, so the package works when moved to another machine.
12
+
13
+ Re-running `create-react-native-brownfield` after `pod install` no longer adds a second Sources build phase to the framework target, which made the next `pod install` fail.
14
+
15
+ The `package:ios` script that `create-react-native-brownfield` adds now passes `--add-spm-package`, so the iOS app adds one local Swift package instead of every XCFramework. Existing scripts aren't changed.
16
+
17
+ - [#508](https://github.com/callstack/react-native-brownfield/pull/508) [`7e63e20`](https://github.com/callstack/react-native-brownfield/commit/7e63e204c449ab36ce5b109ff3eb5db086260089) Thanks [@malopezr7](https://github.com/malopezr7)! - Fix an iOS crash with expo-updates 57 when updates are enabled. The Expo runtime no longer asks the Updates controller for its launch asset before the controller has started.
18
+
19
+ - [#495](https://github.com/callstack/react-native-brownfield/pull/495) [`3b38aeb`](https://github.com/callstack/react-native-brownfield/commit/3b38aebb63810a8d1cc686bd70cccdeb279bf119) Thanks [@hurali97](https://github.com/hurali97)! - fix: resolve the namespace from AndroidManifest.xml `package` for libraries without a `namespace` in build.gradle
20
+
21
+ - [#500](https://github.com/callstack/react-native-brownfield/pull/500) [`0f62490`](https://github.com/callstack/react-native-brownfield/commit/0f62490e7602d1aad56ee5d9eba2fd667dc0171a) Thanks [@hurali97](https://github.com/hurali97)! - fix(android): stop the scaffolded `ReactNativeHostManager` from loading React Native twice
22
+
23
+ The `ReactNativeHostManager.kt` generated by `npm create @callstack/react-native-brownfield` for React Native CLI projects called `loadReactNative(application)` and then `ReactNativeBrownfield.initialize(...)`. Both run `DefaultNewArchitectureEntryPoint.load()`, which overrides the React Native feature flags, so the app crashed on startup with `Feature flags cannot be overridden more than once`.
24
+
25
+ The generated host manager now:
26
+ - no longer calls `loadReactNative` (`ReactNativeBrownfield.initialize` already loads the native libraries and feature flags exactly once);
27
+ - passes the `ReactHost` as a factory to the non-deprecated `initialize` overload, so it is built after `SoLoader.init` instead of crashing with `SoLoader.init() not yet called`;
28
+ - keeps the one behavior `loadReactNative` provided on the side by flagging edge-to-edge when `IS_EDGE_TO_EDGE_ENABLED` is set.
29
+
30
+ Fixes [#492](https://github.com/callstack/react-native-brownfield/issues/492).
31
+
32
+ - Updated dependencies [[`79817d1`](https://github.com/callstack/react-native-brownfield/commit/79817d1a21b0cb28ae63d41e935c57af8e7db39f), [`69fe3e3`](https://github.com/callstack/react-native-brownfield/commit/69fe3e3f05a49036cae1f3564a47ef3eed186155)]:
33
+ - @callstack/brownfield-cli@6.0.1
34
+
35
+ ## 6.0.0
36
+
37
+ ### Major Changes
38
+
39
+ - [#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
40
+ `OnJSBundleLoaded` callback now receives `JSBundleTimings` instead of a Boolean.
41
+
42
+ ### Patch Changes
43
+
44
+ - Updated dependencies []:
45
+ - @callstack/brownfield-cli@6.0.0
46
+
3
47
  ## 5.3.0
4
48
 
5
49
  ### 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
+ }