@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
package com.callstack.react.brownfield.utils
|
|
2
|
+
|
|
3
|
+
import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
|
|
4
|
+
import org.gradle.testfixtures.ProjectBuilder
|
|
5
|
+
import org.junit.jupiter.api.Test
|
|
6
|
+
import kotlin.test.assertEquals
|
|
7
|
+
import kotlin.test.assertFailsWith
|
|
8
|
+
|
|
9
|
+
class AndroidArchiveLibraryNamespaceFallbackTest {
|
|
10
|
+
private fun artifactFor(moduleName: String) =
|
|
11
|
+
UnresolvedArtifactInfo(
|
|
12
|
+
moduleGroup = "com.example",
|
|
13
|
+
moduleName = moduleName,
|
|
14
|
+
moduleVersion = "1.0.0",
|
|
15
|
+
file = null,
|
|
16
|
+
isExpoPublishDependency = false,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `falls back to the AndroidManifest package when build-gradle declares no namespace`() {
|
|
21
|
+
val root = ProjectBuilder.builder().build()
|
|
22
|
+
val subProject =
|
|
23
|
+
ProjectBuilder.builder()
|
|
24
|
+
.withParent(root)
|
|
25
|
+
.withName("react-native-launch-arguments")
|
|
26
|
+
.build()
|
|
27
|
+
|
|
28
|
+
// No `namespace` declaration, mirroring libraries whose build.gradle predates AGP namespaces.
|
|
29
|
+
subProject.projectDir.mkdirs()
|
|
30
|
+
subProject.buildFile.writeText(
|
|
31
|
+
"""
|
|
32
|
+
apply plugin: 'com.android.library'
|
|
33
|
+
""".trimIndent(),
|
|
34
|
+
)
|
|
35
|
+
subProject.projectDir.resolve("src/main").mkdirs()
|
|
36
|
+
subProject.projectDir.resolve("src/main/AndroidManifest.xml").writeText(
|
|
37
|
+
"""
|
|
38
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
39
|
+
package="com.reactnativelauncharguments">
|
|
40
|
+
</manifest>
|
|
41
|
+
""".trimIndent(),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
val library = AndroidArchiveLibrary(root, artifactFor("react-native-launch-arguments"), "devRelease")
|
|
45
|
+
|
|
46
|
+
assertEquals("com.reactnativelauncharguments", library.getPackageName())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun `prefers the namespace declared in build-gradle over the manifest package`() {
|
|
51
|
+
val root = ProjectBuilder.builder().build()
|
|
52
|
+
val subProject =
|
|
53
|
+
ProjectBuilder.builder()
|
|
54
|
+
.withParent(root)
|
|
55
|
+
.withName("react-native-screens")
|
|
56
|
+
.build()
|
|
57
|
+
|
|
58
|
+
subProject.projectDir.mkdirs()
|
|
59
|
+
subProject.buildFile.writeText(
|
|
60
|
+
"""
|
|
61
|
+
apply plugin: 'com.android.library'
|
|
62
|
+
android {
|
|
63
|
+
namespace = "com.swmansion.rnscreens"
|
|
64
|
+
}
|
|
65
|
+
""".trimIndent(),
|
|
66
|
+
)
|
|
67
|
+
subProject.projectDir.resolve("src/main").mkdirs()
|
|
68
|
+
subProject.projectDir.resolve("src/main/AndroidManifest.xml").writeText(
|
|
69
|
+
"""
|
|
70
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
71
|
+
package="com.swmansion.rnscreens.legacy">
|
|
72
|
+
</manifest>
|
|
73
|
+
""".trimIndent(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
val library = AndroidArchiveLibrary(root, artifactFor("react-native-screens"), "devRelease")
|
|
77
|
+
|
|
78
|
+
assertEquals("com.swmansion.rnscreens", library.getPackageName())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
fun `reads the package from the manifest root element and ignores comments and child attributes`() {
|
|
83
|
+
val root = ProjectBuilder.builder().build()
|
|
84
|
+
val subProject =
|
|
85
|
+
ProjectBuilder.builder()
|
|
86
|
+
.withParent(root)
|
|
87
|
+
.withName("react-native-legacy-manifest")
|
|
88
|
+
.build()
|
|
89
|
+
|
|
90
|
+
subProject.projectDir.mkdirs()
|
|
91
|
+
subProject.buildFile.writeText(
|
|
92
|
+
"""
|
|
93
|
+
apply plugin: 'com.android.library'
|
|
94
|
+
""".trimIndent(),
|
|
95
|
+
)
|
|
96
|
+
subProject.projectDir.resolve("src/main").mkdirs()
|
|
97
|
+
subProject.projectDir.resolve("src/main/AndroidManifest.xml").writeText(
|
|
98
|
+
"""
|
|
99
|
+
<!-- legacy location: package="com.example.stale" -->
|
|
100
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
101
|
+
package="com.reactnativelegacymanifest">
|
|
102
|
+
<application>
|
|
103
|
+
<activity
|
|
104
|
+
android:name=".LegacyActivity"
|
|
105
|
+
android:exported="false"
|
|
106
|
+
android:targetPackage="com.example.target" />
|
|
107
|
+
</application>
|
|
108
|
+
</manifest>
|
|
109
|
+
""".trimIndent(),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
val library = AndroidArchiveLibrary(root, artifactFor("react-native-legacy-manifest"), "devRelease")
|
|
113
|
+
|
|
114
|
+
assertEquals("com.reactnativelegacymanifest", library.getPackageName())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
fun `errors when neither build-gradle nor the manifest declare a namespace`() {
|
|
119
|
+
val root = ProjectBuilder.builder().build()
|
|
120
|
+
val subProject =
|
|
121
|
+
ProjectBuilder.builder()
|
|
122
|
+
.withParent(root)
|
|
123
|
+
.withName("no-namespace-anywhere")
|
|
124
|
+
.build()
|
|
125
|
+
|
|
126
|
+
subProject.projectDir.mkdirs()
|
|
127
|
+
subProject.buildFile.writeText(
|
|
128
|
+
"""
|
|
129
|
+
apply plugin: 'com.android.library'
|
|
130
|
+
""".trimIndent(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
val library = AndroidArchiveLibrary(root, artifactFor("no-namespace-anywhere"), "devRelease")
|
|
134
|
+
|
|
135
|
+
assertFailsWith<IllegalStateException> { library.getPackageName() }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Frame cadence, not CPU utilization. A missing sampler produces nil FPS/busyRatio.
|
|
4
|
+
@objcMembers public final class BrownfieldThreadMetrics: NSObject {
|
|
5
|
+
public let fps: NSNumber?
|
|
6
|
+
public let busyRatio: NSNumber?
|
|
7
|
+
public let sampledMs: Double
|
|
8
|
+
public let frameCount: Int
|
|
9
|
+
|
|
10
|
+
internal init(fps: Double?, busyRatio: Double?, sampledMs: Double, frameCount: Int) {
|
|
11
|
+
self.fps = fps.map { NSNumber(value: $0) }
|
|
12
|
+
self.busyRatio = busyRatio.map { NSNumber(value: $0) }
|
|
13
|
+
self.sampledMs = sampledMs
|
|
14
|
+
self.frameCount = frameCount
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@objcMembers public final class BrownfieldDisplayInterval: NSObject {
|
|
19
|
+
public let duration: Double
|
|
20
|
+
public let jsThread: BrownfieldThreadMetrics
|
|
21
|
+
public let uiThread: BrownfieldThreadMetrics
|
|
22
|
+
|
|
23
|
+
internal init(duration: Double, jsThread: BrownfieldThreadMetrics, uiThread: BrownfieldThreadMetrics) {
|
|
24
|
+
self.duration = duration
|
|
25
|
+
self.jsThread = jsThread
|
|
26
|
+
self.uiThread = uiThread
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// An immutable completed snapshot, delivered once through onMetrics.
|
|
31
|
+
@objcMembers public final class BrownfieldDisplayMetrics: NSObject {
|
|
32
|
+
public let presentationID: String
|
|
33
|
+
public let moduleName: String
|
|
34
|
+
public let initialDisplay: BrownfieldDisplayInterval?
|
|
35
|
+
public let fullDisplay: BrownfieldDisplayInterval?
|
|
36
|
+
public let isFinal: Bool
|
|
37
|
+
public let isCancelled: Bool
|
|
38
|
+
|
|
39
|
+
internal init(presentationID: String, moduleName: String, initialDisplay: BrownfieldDisplayInterval?,
|
|
40
|
+
fullDisplay: BrownfieldDisplayInterval?, isFinal: Bool, isCancelled: Bool = false) {
|
|
41
|
+
self.presentationID = presentationID
|
|
42
|
+
self.moduleName = moduleName
|
|
43
|
+
self.initialDisplay = initialDisplay
|
|
44
|
+
self.fullDisplay = fullDisplay
|
|
45
|
+
self.isFinal = isFinal
|
|
46
|
+
self.isCancelled = isCancelled
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Pure cadence accumulator, kept separate from run-loop and locking mechanics.
|
|
51
|
+
internal struct BrownfieldFrameSamples {
|
|
52
|
+
var start: CFTimeInterval?
|
|
53
|
+
var last: CFTimeInterval = 0
|
|
54
|
+
var budget: CFTimeInterval = 1 / 60
|
|
55
|
+
var frames = 0
|
|
56
|
+
var busy: CFTimeInterval = 0
|
|
57
|
+
|
|
58
|
+
mutating func begin(at now: CFTimeInterval) {
|
|
59
|
+
start = now
|
|
60
|
+
last = now
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mutating func record(at now: CFTimeInterval, frameInterval: CFTimeInterval) {
|
|
64
|
+
budget = max(frameInterval, 1 / 240)
|
|
65
|
+
busy += max(0, now - last - budget)
|
|
66
|
+
frames += 1
|
|
67
|
+
last = now
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func snapshot(at now: CFTimeInterval) -> BrownfieldThreadMetrics {
|
|
71
|
+
guard let start, now > start else {
|
|
72
|
+
return BrownfieldThreadMetrics(fps: nil, busyRatio: nil, sampledMs: 0, frameCount: 0)
|
|
73
|
+
}
|
|
74
|
+
let elapsed = now - start
|
|
75
|
+
let blocked = busy + max(0, now - last - budget)
|
|
76
|
+
return BrownfieldThreadMetrics(fps: Double(frames) / elapsed,
|
|
77
|
+
busyRatio: min(1, blocked / elapsed), sampledMs: elapsed * 1000, frameCount: frames)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Lock protects the JS run-loop sampler from main-thread snapshots and cancellation.
|
|
82
|
+
internal final class BrownfieldFrameSampler: NSObject {
|
|
83
|
+
private let lock = NSLock()
|
|
84
|
+
private var link: CADisplayLink?
|
|
85
|
+
private var stopped = false
|
|
86
|
+
private var samples = BrownfieldFrameSamples()
|
|
87
|
+
|
|
88
|
+
func startOnCurrentRunLoop() {
|
|
89
|
+
lock.lock()
|
|
90
|
+
defer { lock.unlock() }
|
|
91
|
+
guard !stopped, link == nil else { return }
|
|
92
|
+
samples.begin(at: CACurrentMediaTime())
|
|
93
|
+
let link = CADisplayLink(target: self, selector: #selector(tick(_:)))
|
|
94
|
+
self.link = link
|
|
95
|
+
link.add(to: .current, forMode: .common)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@objc private func tick(_ link: CADisplayLink) {
|
|
99
|
+
lock.lock()
|
|
100
|
+
defer { lock.unlock() }
|
|
101
|
+
guard !stopped else { return }
|
|
102
|
+
samples.record(at: CACurrentMediaTime(), frameInterval: link.targetTimestamp - link.timestamp)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func snapshot(at now: CFTimeInterval) -> BrownfieldThreadMetrics {
|
|
106
|
+
lock.lock()
|
|
107
|
+
defer { lock.unlock() }
|
|
108
|
+
return samples.snapshot(at: now)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func stop() {
|
|
112
|
+
lock.lock()
|
|
113
|
+
stopped = true
|
|
114
|
+
let oldLink = link
|
|
115
|
+
link = nil
|
|
116
|
+
lock.unlock()
|
|
117
|
+
oldLink?.invalidate()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// All session state belongs to the main thread. The JS sampler alone crosses threads.
|
|
122
|
+
internal final class BrownfieldDisplaySession {
|
|
123
|
+
let id = UUID().uuidString
|
|
124
|
+
let moduleName: String
|
|
125
|
+
private let waitForFullDisplay: Bool
|
|
126
|
+
private var callback: ((BrownfieldDisplayMetrics) -> Void)?
|
|
127
|
+
private let start = CACurrentMediaTime()
|
|
128
|
+
private let ui: BrownfieldFrameSampler?
|
|
129
|
+
private let js: BrownfieldFrameSampler?
|
|
130
|
+
private var initial: BrownfieldDisplayInterval?
|
|
131
|
+
private var fullWasMarked = false
|
|
132
|
+
private var finished = false
|
|
133
|
+
private var jsScheduled = false
|
|
134
|
+
private var tokens: [NSObjectProtocol] = []
|
|
135
|
+
/// Set once the appearance probe is inside the React Native root view.
|
|
136
|
+
var hasRootView = false
|
|
137
|
+
|
|
138
|
+
init(moduleName: String, waitForFullDisplay: Bool, collectThreadMetrics: Bool = false, callback: ((BrownfieldDisplayMetrics) -> Void)?) {
|
|
139
|
+
precondition(Thread.isMainThread)
|
|
140
|
+
self.moduleName = moduleName
|
|
141
|
+
self.waitForFullDisplay = waitForFullDisplay
|
|
142
|
+
self.callback = callback
|
|
143
|
+
let shouldSample = collectThreadMetrics && callback != nil
|
|
144
|
+
ui = shouldSample ? BrownfieldFrameSampler() : nil
|
|
145
|
+
js = shouldSample ? BrownfieldFrameSampler() : nil
|
|
146
|
+
guard callback != nil else {
|
|
147
|
+
finished = true
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
ui?.startOnCurrentRunLoop()
|
|
151
|
+
scheduleJS()
|
|
152
|
+
if js != nil {
|
|
153
|
+
tokens.append(NotificationCenter.default.addObserver(
|
|
154
|
+
forName: Notification.Name("BrownfieldPerformanceBridgeReady"), object: nil, queue: .main
|
|
155
|
+
) { [weak self] _ in
|
|
156
|
+
guard let self else { return }
|
|
157
|
+
if self.jsScheduled { self.cancel() } else { self.scheduleJS() }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
if waitForFullDisplay {
|
|
161
|
+
tokens.append(NotificationCenter.default.addObserver(
|
|
162
|
+
forName: Notification.Name("BrownfieldMarkFullyDisplayed"), object: nil, queue: .main
|
|
163
|
+
) { [weak self] notification in
|
|
164
|
+
guard let self, notification.object as? String == self.id, !self.finished else { return }
|
|
165
|
+
self.fullWasMarked = true
|
|
166
|
+
if self.initial != nil { self.complete() }
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
// Cancellation releases samplers when the application stops drawing.
|
|
170
|
+
tokens.append(NotificationCenter.default.addObserver(
|
|
171
|
+
forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main
|
|
172
|
+
) { [weak self] _ in self?.cancel() })
|
|
173
|
+
tokens.append(NotificationCenter.default.addObserver(
|
|
174
|
+
forName: Notification.Name("RCTJavaScriptDidFailToLoadNotification"), object: nil, queue: .main
|
|
175
|
+
) { [weak self] _ in self?.cancel() })
|
|
176
|
+
tokens.append(NotificationCenter.default.addObserver(
|
|
177
|
+
forName: Notification.Name("BrownfieldPerformanceStop"), object: nil, queue: .main
|
|
178
|
+
) { [weak self] _ in self?.cancel() })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func scheduleJS() {
|
|
182
|
+
guard !finished, !jsScheduled, let sampler = js else { return }
|
|
183
|
+
jsScheduled = BrownfieldPerformanceBridge.dispatch(toJavaScript: {
|
|
184
|
+
sampler.startOnCurrentRunLoop()
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func interval() -> BrownfieldDisplayInterval {
|
|
189
|
+
let now = CACurrentMediaTime()
|
|
190
|
+
return BrownfieldDisplayInterval(duration: (now - start) * 1000,
|
|
191
|
+
jsThread: js?.snapshot(at: now) ?? BrownfieldFrameSamples().snapshot(at: now),
|
|
192
|
+
uiThread: ui?.snapshot(at: now) ?? BrownfieldFrameSamples().snapshot(at: now))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func appeared() {
|
|
196
|
+
guard !finished, initial == nil else { return }
|
|
197
|
+
initial = interval()
|
|
198
|
+
if !waitForFullDisplay || fullWasMarked {
|
|
199
|
+
complete()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func complete() {
|
|
204
|
+
guard !finished, let initial else { return }
|
|
205
|
+
let full = waitForFullDisplay ? interval() : initial
|
|
206
|
+
finished = true
|
|
207
|
+
let result = BrownfieldDisplayMetrics(presentationID: id, moduleName: moduleName,
|
|
208
|
+
initialDisplay: initial, fullDisplay: full, isFinal: true)
|
|
209
|
+
let completion = callback
|
|
210
|
+
callback = nil
|
|
211
|
+
stop()
|
|
212
|
+
completion?(result)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func cancel() {
|
|
216
|
+
guard !finished else { return }
|
|
217
|
+
finished = true
|
|
218
|
+
callback = nil
|
|
219
|
+
stop()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private func stop() {
|
|
223
|
+
ui?.stop()
|
|
224
|
+
js?.stop()
|
|
225
|
+
tokens.forEach(NotificationCenter.default.removeObserver)
|
|
226
|
+
tokens.removeAll()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
deinit { stop() }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// A child probe preserves the RN root view's type, layout, and touch handling.
|
|
233
|
+
internal final class BrownfieldAppearanceProbe: UIView {
|
|
234
|
+
let session: BrownfieldDisplaySession
|
|
235
|
+
/// Set when a view controller hosts the root. The probe then reports appearance only while
|
|
236
|
+
/// that controller is visible, for a root that arrives after `viewDidAppear`.
|
|
237
|
+
var isHostVisible: (() -> Bool)?
|
|
238
|
+
private var hasAttached = false
|
|
239
|
+
|
|
240
|
+
init(session: BrownfieldDisplaySession) {
|
|
241
|
+
self.session = session
|
|
242
|
+
super.init(frame: .zero)
|
|
243
|
+
isUserInteractionEnabled = false
|
|
244
|
+
isHidden = true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
248
|
+
|
|
249
|
+
override func didMoveToWindow() {
|
|
250
|
+
super.didMoveToWindow()
|
|
251
|
+
if window != nil {
|
|
252
|
+
hasAttached = true
|
|
253
|
+
if let isHostVisible {
|
|
254
|
+
if isHostVisible() { session.appeared() }
|
|
255
|
+
} else {
|
|
256
|
+
DispatchQueue.main.async { [weak self] in
|
|
257
|
+
guard let self, self.window != nil else { return }
|
|
258
|
+
self.session.appeared()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else if hasAttached {
|
|
262
|
+
session.cancel()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
@interface BrownfieldPerformanceBridge : NSObject
|
|
5
|
+
// Call on main. Returns NO until the JS thread or an RN bridge/proxy is available.
|
|
6
|
+
+ (BOOL)dispatchToJavaScript:(dispatch_block_t)block;
|
|
7
|
+
@end
|
|
8
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#import "BrownfieldPerformanceBridge.h"
|
|
2
|
+
#import <React/RCTBridge.h>
|
|
3
|
+
#import <React/RCTBridge+Private.h>
|
|
4
|
+
#import <React/RCTBridgeModule.h>
|
|
5
|
+
#import <React/RCTConstants.h>
|
|
6
|
+
#import <objc/runtime.h>
|
|
7
|
+
#include <atomic>
|
|
8
|
+
|
|
9
|
+
// Accessed on main, like BrownfieldDisplaySession.
|
|
10
|
+
static __weak RCTBridge *sPerformanceBridge;
|
|
11
|
+
static __weak NSThread *sJavaScriptThread;
|
|
12
|
+
static std::atomic<uint64_t> sGeneration{0};
|
|
13
|
+
|
|
14
|
+
static void BrownfieldPerformanceReset(void)
|
|
15
|
+
{
|
|
16
|
+
++sGeneration;
|
|
17
|
+
sPerformanceBridge = nil;
|
|
18
|
+
sJavaScriptThread = nil;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@implementation BrownfieldPerformanceBridge
|
|
22
|
+
+ (BOOL)dispatchToJavaScript:(dispatch_block_t)block
|
|
23
|
+
{
|
|
24
|
+
NSThread *thread = sJavaScriptThread;
|
|
25
|
+
if (thread && !thread.finished && !thread.cancelled) {
|
|
26
|
+
[self performSelector:@selector(runJavaScriptBlock:)
|
|
27
|
+
onThread:thread
|
|
28
|
+
withObject:[block copy]
|
|
29
|
+
waitUntilDone:NO
|
|
30
|
+
modes:@[NSRunLoopCommonModes]];
|
|
31
|
+
return YES;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
RCTBridge *bridge = sPerformanceBridge;
|
|
35
|
+
// NSProxy's respondsToSelector: does not reliably describe RCTBridgeProxy.
|
|
36
|
+
if (!bridge || !class_getInstanceMethod(object_getClass(bridge), @selector(dispatchBlock:queue:))) {
|
|
37
|
+
return NO;
|
|
38
|
+
}
|
|
39
|
+
[bridge dispatchBlock:block queue:RCTJSThread];
|
|
40
|
+
return YES;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
+ (void)runJavaScriptBlock:(dispatch_block_t)block
|
|
44
|
+
{
|
|
45
|
+
block();
|
|
46
|
+
}
|
|
47
|
+
@end
|
|
48
|
+
|
|
49
|
+
// A separate iOS-only module leaves Android's generated module contract unchanged.
|
|
50
|
+
@interface BrownfieldPerformance : NSObject <RCTBridgeModule>
|
|
51
|
+
@end
|
|
52
|
+
@implementation BrownfieldPerformance
|
|
53
|
+
RCT_EXPORT_MODULE();
|
|
54
|
+
+ (BOOL)requiresMainQueueSetup { return NO; }
|
|
55
|
+
RCT_EXPORT_METHOD(markFullyDisplayed:(NSString *)presentationID)
|
|
56
|
+
{
|
|
57
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
58
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"BrownfieldMarkFullyDisplayed"
|
|
59
|
+
object:presentationID];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
@end
|
|
63
|
+
|
|
64
|
+
__attribute__((constructor)) static void BrownfieldPerformanceRegister(void)
|
|
65
|
+
{
|
|
66
|
+
// Bridgeless RN (including 0.86) posts synchronously from loadScript's afterLoad
|
|
67
|
+
// callback on the JS thread. Observe without a queue so we capture that thread,
|
|
68
|
+
// whose run loop is maintained by RCTJSThreadManager. currentBridge is a stub
|
|
69
|
+
// when the legacy architecture is removed, and this notification has no payload.
|
|
70
|
+
[[NSNotificationCenter defaultCenter] addObserverForName:@"RCTInstanceDidLoadBundle"
|
|
71
|
+
object:nil queue:nil usingBlock:^(__unused NSNotification *notification) {
|
|
72
|
+
const auto generation = sGeneration.load();
|
|
73
|
+
NSThread *thread = NSThread.currentThread;
|
|
74
|
+
if (thread.isMainThread) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
78
|
+
if (generation != sGeneration.load()) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
sJavaScriptThread = thread;
|
|
82
|
+
sPerformanceBridge = nil;
|
|
83
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"BrownfieldPerformanceBridgeReady" object:nil];
|
|
84
|
+
});
|
|
85
|
+
}];
|
|
86
|
+
[[NSNotificationCenter defaultCenter] addObserverForName:RCTJavaScriptDidLoadNotification
|
|
87
|
+
object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *notification) {
|
|
88
|
+
sPerformanceBridge = notification.userInfo[@"bridge"];
|
|
89
|
+
// Newer RN also sends this notification after the instance notification.
|
|
90
|
+
// Announcing readiness twice would cancel an already attached display session.
|
|
91
|
+
if (!sJavaScriptThread) {
|
|
92
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:@"BrownfieldPerformanceBridgeReady" object:nil];
|
|
93
|
+
}
|
|
94
|
+
}];
|
|
95
|
+
[[NSNotificationCenter defaultCenter] addObserverForName:@"BrownfieldPerformanceStop"
|
|
96
|
+
object:nil queue:NSOperationQueue.mainQueue usingBlock:^(__unused NSNotification *notification) {
|
|
97
|
+
BrownfieldPerformanceReset();
|
|
98
|
+
}];
|
|
99
|
+
[[NSNotificationCenter defaultCenter] addObserverForName:RCTJavaScriptDidFailToLoadNotification
|
|
100
|
+
object:nil queue:NSOperationQueue.mainQueue usingBlock:^(__unused NSNotification *notification) {
|
|
101
|
+
BrownfieldPerformanceReset();
|
|
102
|
+
}];
|
|
103
|
+
}
|
|
@@ -15,6 +15,17 @@ final class ExpoHostRuntime {
|
|
|
15
15
|
private var delegate = ExpoHostRuntimeDelegate()
|
|
16
16
|
private var reactNativeFactory: RCTReactNativeFactory?
|
|
17
17
|
private var expoDelegate: ExpoAppDelegate?
|
|
18
|
+
#if canImport(EXUpdates)
|
|
19
|
+
private let updatesStartup = ExpoUpdatesStartup()
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
func prepareReactNative(_ completion: @escaping () -> Void) {
|
|
23
|
+
#if canImport(EXUpdates)
|
|
24
|
+
updatesStartup.prepareReactNative(completion)
|
|
25
|
+
#else
|
|
26
|
+
completion()
|
|
27
|
+
#endif
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
private func configureDevLoadingView(with bundleURL: URL? = nil) {
|
|
20
31
|
#if DEBUG
|
|
@@ -37,7 +48,7 @@ final class ExpoHostRuntime {
|
|
|
37
48
|
*
|
|
38
49
|
* @param onBundleLoaded Optional callback invoked on the main thread after JS bundle is fully loaded.
|
|
39
50
|
*/
|
|
40
|
-
public func startReactNative(onBundleLoaded: (() -> Void)?) {
|
|
51
|
+
public func startReactNative(onBundleLoaded: ((JSBundleTimings) -> Void)?) {
|
|
41
52
|
// The callback registration is outside of the guard below. An earlier `startReactNative` call
|
|
42
53
|
// can already have made the factory, and the callback must still run.
|
|
43
54
|
if let onBundleLoaded {
|
|
@@ -73,6 +84,9 @@ final class ExpoHostRuntime {
|
|
|
73
84
|
reactNativeFactory = nil
|
|
74
85
|
expoDelegate = nil
|
|
75
86
|
preloadState.reset()
|
|
87
|
+
#if canImport(EXUpdates)
|
|
88
|
+
updatesStartup.cancelPendingRequests()
|
|
89
|
+
#endif
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
/**
|
|
@@ -164,11 +178,42 @@ final class ExpoHostRuntime {
|
|
|
164
178
|
return ExpoAppDelegateSubscriberManager.application(application, willFinishLaunchingWithOptions: launchOptions)
|
|
165
179
|
}
|
|
166
180
|
|
|
181
|
+
/// `onRootView` receives the React Native root view, or `nil` when React Native can't create
|
|
182
|
+
/// it. While Updates selects its launch asset, this happens after `view` returns a container.
|
|
167
183
|
func view(
|
|
184
|
+
moduleName: String,
|
|
185
|
+
initialProps: [AnyHashable: Any]?,
|
|
186
|
+
launchOptions: [AnyHashable: Any]?,
|
|
187
|
+
onRootView: ((UIView?) -> Void)? = nil
|
|
188
|
+
) -> UIView? {
|
|
189
|
+
// Return a container while Updates selects its launch asset. This also supports the
|
|
190
|
+
// public view API, which does not go through ReactNativeViewController.
|
|
191
|
+
let container = UIView()
|
|
192
|
+
var immediateView: UIView?
|
|
193
|
+
var preparing = true
|
|
194
|
+
prepareReactNative { [weak self, weak container] in
|
|
195
|
+
guard let self, preparing || container != nil else { return }
|
|
196
|
+
let root = self.createView(moduleName: moduleName, initialProps: initialProps,
|
|
197
|
+
launchOptions: launchOptions)
|
|
198
|
+
if preparing {
|
|
199
|
+
immediateView = root
|
|
200
|
+
} else if let container, let root {
|
|
201
|
+
root.frame = container.bounds
|
|
202
|
+
root.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
203
|
+
container.addSubview(root)
|
|
204
|
+
}
|
|
205
|
+
onRootView?(root)
|
|
206
|
+
}
|
|
207
|
+
preparing = false
|
|
208
|
+
return immediateView ?? container
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private func createView(
|
|
168
212
|
moduleName: String,
|
|
169
213
|
initialProps: [AnyHashable: Any]?,
|
|
170
214
|
launchOptions: [AnyHashable: Any]?
|
|
171
215
|
) -> UIView? {
|
|
216
|
+
startReactNative()
|
|
172
217
|
let bundleURL = delegate.bundleURL()
|
|
173
218
|
configureDevLoadingView(with: bundleURL)
|
|
174
219
|
|
|
@@ -201,8 +246,8 @@ extension ExpoHostRuntime: ReactHostPreloading {
|
|
|
201
246
|
|
|
202
247
|
/**
|
|
203
248
|
* expo-dev-launcher finds the Metro URL only after the user selects an app in the launcher. This
|
|
204
|
-
* is a Debug behavior.
|
|
205
|
-
*
|
|
249
|
+
* is a Debug behavior. Packaged apps opting into an embedded bundle or an explicit URL
|
|
250
|
+
* can preload without the launcher. In Release the launcher does nothing.
|
|
206
251
|
*
|
|
207
252
|
* The check reads the class name, because this pod has no dependency on expo-dev-launcher.
|
|
208
253
|
* `EXDevLauncherController` is the name in expo-dev-launcher 56 and 57. If a later version
|
|
@@ -213,7 +258,8 @@ extension ExpoHostRuntime: ReactHostPreloading {
|
|
|
213
258
|
*/
|
|
214
259
|
func canPreloadReactNative() -> Bool {
|
|
215
260
|
#if DEBUG
|
|
216
|
-
if
|
|
261
|
+
if !preferEmbeddedBundleInDebug && bundleURLOverride?() == nil,
|
|
262
|
+
NSClassFromString("EXDevLauncherController") != nil {
|
|
217
263
|
return false
|
|
218
264
|
}
|
|
219
265
|
#endif
|
|
@@ -251,8 +297,9 @@ class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
|
|
|
251
297
|
}
|
|
252
298
|
|
|
253
299
|
#if canImport(EXUpdates)
|
|
254
|
-
if
|
|
255
|
-
AppController.
|
|
300
|
+
if AppController.isInitialized(),
|
|
301
|
+
AppController.sharedInstance.isActiveController,
|
|
302
|
+
AppController.sharedInstance.isStarted,
|
|
256
303
|
let launchAssetURL = AppController.sharedInstance.launchAssetUrl()
|
|
257
304
|
{
|
|
258
305
|
return launchAssetURL
|
|
@@ -286,20 +333,19 @@ class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
|
|
|
286
333
|
*
|
|
287
334
|
* The override wins over every other step, thus an override makes the URL stable.
|
|
288
335
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* `bundleURL()` gives the embedded bundle. A host from that time keeps the embedded bundle, and
|
|
292
|
-
* Expo never applies the update. Thus the URL is stable only after `launchAssetUrl()` has a
|
|
293
|
-
* value. The class can be in the binary while the app does not use it, thus this check reads the
|
|
294
|
-
* state and not the class.
|
|
336
|
+
* The shared runtime waits for Updates before preloading or creating views. Inactive
|
|
337
|
+
* controllers use the normal bundle resolver and do not need an Updates launch asset.
|
|
295
338
|
*/
|
|
296
339
|
var isBundleURLStable: Bool {
|
|
297
340
|
if bundleURLOverride?() != nil {
|
|
298
341
|
return true
|
|
299
342
|
}
|
|
300
343
|
|
|
301
|
-
#if canImport(EXUpdates)
|
|
302
|
-
|
|
344
|
+
#if canImport(EXUpdates)
|
|
345
|
+
guard AppController.isInitialized() else { return false }
|
|
346
|
+
return !AppController.sharedInstance.isActiveController
|
|
347
|
+
|| (AppController.sharedInstance.isStarted
|
|
348
|
+
&& AppController.sharedInstance.launchAssetUrl() != nil)
|
|
303
349
|
#else
|
|
304
350
|
return true
|
|
305
351
|
#endif
|