LogBox - lazily initialize on iOS, use sync APIs

Summary:
Update LogBox on iOS to lazily initialize, using a synchronous RCTSurface, behind RCTSharedApplication checks.

This results in faster of LogBox, without keeping around a long lived window in the background, and only used when LogBox is used.

On Android, we still start the react app in the background but we create a dialog when it's shown and then destroy it when it's hidden. Once we have the sync APIs on android we can update it to use the same strategy.

Changelog: [Internal]

Reviewed By: fkgozali

Differential Revision: D18925538

fbshipit-source-id: 1a72c39aa0fc26c8ba657d36c7fa7bc0ae777eb9
This commit is contained in:
Rick Hanlon
2019-12-13 03:07:06 -08:00
parent e97baa6066
commit 0fd83e4786
7 changed files with 152 additions and 167 deletions
+34 -24
View File
@@ -22,7 +22,7 @@ import type {
} from './parseLogBoxLog';
import parseErrorStack from '../../Core/Devtools/parseErrorStack';
import type {ExtendedError} from '../../Core/Devtools/parseErrorStack';
import NativeLogBox from '../../NativeModules/specs/NativeLogBox';
export type LogBoxLogs = Set<LogBoxLog>;
export type LogData = $ReadOnly<{|
level: LogLevel,
@@ -86,20 +86,6 @@ const LOGBOX_ERROR_MESSAGE =
'An error was thrown when attempting to render log messages via LogBox.';
function getNextState() {
const logArray = Array.from(logs);
let index = logArray.length - 1;
while (index >= 0) {
// The latest syntax error is selected and displayed before all other logs.
if (logArray[index].level === 'syntax') {
return {
logs,
isDisabled: _isDisabled,
selectedLogIndex: index,
};
}
index -= 1;
}
return {
logs,
isDisabled: _isDisabled,
@@ -175,9 +161,10 @@ function appendNewLog(newLog) {
let addPendingLog = () => {
logs.add(newLog);
if (_selectedIndex <= 0) {
_selectedIndex = logs.size - 1;
setSelectedLog(logs.size - 1);
} else {
handleUpdate();
}
handleUpdate();
addPendingLog = null;
};
@@ -196,6 +183,9 @@ function appendNewLog(newLog) {
handleUpdate();
}
});
} else if (newLog.level === 'syntax') {
logs.add(newLog);
setSelectedLog(logs.size - 1);
} else {
logs.add(newLog);
handleUpdate();
@@ -259,21 +249,42 @@ export function symbolicateLogLazy(log: LogBoxLog) {
export function clear(): void {
if (logs.size > 0) {
logs = new Set();
_selectedIndex = -1;
handleUpdate();
setSelectedLog(-1);
}
}
export function setSelectedLog(index: number): void {
_selectedIndex = index;
export function setSelectedLog(proposedNewIndex: number): void {
const oldIndex = _selectedIndex;
let newIndex = proposedNewIndex;
const logArray = Array.from(logs);
let index = logArray.length - 1;
while (index >= 0) {
// The latest syntax error is selected and displayed before all other logs.
if (logArray[index].level === 'syntax') {
newIndex = index;
break;
}
index -= 1;
}
_selectedIndex = newIndex;
handleUpdate();
if (NativeLogBox) {
setTimeout(() => {
if (oldIndex < 0 && newIndex >= 0) {
NativeLogBox.show();
} else if (oldIndex >= 0 && newIndex < 0) {
NativeLogBox.hide();
}
}, 0);
}
}
export function clearWarnings(): void {
const newLogs = Array.from(logs).filter(log => log.level !== 'warn');
if (newLogs.length !== logs.size) {
logs = new Set(newLogs);
_selectedIndex = -1;
setSelectedLog(-1);
handleUpdate();
}
}
@@ -284,8 +295,7 @@ export function clearErrors(): void {
);
if (newLogs.length !== logs.size) {
logs = new Set(newLogs);
_selectedIndex = -1;
handleUpdate();
setSelectedLog(-1);
}
}
+9 -27
View File
@@ -15,7 +15,6 @@ import {View, StyleSheet} from 'react-native';
import * as LogBoxData from './Data/LogBoxData';
import LogBoxInspector from './UI/LogBoxInspector';
import type LogBoxLog from './Data/LogBoxLog';
import NativeLogBox from '../NativeModules/specs/NativeLogBox';
type Props = $ReadOnly<{|
logs: $ReadOnlyArray<LogBoxLog>,
@@ -23,35 +22,18 @@ type Props = $ReadOnly<{|
isDisabled?: ?boolean,
|}>;
function NativeLogBoxVisibility(props) {
React.useLayoutEffect(() => {
if (NativeLogBox) {
if (props.visible) {
// Schedule this to try and prevent flashing the old state.
setTimeout(() => NativeLogBox.show(), 10);
} else {
NativeLogBox.hide();
}
}
}, [props.visible]);
return props.children;
}
export class _LogBoxInspectorContainer extends React.Component<Props> {
render(): React.Node {
return (
<NativeLogBoxVisibility visible={this.props.selectedLogIndex >= 0}>
<View style={StyleSheet.absoluteFill}>
<LogBoxInspector
onDismiss={this._handleDismiss}
onMinimize={this._handleMinimize}
onChangeSelectedIndex={this._handleSetSelectedLog}
logs={this.props.logs}
selectedIndex={this.props.selectedLogIndex}
/>
</View>
</NativeLogBoxVisibility>
<View style={StyleSheet.absoluteFill}>
<LogBoxInspector
onDismiss={this._handleDismiss}
onMinimize={this._handleMinimize}
onChangeSelectedIndex={this._handleSetSelectedLog}
logs={this.props.logs}
selectedIndex={this.props.selectedLogIndex}
/>
</View>
);
}
+1 -1
View File
@@ -36,8 +36,8 @@ type Props = $ReadOnly<{|
function LogBoxInspector(props: Props): React.Node {
const {logs, selectedIndex} = props;
let log = logs[selectedIndex];
const log = logs[selectedIndex];
React.useEffect(() => {
if (log) {
LogBoxData.symbolicateLogNow(log);
+4 -2
View File
@@ -101,9 +101,11 @@ const styles = StyleSheet.create({
syntaxErrorText: {
textAlign: 'center',
width: '100%',
height: 48,
fontSize: 14,
paddingTop: 15,
paddingBottom: 15,
lineHeight: 20,
paddingTop: 20,
paddingBottom: 50,
fontStyle: 'italic',
color: LogBoxStyle.getTextColor(0.6),
},
@@ -29,8 +29,10 @@ exports[`LogBoxInspectorFooter should render no buttons and a message for syntax
"color": "rgba(255, 255, 255, 0.6)",
"fontSize": 14,
"fontStyle": "italic",
"paddingBottom": 15,
"paddingTop": 15,
"height": 48,
"lineHeight": 20,
"paddingBottom": 50,
"paddingTop": 20,
"textAlign": "center",
"width": "100%",
}
@@ -1,66 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LogBoxNotificationContainer should render inspector with logs, even when disabled 1`] = `
<NativeLogBoxVisibility
visible={false}
>
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
<View
style={
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
>
<LogBoxInspector
logs={
Array [
LogBoxLog {
"category": "Some kind of message",
"codeFrame": undefined,
"componentStack": Array [],
"count": 1,
"isComponentError": false,
"level": "warn",
"message": Object {
"content": "Some kind of message",
"substitutions": Array [],
},
"stack": Array [],
"symbolicated": Object {
"error": null,
"stack": null,
"status": "NONE",
},
}
>
<LogBoxInspector
logs={
Array [
LogBoxLog {
"category": "Some kind of message",
"codeFrame": undefined,
"componentStack": Array [],
"count": 1,
"isComponentError": false,
"level": "warn",
"message": Object {
"content": "Some kind of message",
"substitutions": Array [],
},
LogBoxLog {
"category": "Some kind of message (latest)",
"codeFrame": undefined,
"componentStack": Array [],
"count": 1,
"isComponentError": false,
"level": "error",
"message": Object {
"content": "Some kind of message (latest)",
"substitutions": Array [],
},
"stack": Array [],
"symbolicated": Object {
"error": null,
"stack": null,
"status": "NONE",
},
"stack": Array [],
"symbolicated": Object {
"error": null,
"stack": null,
"status": "NONE",
},
]
}
onChangeSelectedIndex={[Function]}
onDismiss={[Function]}
onMinimize={[Function]}
selectedIndex={-1}
/>
</View>
</NativeLogBoxVisibility>
},
LogBoxLog {
"category": "Some kind of message (latest)",
"codeFrame": undefined,
"componentStack": Array [],
"count": 1,
"isComponentError": false,
"level": "error",
"message": Object {
"content": "Some kind of message (latest)",
"substitutions": Array [],
},
"stack": Array [],
"symbolicated": Object {
"error": null,
"stack": null,
"status": "NONE",
},
},
]
}
onChangeSelectedIndex={[Function]}
onDismiss={[Function]}
onMinimize={[Function]}
selectedIndex={-1}
/>
</View>
`;
+45 -52
View File
@@ -17,6 +17,8 @@
#import <React/RCTJSStackFrame.h>
#import <React/RCTRedBoxSetEnabled.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTRedBoxSetEnabled.h>
#import <React/RCTSurface.h>
#import <React/RCTUtils.h>
#import <objc/runtime.h>
@@ -25,61 +27,52 @@
#if RCT_DEV_MENU
@class RCTLogBoxWindow;
@protocol RCTLogBoxWindowActionDelegate <NSObject>
- (void)logBoxWindow:(RCTLogBoxWindow *)logBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromlogBoxWindow:(RCTLogBoxWindow *)logBoxWindow;
- (void)loadExtraDataViewController;
@class RCTLogBoxView;
@interface RCTLogBoxView : UIView
@end
@interface RCTLogBoxWindow : UIWindow <UITableViewDelegate>
@property (nonatomic, weak) id<RCTLogBoxWindowActionDelegate> actionDelegate;
@end
@implementation RCTLogBoxWindow
@implementation RCTLogBoxView
{
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
int _lastErrorCookie;
UIViewController *_rootViewController;
RCTSurface *_surface;
}
- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge
{
_lastErrorCookie = -1;
if ((self = [super initWithFrame:frame])) {
#if TARGET_OS_TV
self.windowLevel = UIWindowLevelAlert + 1000;
#else
self.windowLevel = UIWindowLevelStatusBar - 1;
#endif
self.backgroundColor = [UIColor clearColor];
self.hidden = YES;
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"LogBox" initialProperties:nil];
_surface = [[RCTSurface alloc] initWithBridge:bridge moduleName:@"LogBox" initialProperties:@{}];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.rootViewController = rootViewController;
[_surface start];
[_surface setSize:frame.size];
if (![_surface synchronouslyWaitForStage:RCTSurfaceStageSurfaceDidInitialMounting timeout:.5]) {
RCTLogInfo(@"Failed to mount LogBox within 500ms");
}
_rootViewController = [UIViewController new];
_rootViewController.view = (UIView *)_surface.view;
_rootViewController.view.backgroundColor = [UIColor clearColor];
_rootViewController.modalPresentationStyle = UIModalPresentationFullScreen;
}
return self;
}
- (void)dealloc
{
// Dismiss by deallocating the window.
// This will also handle JS reload, otherwise the LogBox view would be stuck on top.
[_rootViewController.view resignFirstResponder];
[_rootViewController dismissViewControllerAnimated:NO completion:NULL];
}
- (void)show
{
[self becomeFirstResponder];
[self makeKeyAndVisible];
}
- (void)dismiss
{
self.hidden = YES;
[self resignFirstResponder];
[RCTSharedApplication().delegate.window makeKeyWindow];
[RCTSharedApplication().delegate.window.rootViewController presentViewController:_rootViewController animated:NO completion:^{
[self->_rootViewController.view becomeFirstResponder];
}];
}
@end
@@ -89,7 +82,7 @@
@implementation RCTLogBox
{
RCTLogBoxWindow *_window;
RCTLogBoxView *_view;
}
@synthesize bridge = _bridge;
@@ -101,34 +94,34 @@ RCT_EXPORT_MODULE()
return YES;
}
- (void)setBridge:(RCTBridge *)bridge
RCT_EXPORT_METHOD(show)
{
_bridge = bridge;
if (RCTRedBoxGetEnabled()) {
dispatch_async(dispatch_get_main_queue(), ^{
self->_window = [[RCTLogBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds bridge: self->_bridge];
if (!self->_view) {
self->_view = [[RCTLogBoxView alloc] initWithFrame:[UIScreen mainScreen].bounds bridge: self->_bridge];
}
[self->_view show];
});
}
}
RCT_EXPORT_METHOD(show)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_window show];
});
}
RCT_EXPORT_METHOD(hide)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_window dismiss];
});
if (RCTRedBoxGetEnabled()) {
dispatch_async(dispatch_get_main_queue(), ^{
self->_view = nil;
});
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return std::make_shared<facebook::react::NativeLogBoxSpecJSI>(self, jsInvoker);
if (RCTRedBoxGetEnabled()) {
return std::make_shared<facebook::react::NativeLogBoxSpecJSI>(self, jsInvoker);
}
return nullptr;
}
@end