@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,50 @@
|
|
|
1
|
+
#if canImport(EXUpdates)
|
|
2
|
+
import Foundation
|
|
3
|
+
internal import EXUpdates
|
|
4
|
+
|
|
5
|
+
final class ExpoUpdatesStartup: NSObject {
|
|
6
|
+
private var updatesReady = false
|
|
7
|
+
private var updatesCallbacks: [() -> Void] = []
|
|
8
|
+
|
|
9
|
+
// Updates belongs to the application lifetime, not to a view or React Host session.
|
|
10
|
+
// Inactive controllers (including DevLauncherAppController) must never be started here.
|
|
11
|
+
func prepareReactNative(_ completion: @escaping () -> Void) {
|
|
12
|
+
if !Thread.isMainThread {
|
|
13
|
+
DispatchQueue.main.async { [weak self] in self?.prepareReactNative(completion) }
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
AppController.initializeWithoutStarting()
|
|
17
|
+
let controller = AppController.sharedInstance
|
|
18
|
+
guard controller.isActiveController else { completion(); return }
|
|
19
|
+
// expo-updates 57 traps in launchAssetUrl() until start() sets its startup procedure.
|
|
20
|
+
if updatesReady || (controller.isStarted && controller.launchAssetUrl() != nil) {
|
|
21
|
+
completion()
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
updatesCallbacks.append(completion)
|
|
25
|
+
controller.delegate = self
|
|
26
|
+
if !controller.isStarted {
|
|
27
|
+
controller.start()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func cancelPendingRequests() { updatesCallbacks.removeAll() }
|
|
32
|
+
}
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
#if canImport(EXUpdates)
|
|
36
|
+
extension ExpoUpdatesStartup: AppControllerDelegate {
|
|
37
|
+
func appController(_ appController: any AppControllerInterface, didStartWithSuccess success: Bool) {
|
|
38
|
+
DispatchQueue.main.async { [weak self] in
|
|
39
|
+
guard let self else { return }
|
|
40
|
+
self.updatesReady = true
|
|
41
|
+
if !success {
|
|
42
|
+
NSLog("%@", "Expo Updates failed to select a launch asset; using the bundle resolver fallback.")
|
|
43
|
+
}
|
|
44
|
+
let callbacks = self.updatesCallbacks
|
|
45
|
+
self.updatesCallbacks.removeAll()
|
|
46
|
+
callbacks.forEach { $0() }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
#endif
|
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Watches the
|
|
4
|
+
* Watches the Brownfield timing-ready notification, and calls the callback that waits for it.
|
|
5
5
|
*
|
|
6
|
-
* React Native
|
|
7
|
-
*
|
|
6
|
+
* The timing observer forwards React Native completion to the main thread after capturing
|
|
7
|
+
* timings. This class also delivers callbacks on the main thread for UI updates.
|
|
8
8
|
*
|
|
9
9
|
* The class starts to watch at the initialization, and not at the first callback. The class then
|
|
10
10
|
* knows that React Native loaded the bundle, also if nobody waited for the notification.
|
|
11
11
|
*/
|
|
12
12
|
final class JSBundleLoadObserver {
|
|
13
|
-
private var pendingCallback: (() -> Void)?
|
|
13
|
+
private var pendingCallback: ((JSBundleTimings) -> Void)?
|
|
14
14
|
private var didLoadBundle = false
|
|
15
|
-
private var
|
|
15
|
+
private var latestTimings: JSBundleTimings?
|
|
16
|
+
private var observerTokens: [NSObjectProtocol] = []
|
|
16
17
|
|
|
17
18
|
init() {
|
|
18
|
-
|
|
19
|
-
forName:
|
|
19
|
+
observerTokens.append(NotificationCenter.default.addObserver(
|
|
20
|
+
forName: .JSBundleTimingDidLoad,
|
|
20
21
|
object: nil,
|
|
21
22
|
queue: .main
|
|
22
23
|
) { [weak self] _ in
|
|
23
24
|
self?.bundleDidLoad()
|
|
24
|
-
}
|
|
25
|
+
})
|
|
26
|
+
observerTokens.append(NotificationCenter.default.addObserver(
|
|
27
|
+
forName: NSNotification.Name("RCTJavaScriptDidFailToLoadNotification"),
|
|
28
|
+
object: nil,
|
|
29
|
+
queue: .main
|
|
30
|
+
) { [weak self] _ in
|
|
31
|
+
self?.bundleDidFailToLoad()
|
|
32
|
+
})
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
deinit {
|
|
28
|
-
|
|
29
|
-
NotificationCenter.default.removeObserver(observerToken)
|
|
30
|
-
}
|
|
36
|
+
observerTokens.forEach(NotificationCenter.default.removeObserver)
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -35,31 +41,37 @@ final class JSBundleLoadObserver {
|
|
|
35
41
|
* React Native already loaded the bundle, the class calls the callback in the next turn of the
|
|
36
42
|
* main run loop.
|
|
37
43
|
*
|
|
38
|
-
* @param onBundleLoaded The class always calls this callback on the main thread
|
|
44
|
+
* @param onBundleLoaded The class always calls this callback on the main thread, with the
|
|
45
|
+
* timings of the session that loaded the bundle.
|
|
39
46
|
*/
|
|
40
|
-
func observe(onBundleLoaded: @escaping () -> Void) {
|
|
47
|
+
func observe(onBundleLoaded: @escaping (JSBundleTimings) -> Void) {
|
|
41
48
|
onMainThread { [weak self] in
|
|
42
49
|
self?.register(onBundleLoaded)
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/**
|
|
47
|
-
* Removes the callback that waits,
|
|
48
|
-
* method when you stop React Native. A callback of the earlier
|
|
49
|
-
* session.
|
|
54
|
+
* Removes the callback that waits, forgets the bundle of the earlier session, and clears stored
|
|
55
|
+
* JS bundle timings. Call this method when you stop React Native. A callback of the earlier
|
|
56
|
+
* session must not run in the next session, and the next session must not reuse the last timings.
|
|
50
57
|
*/
|
|
51
58
|
func reset() {
|
|
52
59
|
onMainThread { [weak self] in
|
|
53
60
|
self?.pendingCallback = nil
|
|
54
61
|
self?.didLoadBundle = false
|
|
62
|
+
self?.latestTimings = nil
|
|
63
|
+
JSBundleTimingObserver.reset()
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
// MARK: - Main thread only
|
|
59
68
|
|
|
60
|
-
private func register(_ onBundleLoaded: @escaping () -> Void) {
|
|
69
|
+
private func register(_ onBundleLoaded: @escaping (JSBundleTimings) -> Void) {
|
|
61
70
|
guard !didLoadBundle else {
|
|
62
|
-
|
|
71
|
+
let timings = latestTimings ?? Self.snapshotTimings()
|
|
72
|
+
DispatchQueue.main.async {
|
|
73
|
+
onBundleLoaded(timings)
|
|
74
|
+
}
|
|
63
75
|
return
|
|
64
76
|
}
|
|
65
77
|
|
|
@@ -68,11 +80,36 @@ final class JSBundleLoadObserver {
|
|
|
68
80
|
|
|
69
81
|
private func bundleDidLoad() {
|
|
70
82
|
didLoadBundle = true
|
|
83
|
+
latestTimings = Self.snapshotTimings()
|
|
71
84
|
|
|
72
85
|
let callback = pendingCallback
|
|
73
86
|
pendingCallback = nil
|
|
74
87
|
|
|
75
|
-
callback
|
|
88
|
+
if let callback, let timings = latestTimings {
|
|
89
|
+
callback(timings)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A failed load, for example a reload in development, replaces the bundle that loaded earlier.
|
|
95
|
+
* Forget it and its timings, so a new callback waits for the next load. A callback that waits
|
|
96
|
+
* keeps waiting: it belongs to this session, and runs when a reload succeeds.
|
|
97
|
+
*/
|
|
98
|
+
private func bundleDidFailToLoad() {
|
|
99
|
+
didLoadBundle = false
|
|
100
|
+
latestTimings = nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The timing observer posts our notification only after storing the snapshot. This avoids
|
|
105
|
+
* depending on NotificationCenter observer ordering, including for late registrations.
|
|
106
|
+
*/
|
|
107
|
+
private static func snapshotTimings() -> JSBundleTimings {
|
|
108
|
+
JSBundleTimings(
|
|
109
|
+
jsBundleLoadTime: JSBundleTimingObserver.jsBundleLoadTime,
|
|
110
|
+
jsBundleEvaluationTime: JSBundleTimingObserver.jsBundleEvaluationTime,
|
|
111
|
+
timeline: JSBundleTimingObserver.timeline
|
|
112
|
+
)
|
|
76
113
|
}
|
|
77
114
|
|
|
78
115
|
private func onMainThread(_ work: @escaping () -> Void) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
// Posted on main after the timing snapshot is ready.
|
|
6
|
+
FOUNDATION_EXPORT NSNotificationName const JSBundleTimingDidLoadNotification;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Captures JS bundle timings from `RCTPerformanceLogger` when React Native
|
|
10
|
+
* finishes (or fails) loading the bundle.
|
|
11
|
+
*
|
|
12
|
+
* The observer registers at binary load, before Swift host singletons run.
|
|
13
|
+
*/
|
|
14
|
+
@interface JSBundleTimingObserver : NSObject
|
|
15
|
+
|
|
16
|
+
@property (class, nonatomic, readonly, nullable) NSNumber *jsBundleLoadTime;
|
|
17
|
+
@property (class, nonatomic, readonly, nullable) NSNumber *jsBundleEvaluationTime;
|
|
18
|
+
|
|
19
|
+
// Completed startup intervals, in monotonic milliseconds.
|
|
20
|
+
@property (class, nonatomic, readonly) NSArray<NSDictionary<NSString *, id> *> *timeline;
|
|
21
|
+
|
|
22
|
+
+ (void)reset;
|
|
23
|
+
|
|
24
|
+
@end
|
|
25
|
+
|
|
26
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#import "JSBundleTimingObserver.h"
|
|
2
|
+
|
|
3
|
+
#import <React/RCTBridge.h>
|
|
4
|
+
#import <React/RCTConstants.h>
|
|
5
|
+
#import <React/RCTPerformanceLogger.h>
|
|
6
|
+
#import <React/RCTPLTag.h>
|
|
7
|
+
#import <objc/runtime.h>
|
|
8
|
+
|
|
9
|
+
static NSNumber *sJSBundleLoadTime;
|
|
10
|
+
static NSNumber *sJSBundleEvaluationTime;
|
|
11
|
+
static NSArray *sTimeline;
|
|
12
|
+
|
|
13
|
+
static NSString *const kRCTInstanceDidLoadBundleNotification = @"RCTInstanceDidLoadBundle";
|
|
14
|
+
|
|
15
|
+
NSNotificationName const JSBundleTimingDidLoadNotification = @"BrownfieldJSBundleTimingDidLoad";
|
|
16
|
+
|
|
17
|
+
// Check the endpoints: a completed interval can round to zero milliseconds,
|
|
18
|
+
// while an unfinished interval can produce a negative durationForTag: value.
|
|
19
|
+
static NSNumber *JSBundleTimingNumberOrNil(NSArray<NSNumber *> *values, RCTPLTag tag)
|
|
20
|
+
{
|
|
21
|
+
NSUInteger index = (NSUInteger)tag * 2;
|
|
22
|
+
if (values.count <= index + 1) {
|
|
23
|
+
return nil;
|
|
24
|
+
}
|
|
25
|
+
int64_t start = values[index].longLongValue;
|
|
26
|
+
int64_t stop = values[index + 1].longLongValue;
|
|
27
|
+
return start > 0 && stop >= start ? @(stop - start) : nil;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static void JSBundleTimingClearTagSnapshot(void)
|
|
31
|
+
{
|
|
32
|
+
sJSBundleLoadTime = nil;
|
|
33
|
+
sJSBundleEvaluationTime = nil;
|
|
34
|
+
sTimeline = @[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@implementation JSBundleTimingObserver
|
|
38
|
+
|
|
39
|
+
+ (NSNumber *)jsBundleLoadTime
|
|
40
|
+
{
|
|
41
|
+
return sJSBundleLoadTime;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
+ (NSNumber *)jsBundleEvaluationTime
|
|
45
|
+
{
|
|
46
|
+
return sJSBundleEvaluationTime;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
+ (NSArray *)timeline { return sTimeline ?: @[]; }
|
|
50
|
+
|
|
51
|
+
+ (void)reset
|
|
52
|
+
{
|
|
53
|
+
if ([NSThread isMainThread]) {
|
|
54
|
+
JSBundleTimingClearTagSnapshot();
|
|
55
|
+
} else {
|
|
56
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
57
|
+
[self reset];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
+ (RCTPerformanceLogger *)performanceLoggerFromNotification:(NSNotification *)notification
|
|
63
|
+
{
|
|
64
|
+
id bridge = notification.userInfo[@"bridge"];
|
|
65
|
+
RCTPerformanceLogger *logger = nil;
|
|
66
|
+
|
|
67
|
+
// RCTBridgeProxy inherits NSProxy's forwarding-based respondsToSelector:.
|
|
68
|
+
// It can report NO even for its synthesized performanceLogger getter. Inspect
|
|
69
|
+
// the actual class method table so we can safely call an implemented getter.
|
|
70
|
+
Class bridgeClass = object_getClass(bridge);
|
|
71
|
+
BOOL hasLoggerGetter = class_getInstanceMethod(bridgeClass, @selector(performanceLogger)) != NULL;
|
|
72
|
+
if (hasLoggerGetter) {
|
|
73
|
+
logger = [bridge performanceLogger];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return logger;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
+ (void)captureTimingsFromNotification:(NSNotification *)notification
|
|
80
|
+
{
|
|
81
|
+
RCTPerformanceLogger *logger = [self performanceLoggerFromNotification:notification];
|
|
82
|
+
if (logger == nil) {
|
|
83
|
+
JSBundleTimingClearTagSnapshot();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
NSArray<NSNumber *> *values = [logger valuesForTags];
|
|
88
|
+
sJSBundleLoadTime = JSBundleTimingNumberOrNil(values, RCTPLScriptDownload);
|
|
89
|
+
sJSBundleEvaluationTime = JSBundleTimingNumberOrNil(values, RCTPLScriptExecution);
|
|
90
|
+
// These intervals can overlap. Preserve endpoints instead of adding durations.
|
|
91
|
+
RCTPLTag tags[] = {RCTPLScriptDownload, RCTPLScriptExecution};
|
|
92
|
+
NSArray *names = @[@"RCTPLScriptDownload", @"RCTPLScriptExecution"];
|
|
93
|
+
NSMutableArray *timeline = [NSMutableArray new];
|
|
94
|
+
for (NSUInteger i = 0; i < 2; i++) {
|
|
95
|
+
NSUInteger index = (NSUInteger)tags[i] * 2;
|
|
96
|
+
if (JSBundleTimingNumberOrNil(values, tags[i]) != nil) {
|
|
97
|
+
[timeline addObject:@{@"tag": names[i], @"startMs": values[index], @"stopMs": values[index + 1]}];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
sTimeline = [timeline sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"startMs" ascending:YES]]];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@end
|
|
104
|
+
|
|
105
|
+
__attribute__((constructor)) static void JSBundleTimingObserverRegister(void)
|
|
106
|
+
{
|
|
107
|
+
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
|
108
|
+
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
|
|
109
|
+
|
|
110
|
+
[center addObserverForName:RCTJavaScriptDidLoadNotification
|
|
111
|
+
object:nil
|
|
112
|
+
queue:mainQueue
|
|
113
|
+
usingBlock:^(NSNotification *notification) {
|
|
114
|
+
[JSBundleTimingObserver captureTimingsFromNotification:notification];
|
|
115
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:JSBundleTimingDidLoadNotification object:nil];
|
|
116
|
+
}];
|
|
117
|
+
|
|
118
|
+
[center addObserverForName:kRCTInstanceDidLoadBundleNotification
|
|
119
|
+
object:nil
|
|
120
|
+
queue:mainQueue
|
|
121
|
+
usingBlock:^(NSNotification *notification) {
|
|
122
|
+
// RN versions that expose the instance logger on the bridge proxy
|
|
123
|
+
// send RCTJavaScriptDidLoadNotification next, with that proxy.
|
|
124
|
+
// currentBridge is a stub, and this earlier notification has no payload.
|
|
125
|
+
Class proxyClass = NSClassFromString(@"RCTBridgeProxy");
|
|
126
|
+
BOOL hasLoggerSetter = [proxyClass instancesRespondToSelector:NSSelectorFromString(@"setPerformanceLogger:")];
|
|
127
|
+
if (hasLoggerSetter) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
[JSBundleTimingObserver captureTimingsFromNotification:notification];
|
|
131
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:JSBundleTimingDidLoadNotification object:nil];
|
|
132
|
+
}];
|
|
133
|
+
|
|
134
|
+
[center addObserverForName:RCTJavaScriptDidFailToLoadNotification
|
|
135
|
+
object:nil
|
|
136
|
+
queue:mainQueue
|
|
137
|
+
usingBlock:^(__unused NSNotification *notification) {
|
|
138
|
+
[JSBundleTimingObserver reset];
|
|
139
|
+
}];
|
|
140
|
+
}
|
|
@@ -103,10 +103,14 @@ protocol ReactHostPreloading: AnyObject {
|
|
|
103
103
|
*/
|
|
104
104
|
func canPreloadReactNative() -> Bool
|
|
105
105
|
|
|
106
|
+
func prepareReactNative(_ completion: @escaping () -> Void)
|
|
107
|
+
|
|
106
108
|
func prepareDevLoadingView()
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
extension ReactHostPreloading {
|
|
112
|
+
func prepareReactNative(_ completion: @escaping () -> Void) { completion() }
|
|
113
|
+
|
|
110
114
|
/**
|
|
111
115
|
* The implementation of
|
|
112
116
|
* `ReactNativeBrownfield.startReactNative(launchOptions:preloadBundle:onBundleLoaded:)`, which
|
|
@@ -115,7 +119,7 @@ extension ReactHostPreloading {
|
|
|
115
119
|
func startReactNative(
|
|
116
120
|
launchOptions: [AnyHashable: Any]?,
|
|
117
121
|
preloadBundle: Bool,
|
|
118
|
-
onBundleLoaded: (() -> Void)?
|
|
122
|
+
onBundleLoaded: ((JSBundleTimings) -> Void)?
|
|
119
123
|
) {
|
|
120
124
|
preloadState.storeLaunchOptions(launchOptions)
|
|
121
125
|
|
|
@@ -131,10 +135,10 @@ extension ReactHostPreloading {
|
|
|
131
135
|
preloadState.requestPreload()
|
|
132
136
|
|
|
133
137
|
if Thread.isMainThread {
|
|
134
|
-
createReactHost()
|
|
138
|
+
prepareReactNative { [weak self] in self?.createReactHost() }
|
|
135
139
|
} else {
|
|
136
140
|
DispatchQueue.main.async { [weak self] in
|
|
137
|
-
self?.createReactHost()
|
|
141
|
+
self?.prepareReactNative { [weak self] in self?.createReactHost() }
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
}
|
|
@@ -4,6 +4,35 @@ import UIKit
|
|
|
4
4
|
internal import Expo
|
|
5
5
|
#endif
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* JS bundle timings from React Native's performance logger.
|
|
9
|
+
*
|
|
10
|
+
* - `jsBundleLoadTime` is `RCTPLScriptDownload` (Metro HTTP in Debug, file read in Release).
|
|
11
|
+
* - `jsBundleEvaluationTime` is `RCTPLScriptExecution`.
|
|
12
|
+
*
|
|
13
|
+
* A tag field is `nil` when the tag never completed (failed load, logger missing, or
|
|
14
|
+
* bridgeless React Native below 0.87).
|
|
15
|
+
*/
|
|
16
|
+
@objc public class JSBundleTimings: NSObject {
|
|
17
|
+
@objc public let jsBundleLoadTime: NSNumber?
|
|
18
|
+
@objc public let jsBundleEvaluationTime: NSNumber?
|
|
19
|
+
@objc public let timeline: [[String: Any]]
|
|
20
|
+
|
|
21
|
+
@objc public init(jsBundleLoadTime: NSNumber?, jsBundleEvaluationTime: NSNumber?) {
|
|
22
|
+
self.timeline = []
|
|
23
|
+
self.jsBundleLoadTime = jsBundleLoadTime
|
|
24
|
+
self.jsBundleEvaluationTime = jsBundleEvaluationTime
|
|
25
|
+
super.init()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc public init(jsBundleLoadTime: NSNumber?, jsBundleEvaluationTime: NSNumber?, timeline: [[String: Any]]) {
|
|
29
|
+
self.jsBundleLoadTime = jsBundleLoadTime
|
|
30
|
+
self.jsBundleEvaluationTime = jsBundleEvaluationTime
|
|
31
|
+
self.timeline = timeline
|
|
32
|
+
super.init()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
@objc public class ReactNativeBrownfield: NSObject {
|
|
8
37
|
public static let shared = ReactNativeBrownfield()
|
|
9
38
|
|
|
@@ -100,6 +129,7 @@ internal import Expo
|
|
|
100
129
|
* Stops React Native.
|
|
101
130
|
*/
|
|
102
131
|
@objc public func stopReactNative() {
|
|
132
|
+
NotificationCenter.default.post(name: Notification.Name("BrownfieldPerformanceStop"), object: nil)
|
|
103
133
|
#if canImport(Expo)
|
|
104
134
|
ExpoHostRuntime.shared.stopReactNative()
|
|
105
135
|
#else
|
|
@@ -112,19 +142,57 @@ internal import Expo
|
|
|
112
142
|
initialProps: [AnyHashable: Any]?,
|
|
113
143
|
launchOptions: [AnyHashable: Any]? = nil
|
|
114
144
|
) -> UIView? {
|
|
145
|
+
makeView(moduleName: moduleName, initialProps: initialProps, launchOptions: launchOptions,
|
|
146
|
+
session: nil, isHostVisible: nil)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Creates a measured presentation. The completion receives one immutable snapshot on main.
|
|
150
|
+
/// Opt in to wait for JS `markFullyDisplayed(brownfieldPresentationID)`.
|
|
151
|
+
/// Thread sampling requires both collectThreadMetrics and an onMetrics callback.
|
|
152
|
+
@objc public func view(
|
|
153
|
+
moduleName: String,
|
|
154
|
+
initialProps: [AnyHashable: Any]?,
|
|
155
|
+
launchOptions: [AnyHashable: Any]? = nil,
|
|
156
|
+
waitForFullDisplay: Bool = false,
|
|
157
|
+
collectThreadMetrics: Bool = false,
|
|
158
|
+
onMetrics: ((BrownfieldDisplayMetrics) -> Void)?
|
|
159
|
+
) -> UIView? {
|
|
160
|
+
let session = BrownfieldDisplaySession(moduleName: moduleName,
|
|
161
|
+
waitForFullDisplay: waitForFullDisplay, collectThreadMetrics: collectThreadMetrics,
|
|
162
|
+
callback: onMetrics)
|
|
163
|
+
return makeView(moduleName: moduleName, initialProps: initialProps,
|
|
164
|
+
launchOptions: launchOptions, session: session, isHostVisible: nil)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Only measured presentations get a session. Without one, `initialProps` pass through as is.
|
|
168
|
+
/// `isHostVisible` is set by `ReactNativeViewController`, which reports appearance itself.
|
|
169
|
+
internal func makeView(moduleName: String, initialProps: [AnyHashable: Any]?,
|
|
170
|
+
launchOptions: [AnyHashable: Any]?, session: BrownfieldDisplaySession?,
|
|
171
|
+
isHostVisible: (() -> Bool)?) -> UIView? {
|
|
172
|
+
var props = initialProps
|
|
173
|
+
if let session {
|
|
174
|
+
props = (props ?? [:]).merging(["brownfieldPresentationID": session.id]) { $1 }
|
|
175
|
+
}
|
|
176
|
+
// The probe goes into the React Native root, not into a placeholder, so initial display
|
|
177
|
+
// can't complete before the root exists.
|
|
178
|
+
let attachProbe: (UIView?) -> Void = { rootView in
|
|
179
|
+
guard let session else { return }
|
|
180
|
+
guard let rootView else { session.cancel(); return }
|
|
181
|
+
let probe = BrownfieldAppearanceProbe(session: session)
|
|
182
|
+
probe.isHostVisible = isHostVisible
|
|
183
|
+
session.hasRootView = true
|
|
184
|
+
rootView.addSubview(probe)
|
|
185
|
+
}
|
|
115
186
|
#if canImport(Expo)
|
|
116
|
-
ExpoHostRuntime.shared.view(
|
|
117
|
-
|
|
118
|
-
initialProps: initialProps,
|
|
119
|
-
launchOptions: launchOptions
|
|
120
|
-
)
|
|
187
|
+
let view = ExpoHostRuntime.shared.view(moduleName: moduleName, initialProps: props,
|
|
188
|
+
launchOptions: launchOptions, onRootView: attachProbe)
|
|
121
189
|
#else
|
|
122
|
-
ReactNativeHostRuntime.shared.view(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
launchOptions: launchOptions
|
|
126
|
-
)
|
|
190
|
+
let view = ReactNativeHostRuntime.shared.view(moduleName: moduleName, initialProps: props,
|
|
191
|
+
launchOptions: launchOptions)
|
|
192
|
+
attachProbe(view)
|
|
127
193
|
#endif
|
|
194
|
+
if view == nil { session?.cancel() }
|
|
195
|
+
return view
|
|
128
196
|
}
|
|
129
197
|
|
|
130
198
|
/**
|
|
@@ -193,10 +261,10 @@ internal import Expo
|
|
|
193
261
|
* Starts React Native with optional callback when bundle is loaded.
|
|
194
262
|
*
|
|
195
263
|
* @param onBundleLoaded Optional callback invoked on the main thread after the JS bundle is
|
|
196
|
-
* fully loaded. It runs in the next turn of the main run loop
|
|
197
|
-
* loaded. A new callback replaces the callback that waits.
|
|
264
|
+
* fully loaded. It receives `JSBundleTimings`. It runs in the next turn of the main run loop
|
|
265
|
+
* when the bundle is already loaded. A new callback replaces the callback that waits.
|
|
198
266
|
*/
|
|
199
|
-
@objc public func startReactNative(onBundleLoaded: (() -> Void)?) {
|
|
267
|
+
@objc public func startReactNative(onBundleLoaded: ((JSBundleTimings) -> Void)?) {
|
|
200
268
|
#if canImport(Expo)
|
|
201
269
|
ExpoHostRuntime.shared.startReactNative(onBundleLoaded: onBundleLoaded)
|
|
202
270
|
#else
|
|
@@ -220,7 +288,7 @@ internal import Expo
|
|
|
220
288
|
@objc public func startReactNative(
|
|
221
289
|
launchOptions: [AnyHashable: Any]?,
|
|
222
290
|
preloadBundle: Bool,
|
|
223
|
-
onBundleLoaded: (() -> Void)?
|
|
291
|
+
onBundleLoaded: ((JSBundleTimings) -> Void)?
|
|
224
292
|
) {
|
|
225
293
|
#if canImport(Expo)
|
|
226
294
|
ExpoHostRuntime.shared.startReactNative(
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
79FEE77E2FB499E400E407DE /* ReactNativeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEE77C2FB499E400E407DE /* ReactNativeViewController.swift */; };
|
|
18
18
|
79FEE7802FB499EC00E407DE /* ReactNativeHostRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEE77F2FB499EC00E407DE /* ReactNativeHostRuntime.swift */; };
|
|
19
19
|
79FEE7822FB499F300E407DE /* ExpoHostRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEE7812FB499F300E407DE /* ExpoHostRuntime.swift */; };
|
|
20
|
+
79FEE7852FB500A100E407DE /* JSBundleTimingObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 79FEE7842FB500A100E407DE /* JSBundleTimingObserver.m */; };
|
|
21
|
+
BEAF10000000000000000000 /* BrownfieldDisplayMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEAF00000000000000000000 /* BrownfieldDisplayMetrics.swift */; };
|
|
22
|
+
BEAF10000000000000000002 /* BrownfieldPerformanceBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = BEAF00000000000000000002 /* BrownfieldPerformanceBridge.mm */; };
|
|
20
23
|
/* End PBXBuildFile section */
|
|
21
24
|
|
|
22
25
|
/* Begin PBXCopyFilesBuildPhase section */
|
|
@@ -44,6 +47,11 @@
|
|
|
44
47
|
79FEE77C2FB499E400E407DE /* ReactNativeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactNativeViewController.swift; sourceTree = "<group>"; };
|
|
45
48
|
79FEE77F2FB499EC00E407DE /* ReactNativeHostRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactNativeHostRuntime.swift; sourceTree = "<group>"; };
|
|
46
49
|
79FEE7812FB499F300E407DE /* ExpoHostRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpoHostRuntime.swift; sourceTree = "<group>"; };
|
|
50
|
+
79FEE7832FB500A100E407DE /* JSBundleTimingObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSBundleTimingObserver.h; sourceTree = "<group>"; };
|
|
51
|
+
79FEE7842FB500A100E407DE /* JSBundleTimingObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JSBundleTimingObserver.m; sourceTree = "<group>"; };
|
|
52
|
+
BEAF00000000000000000000 /* BrownfieldDisplayMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldDisplayMetrics.swift; sourceTree = "<group>"; };
|
|
53
|
+
BEAF00000000000000000001 /* BrownfieldPerformanceBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BrownfieldPerformanceBridge.h; sourceTree = "<group>"; };
|
|
54
|
+
BEAF00000000000000000002 /* BrownfieldPerformanceBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BrownfieldPerformanceBridge.mm; sourceTree = "<group>"; };
|
|
47
55
|
/* End PBXFileReference section */
|
|
48
56
|
|
|
49
57
|
/* Begin PBXFrameworksBuildPhase section */
|
|
@@ -65,7 +73,12 @@
|
|
|
65
73
|
79FEE7692FB4997100E407DE /* Expo */,
|
|
66
74
|
696CB22A230EF910000207AF /* Products */,
|
|
67
75
|
79FEE76D2FB499C700E407DE /* BrownfieldBundlePathResolver.swift */,
|
|
76
|
+
BEAF00000000000000000000 /* BrownfieldDisplayMetrics.swift */,
|
|
77
|
+
BEAF00000000000000000001 /* BrownfieldPerformanceBridge.h */,
|
|
78
|
+
BEAF00000000000000000002 /* BrownfieldPerformanceBridge.mm */,
|
|
68
79
|
79FEE76E2FB499C700E407DE /* JSBundleLoadObserver.swift */,
|
|
80
|
+
79FEE7832FB500A100E407DE /* JSBundleTimingObserver.h */,
|
|
81
|
+
79FEE7842FB500A100E407DE /* JSBundleTimingObserver.m */,
|
|
69
82
|
79FEE76F2FB499C700E407DE /* Notification+Brownfield.swift */,
|
|
70
83
|
79FEE7702FB499C700E407DE /* ReactNativeBrownfield.swift */,
|
|
71
84
|
79FEE7712FB499C700E407DE /* ReactNativeBrownfieldModule.h */,
|
|
@@ -170,7 +183,10 @@
|
|
|
170
183
|
79FEE7762FB499C700E407DE /* Notification+Brownfield.swift in Sources */,
|
|
171
184
|
79FEE7822FB499F300E407DE /* ExpoHostRuntime.swift in Sources */,
|
|
172
185
|
79FEE7772FB499C700E407DE /* ReactNativeBrownfieldModule.swift in Sources */,
|
|
186
|
+
BEAF10000000000000000000 /* BrownfieldDisplayMetrics.swift in Sources */,
|
|
187
|
+
BEAF10000000000000000002 /* BrownfieldPerformanceBridge.mm in Sources */,
|
|
173
188
|
79FEE7782FB499C700E407DE /* JSBundleLoadObserver.swift in Sources */,
|
|
189
|
+
79FEE7852FB500A100E407DE /* JSBundleTimingObserver.m in Sources */,
|
|
174
190
|
79FEE7792FB499C700E407DE /* ReactNativeBrownfield.swift in Sources */,
|
|
175
191
|
79FEE77D2FB499E400E407DE /* ReactNativeView.swift in Sources */,
|
|
176
192
|
79FEE77E2FB499E400E407DE /* ReactNativeViewController.swift in Sources */,
|
|
@@ -191,7 +191,7 @@ final class ReactNativeHostRuntime {
|
|
|
191
191
|
*
|
|
192
192
|
* @param onBundleLoaded Optional callback invoked on the main thread after JS bundle is fully loaded.
|
|
193
193
|
*/
|
|
194
|
-
public func startReactNative(onBundleLoaded: (() -> Void)?) {
|
|
194
|
+
public func startReactNative(onBundleLoaded: ((JSBundleTimings) -> Void)?) {
|
|
195
195
|
// The callback registration is outside of the guard below. An earlier `startReactNative` call
|
|
196
196
|
// can already have made the factory, and the callback must still run.
|
|
197
197
|
if let onBundleLoaded {
|
|
@@ -17,11 +17,17 @@ struct ReactNativeViewRepresentable: UIViewControllerRepresentable {
|
|
|
17
17
|
var moduleName: String
|
|
18
18
|
var initialProperties: [String: Any] = [:]
|
|
19
19
|
var host: BrownfieldSurfaceHost
|
|
20
|
+
var waitForFullDisplay = false
|
|
21
|
+
var collectThreadMetrics = false
|
|
22
|
+
var onMetrics: ((BrownfieldDisplayMetrics) -> Void)?
|
|
20
23
|
|
|
21
24
|
func makeUIViewController(context: Context) -> UIViewController {
|
|
22
25
|
let controller = ReactNativeViewController(
|
|
23
26
|
moduleName: moduleName,
|
|
24
|
-
initialProperties: initialProperties
|
|
27
|
+
initialProperties: initialProperties,
|
|
28
|
+
waitForFullDisplay: waitForFullDisplay,
|
|
29
|
+
collectThreadMetrics: collectThreadMetrics,
|
|
30
|
+
onMetrics: onMetrics
|
|
25
31
|
)
|
|
26
32
|
controller.isHostedBySwiftUI = true
|
|
27
33
|
host.controller = controller
|
|
@@ -45,17 +51,29 @@ public struct ReactNativeView: View {
|
|
|
45
51
|
@State private var host = BrownfieldSurfaceHost()
|
|
46
52
|
var moduleName: String
|
|
47
53
|
var initialProperties: [String: Any] = [:]
|
|
54
|
+
var waitForFullDisplay = false
|
|
55
|
+
var collectThreadMetrics = false
|
|
56
|
+
var onMetrics: ((BrownfieldDisplayMetrics) -> Void)?
|
|
48
57
|
|
|
49
|
-
public init(moduleName: String, initialProperties: [String : Any] = [:]
|
|
58
|
+
public init(moduleName: String, initialProperties: [String : Any] = [:],
|
|
59
|
+
waitForFullDisplay: Bool = false,
|
|
60
|
+
collectThreadMetrics: Bool = false,
|
|
61
|
+
onMetrics: ((BrownfieldDisplayMetrics) -> Void)? = nil) {
|
|
50
62
|
self.moduleName = moduleName
|
|
51
63
|
self.initialProperties = initialProperties
|
|
64
|
+
self.waitForFullDisplay = waitForFullDisplay
|
|
65
|
+
self.collectThreadMetrics = collectThreadMetrics
|
|
66
|
+
self.onMetrics = onMetrics
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
public var body: some View {
|
|
55
70
|
ReactNativeViewRepresentable(
|
|
56
71
|
moduleName: moduleName,
|
|
57
72
|
initialProperties: initialProperties,
|
|
58
|
-
host: host
|
|
73
|
+
host: host,
|
|
74
|
+
waitForFullDisplay: waitForFullDisplay,
|
|
75
|
+
collectThreadMetrics: collectThreadMetrics,
|
|
76
|
+
onMetrics: onMetrics
|
|
59
77
|
)
|
|
60
78
|
.ignoresSafeArea(.all)
|
|
61
79
|
// `.popToNative` is public API and may be posted from any thread, while
|