@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.
- package/CHANGELOG.md +44 -0
- package/android/build.gradle +1 -0
- package/android/consumer-rules.pro +6 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformance.kt +384 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformanceModule.kt +15 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactDelegateWrapper.kt +8 -5
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfield.kt +78 -23
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeBrownfieldPackage.kt +1 -0
- package/android/src/main/java/com/callstack/reactnativebrownfield/ReactNativeFragment.kt +89 -3
- package/android/src/main/java/com/callstack/reactnativebrownfield/constants/ReactNativeFragmentArgNames.kt +2 -0
- package/android/src/test/java/com/callstack/reactnativebrownfield/PerformanceMetricsTest.kt +54 -0
- package/gradle-plugin/brownfield/gradle.properties +1 -1
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/plugin/RNSourceSets.kt +59 -15
- package/gradle-plugin/brownfield/src/main/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibrary.kt +41 -4
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/AgpCompatibilityMatrixTest.kt +6 -8
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/LegacyConsumerConfigurationTest.kt +6 -4
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/plugin/RNBundleTaskStubs.kt +27 -0
- package/gradle-plugin/brownfield/src/test/kotlin/com/callstack/react/brownfield/utils/AndroidArchiveLibraryNamespaceFallbackTest.kt +137 -0
- package/ios/BrownfieldDisplayMetrics.swift +265 -0
- package/ios/BrownfieldPerformanceBridge.h +8 -0
- package/ios/BrownfieldPerformanceBridge.mm +103 -0
- package/ios/Expo/ExpoHostRuntime.swift +60 -14
- package/ios/Expo/ExpoUpdatesStartup.swift +50 -0
- package/ios/JSBundleLoadObserver.swift +56 -19
- package/ios/JSBundleTimingObserver.h +26 -0
- package/ios/JSBundleTimingObserver.m +140 -0
- package/ios/ReactHostPreloading.swift +7 -3
- package/ios/ReactNativeBrownfield.swift +82 -14
- package/ios/ReactNativeBrownfield.xcodeproj/project.pbxproj +16 -0
- package/ios/Vanilla/ReactNativeHostRuntime.swift +1 -1
- package/ios/Views/ReactNativeView.swift +21 -3
- package/ios/Views/ReactNativeViewController.swift +44 -42
- package/lib/commonjs/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/commonjs/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/commonjs/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/commonjs/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/lib/commonjs/expo-config-plugin/template/android/consumer-rules.pro +7 -0
- package/lib/commonjs/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/lib/commonjs/index.js +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/scaffold/index.js +1 -1
- package/lib/commonjs/scaffold/index.js.map +1 -1
- package/lib/commonjs/scaffold/projectFiles.js +1 -1
- package/lib/commonjs/scaffold/projectFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/android/utils/constants.js +1 -1
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/podfileHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js +1 -1
- package/lib/module/expo-config-plugin/ios/withIosFrameworkFiles.js.map +1 -1
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js +2 -2
- package/lib/module/expo-config-plugin/ios/xcodeHelpers.js.map +1 -1
- package/lib/module/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/lib/module/expo-config-plugin/template/android/consumer-rules.pro +7 -0
- package/lib/module/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/scaffold/index.js +1 -1
- package/lib/module/scaffold/index.js.map +1 -1
- package/lib/module/scaffold/projectFiles.js +1 -1
- package/lib/module/scaffold/projectFiles.js.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +7 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/scaffold/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts +2 -2
- package/lib/typescript/commonjs/src/scaffold/projectFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/android/utils/constants.d.ts +2 -2
- package/lib/typescript/module/src/expo-config-plugin/ios/podfileHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts +5 -0
- package/lib/typescript/module/src/expo-config-plugin/ios/withIosFrameworkFiles.d.ts.map +1 -1
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts +5 -0
- package/lib/typescript/module/src/expo-config-plugin/ios/xcodeHelpers.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +7 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/scaffold/index.d.ts.map +1 -1
- package/lib/typescript/module/src/scaffold/projectFiles.d.ts +2 -2
- package/lib/typescript/module/src/scaffold/projectFiles.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/expo-config-plugin/android/utils/constants.ts +1 -1
- package/src/expo-config-plugin/ios/podfileHelpers.ts +47 -13
- package/src/expo-config-plugin/ios/withIosFrameworkFiles.ts +11 -1
- package/src/expo-config-plugin/ios/xcodeHelpers.ts +67 -9
- package/src/expo-config-plugin/template/android/ReactNativeHostManager.vanilla.kt +5 -7
- package/src/expo-config-plugin/template/android/consumer-rules.pro +7 -0
- package/src/expo-config-plugin/template/ios/ExpoModulesProviderExtension.swift +7 -0
- package/src/index.ts +13 -1
- package/src/scaffold/index.ts +60 -4
- 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
|
package/android/build.gradle
CHANGED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/callstack/reactnativebrownfield/BrownfieldPerformanceModule.kt
ADDED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
+
}
|