LogBox - Optimistically symbolicate

Summary:
This diff adds optimistic loading for symbolicated stack traces by so that we (almost) never show a loading state for stack traces. Because of this, we also remove the "Stack Trace" status except when it is loading or failed. Also refactored the related components to hooks 🎣

Changelog: [Internal]

Reviewed By: mmmulani

Differential Revision: D18110403

fbshipit-source-id: a93b0a63e1c9490fea73ca6ec7c5707670bdea53
This commit is contained in:
Rick Hanlon
2019-10-31 16:26:42 -07:00
committed by Facebook Github Bot
parent f91a21b2c0
commit 8524b6182d
8 changed files with 243 additions and 206 deletions
+15
View File
@@ -82,6 +82,21 @@ export function add(level: LogLevel, args: $ReadOnlyArray<mixed>): void {
handleUpdate();
}
export function symbolicateLogNow(log: LogBoxLog) {
log.symbolicate(() => {
handleUpdate();
});
}
export function retrySymbolicateLogNow(log: LogBoxLog) {
log.retrySymbolicate(() => {
handleUpdate();
});
}
export function symbolicateLogLazy(log: LogBoxLog) {
log.symbolicate();
}
export function clear(): void {
if (logs.size > 0) {
logs.clear();
+8 -4
View File
@@ -63,12 +63,14 @@ class LogBoxLog {
: this.stack;
}
retrySymbolicate(callback: () => void): SymbolicationRequest {
LogBoxSymbolication.deleteStack(this.stack);
retrySymbolicate(callback?: () => void): SymbolicationRequest {
if (this.symbolicated.status !== 'COMPLETE') {
LogBoxSymbolication.deleteStack(this.stack);
}
return this.symbolicate(callback);
}
symbolicate(callback: () => void): SymbolicationRequest {
symbolicate(callback?: () => void): SymbolicationRequest {
let aborted = false;
if (this.symbolicated.status !== 'COMPLETE') {
@@ -81,7 +83,9 @@ class LogBoxLog {
this.symbolicated = {error: null, stack: null, status: 'PENDING'};
}
if (!aborted) {
callback();
if (callback != null) {
callback();
}
}
};
+39 -62
View File
@@ -15,6 +15,7 @@ import * as React from 'react';
import ScrollView from '../../Components/ScrollView/ScrollView';
import StyleSheet from '../../StyleSheet/StyleSheet';
import View from '../../Components/View/View';
import * as LogBoxData from '../Data/LogBoxData';
import LogBoxInspectorFooter from './LogBoxInspectorFooter';
import LogBoxInspectorMessageHeader from './LogBoxInspectorMessageHeader';
import LogBoxInspectorReactFrames from './LogBoxInspectorReactFrames';
@@ -24,7 +25,6 @@ import LogBoxInspectorHeader from './LogBoxInspectorHeader';
import * as LogBoxStyle from './LogBoxStyle';
import type LogBoxLog from '../Data/LogBoxLog';
import type {SymbolicationRequest} from '../Data/LogBoxLog';
type Props = $ReadOnly<{|
onDismiss: () => void,
@@ -34,75 +34,52 @@ type Props = $ReadOnly<{|
selectedIndex: number,
|}>;
class LogBoxInspector extends React.Component<Props> {
_symbolication: ?SymbolicationRequest;
function LogBoxInspector(props: Props): React.Node {
const {logs, selectedIndex} = props;
_handleDismiss = () => {
this.props.onDismiss();
};
render(): React.Node {
const {logs, selectedIndex} = this.props;
const log = logs[selectedIndex];
if (log == null) {
return null;
const log = logs[selectedIndex];
React.useEffect(() => {
// Symbolicate the visible log if it hasn't been already.
if (log != null && log.symbolicated.status !== 'COMPLETE') {
LogBoxData.symbolicateLogNow(log);
}
}, [log]);
return (
<View style={styles.root}>
<LogBoxInspectorHeader
onSelectIndex={this._handleSelectIndex}
selectedIndex={selectedIndex}
total={logs.length}
level={log.level}
/>
<LogBoxInspectorBody
log={log}
onRetry={this._handleRetrySymbolication}
/>
<LogBoxInspectorFooter
onDismiss={this._handleDismiss}
onMinimize={this.props.onMinimize}
/>
</View>
);
}
componentDidMount(): void {
this._handleSymbolication();
}
componentDidUpdate(prevProps: Props): void {
if (
prevProps.logs[prevProps.selectedIndex] !==
this.props.logs[this.props.selectedIndex]
) {
this._handleSymbolication();
React.useEffect(() => {
// Optimistically symbolicate the last and next logs.
if (logs.length > 1) {
const selected = selectedIndex;
const lastIndex = logs.length - 1;
const prevIndex = selected - 1 < 0 ? lastIndex : selected - 1;
const nextIndex = selected + 1 > lastIndex ? 0 : selected + 1;
LogBoxData.symbolicateLogLazy(logs[prevIndex]);
LogBoxData.symbolicateLogLazy(logs[nextIndex]);
}
}, [logs, selectedIndex]);
function _handleRetry() {
LogBoxData.retrySymbolicateLogNow(log);
}
_handleRetrySymbolication = () => {
this.forceUpdate(() => {
const log = this.props.logs[this.props.selectedIndex];
this._symbolication = log.retrySymbolicate(() => {
this.forceUpdate();
});
});
};
_handleSymbolication(): void {
const log = this.props.logs[this.props.selectedIndex];
if (log.symbolicated.status !== 'COMPLETE') {
this._symbolication = log.symbolicate(() => {
this.forceUpdate();
});
}
if (log == null) {
return null;
}
_handleSelectIndex = (selectedIndex: number): void => {
this.props.onChangeSelectedIndex(selectedIndex);
};
return (
<View style={styles.root}>
<LogBoxInspectorHeader
onSelectIndex={props.onChangeSelectedIndex}
selectedIndex={selectedIndex}
total={logs.length}
level={log.level}
/>
<LogBoxInspectorBody log={log} onRetry={_handleRetry} />
<LogBoxInspectorFooter
onDismiss={props.onDismiss}
onMinimize={props.onMinimize}
/>
</View>
);
}
function LogBoxInspectorBody(props) {
@@ -19,8 +19,6 @@ import LogBoxImageSource from './LogBoxImageSource';
import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import type {CompositeAnimation} from '../../Animated/src/AnimatedImplementation';
import type AnimatedInterpolation from '../../Animated/src/nodes/AnimatedInterpolation';
import type {PressEvent} from '../../Types/CoreEventTypes';
type Props = $ReadOnly<{|
@@ -28,76 +26,15 @@ type Props = $ReadOnly<{|
status: 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING',
|}>;
type State = {|
animation: ?CompositeAnimation,
rotate: ?AnimatedInterpolation,
|};
class LogBoxInspectorSourceMapStatus extends React.Component<Props, State> {
state: State = {
function LogBoxInspectorSourceMapStatus(props: Props): React.Node {
const [state, setState] = React.useState({
animation: null,
rotate: null,
};
});
render(): React.Node {
let image;
let color;
switch (this.props.status) {
case 'COMPLETE':
image = LogBoxImageSource.check;
color = LogBoxStyle.getTextColor(0.4);
break;
case 'FAILED':
image = LogBoxImageSource.alertTriangle;
color = LogBoxStyle.getErrorColor(1);
break;
case 'PENDING':
image = LogBoxImageSource.loader;
color = LogBoxStyle.getWarningColor(1);
break;
}
return image == null ? null : (
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
hitSlop={{bottom: 8, left: 8, right: 8, top: 8}}
onPress={this.props.onPress}
style={styles.root}>
<Animated.Image
source={{height: 16, uri: image, width: 16}}
style={[
styles.image,
{tintColor: color},
this.state.rotate == null
? null
: {transform: [{rotate: this.state.rotate}]},
]}
/>
<Text style={[styles.text, {color}]}>Source Map</Text>
</LogBoxButton>
);
}
componentDidMount(): void {
this._updateAnimation();
}
componentDidUpdate(): void {
this._updateAnimation();
}
componentWillUnmount(): void {
if (this.state.animation != null) {
this.state.animation.stop();
}
}
_updateAnimation(): void {
if (this.props.status === 'PENDING') {
if (this.state.animation == null) {
React.useEffect(() => {
if (props.status === 'PENDING') {
if (state.animation == null) {
const animated = new Animated.Value(0);
const animation = Animated.loop(
Animated.timing(animated, {
@@ -107,29 +44,69 @@ class LogBoxInspectorSourceMapStatus extends React.Component<Props, State> {
useNativeDriver: true,
}),
);
this.setState(
{
animation,
rotate: animated.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
() => {
animation.start();
},
);
setState({
animation,
rotate: animated.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
});
animation.start();
}
} else {
if (this.state.animation != null) {
this.state.animation.stop();
this.setState({
if (state.animation != null) {
state.animation.stop();
setState({
animation: null,
rotate: null,
});
}
}
return () => {
if (state.animation != null) {
state.animation.stop();
}
};
}, [props.status, state.animation]);
let image;
let color;
switch (props.status) {
case 'FAILED':
image = LogBoxImageSource.alertTriangle;
color = LogBoxStyle.getErrorColor(1);
break;
case 'PENDING':
image = LogBoxImageSource.loader;
color = LogBoxStyle.getWarningColor(1);
break;
}
if (props.status === 'COMPLETE') {
return null;
}
return image == null ? null : (
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
hitSlop={{bottom: 8, left: 8, right: 8, top: 8}}
onPress={props.onPress}
style={styles.root}>
<Animated.Image
source={{height: 16, uri: image, width: 16}}
style={[
styles.image,
{tintColor: color},
state.rotate == null ? null : {transform: [{rotate: state.rotate}]},
]}
/>
<Text style={[styles.text, {color}]}>Source Map</Text>
</LogBoxButton>
);
}
const styles = StyleSheet.create({
@@ -79,7 +79,7 @@ function StackFrameHeader(props) {
<View style={stackStyles.heading}>
<Text style={stackStyles.headingText}>Stack</Text>
<LogBoxInspectorSourceMapStatus
onPress={props.status === 'FAILED' ? props.onRetry() : null}
onPress={props.status !== 'COMPLETE' ? props.onRetry : null}
status={props.status}
/>
</View>
+29 -50
View File
@@ -20,63 +20,42 @@ import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import LogBoxLog from '../Data/LogBoxLog';
import LogBoxMessage from './LogBoxMessage';
import * as LogBoxData from '../Data/LogBoxData';
type Props = $ReadOnly<{|
log: LogBoxLog,
totalLogCount: number,
level: 'warn' | 'error',
onPressOpen: (index: number) => void,
onPressOpen: () => void,
onPressList: () => void,
onPressDismiss: () => void,
|}>;
class LogBoxLogNotification extends React.Component<Props> {
static GUTTER: number = StyleSheet.hairlineWidth;
static HEIGHT: number = 48;
function LogBoxLogNotification(props: Props): React.Node {
const {totalLogCount, level, log} = props;
shouldComponentUpdate(nextProps: Props): boolean {
const prevProps = this.props;
return (
prevProps.onPressOpen !== nextProps.onPressOpen ||
prevProps.onPressList !== nextProps.onPressList ||
prevProps.onPressDismiss !== nextProps.onPressDismiss ||
prevProps.log !== nextProps.log
);
}
// Eagerly symbolicate so the stack is available when pressing to inspect.
React.useEffect(() => {
LogBoxData.symbolicateLogLazy(log);
}, [log]);
_handlePressOpen = () => {
this.props.onPressOpen(0);
};
_handlePressList = () => {
this.props.onPressList();
};
_handlePressDismiss = () => {
this.props.onPressDismiss();
};
render(): React.Node {
const {totalLogCount, level, log} = this.props;
return (
<View style={toastStyles.container}>
<LogBoxButton
onPress={this._handlePressOpen}
style={toastStyles.press}
backgroundColor={{
default: LogBoxStyle.getBackgroundColor(1),
pressed: LogBoxStyle.getBackgroundColor(0.9),
}}>
<View style={toastStyles.content}>
<CountBadge count={totalLogCount} level={level} />
<Message message={log.message} />
<DismissButton onPress={this._handlePressDismiss} />
</View>
</LogBoxButton>
</View>
);
}
return (
<View style={toastStyles.container}>
<LogBoxButton
onPress={props.onPressOpen}
style={toastStyles.press}
backgroundColor={{
default: LogBoxStyle.getBackgroundColor(1),
pressed: LogBoxStyle.getBackgroundColor(0.9),
}}>
<View style={toastStyles.content}>
<CountBadge count={totalLogCount} level={level} />
<Message message={log.message} />
<DismissButton onPress={props.onPressDismiss} />
</View>
</LogBoxButton>
</View>
);
}
function CountBadge(props) {
@@ -208,19 +187,19 @@ const dismissStyles = StyleSheet.create({
const toastStyles = StyleSheet.create({
container: {
height: LogBoxLogNotification.HEIGHT,
height: 48,
position: 'relative',
width: '100%',
justifyContent: 'center',
marginTop: LogBoxLogNotification.GUTTER,
marginTop: 0.5,
backgroundColor: LogBoxStyle.getTextColor(1),
},
press: {
height: LogBoxLogNotification.HEIGHT,
height: 48,
position: 'relative',
width: '100%',
justifyContent: 'center',
marginTop: LogBoxLogNotification.GUTTER,
marginTop: 0.5,
paddingHorizontal: 12,
},
content: {
@@ -17,7 +17,23 @@ const LogBoxInspectorSourceMapStatus = require('../LogBoxInspectorSourceMapStatu
const render = require('../../../../jest/renderer');
describe('LogBoxInspectorSourceMapStatus', () => {
it('should render complete', () => {
it('should render for failed', () => {
const output = render.shallowRender(
<LogBoxInspectorSourceMapStatus onPress={() => {}} status="FAILED" />,
);
expect(output).toMatchSnapshot();
});
it('should render for pending', () => {
const output = render.shallowRender(
<LogBoxInspectorSourceMapStatus onPress={() => {}} status="PENDING" />,
);
expect(output).toMatchSnapshot();
});
it('should render null for complete', () => {
const output = render.shallowRender(
<LogBoxInspectorSourceMapStatus onPress={() => {}} status="COMPLETE" />,
);
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LogBoxInspectorSourceMapStatus should render complete 1`] = `
exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = `
<LogBoxButton
backgroundColor={
Object {
@@ -31,7 +31,7 @@ exports[`LogBoxInspectorSourceMapStatus should render complete 1`] = `
source={
Object {
"height": 16,
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAf0lEQVRYhe2UvQ2AIBQGL3EBR3AESkv3bxxFN8DmWUgwvkI+En1X0cBd+IMg+DuDyDMCs413kfMiX4EMbD3l8oCaPIU85B4mYLEF5XJscrYFPRGvb/sZ4IlocubJGdH0wj1FSG77XYT0qdUi5O+8jOjyyZQRUnkZ0UUeBMF3OQC/0VsyGlxligAAAABJRU5ErkJggg==",
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABVklEQVRYheWX4U3DMBBGH4gBMoJHyAgeoSNkAxjBG5QNOkJHCGzQDcoGZQP4gY3Oqe1cEscS4pNOqs9Jvqvv6ZrCf9fDhnutD4A3H810Br4mcW5l7hLmIdze5mZi+OJD5syeBYzC6CjyR5Ef9zI/CJMb0Im9zufC/qG2eQdchcGQuGYQ+9dJgZvl0B2xbJGrZW6IIevFXp9YVwcyB540syJfFcgSeJb0cVcDcg68XAFQCUhH+ShLBcBGIA158LQFqIB8zBRwEp9fgctcxQld/L2pZxZVAk/KkucjaDGQmoknrz35KEE2sABIRxm8tVIBaZgHb61UQOYmXk7aFgQVJ6QWPCnLAriYAVILnpTxD7yh/9EZiIEE4m+y29uMkGy1nQ6i9wYFRB5PwKdYP/v1msmnUe89gn695bG0iqjdXeMiRu9599csvGKZ0jlu0Ac/7d2rxX9Q37HW6QfX/ZguAAAAAElFTkSuQmCC",
"width": 16,
}
}
@@ -42,7 +42,7 @@ exports[`LogBoxInspectorSourceMapStatus should render complete 1`] = `
"tintColor": "rgba(255, 255, 255, 0.4)",
},
Object {
"tintColor": "rgba(255, 255, 255, 0.4)",
"tintColor": "rgba(243, 83, 105, 1)",
},
null,
]
@@ -57,7 +57,7 @@ exports[`LogBoxInspectorSourceMapStatus should render complete 1`] = `
"lineHeight": 16,
},
Object {
"color": "rgba(255, 255, 255, 0.4)",
"color": "rgba(243, 83, 105, 1)",
},
]
}
@@ -66,3 +66,72 @@ exports[`LogBoxInspectorSourceMapStatus should render complete 1`] = `
</Text>
</LogBoxButton>
`;
exports[`LogBoxInspectorSourceMapStatus should render for pending 1`] = `
<LogBoxButton
backgroundColor={
Object {
"default": "transparent",
"pressed": "rgba(51, 51, 51, 1)",
}
}
hitSlop={
Object {
"bottom": 8,
"left": 8,
"right": 8,
"top": 8,
}
}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"borderRadius": 12,
"flexDirection": "row",
"height": 24,
"paddingHorizontal": 8,
}
}
>
<AnimatedComponent
source={
Object {
"height": 16,
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABN0lEQVRYhe2WzU3EMBCFP34KyJEjJaQDXAIlJJ24BSow2wEdhHSwJSwd7JHbcmC0mOxMnDiWDIInWbHkN29exo4n8IvRAEFGU8OAA04yulyR60Jm7msbyIZloAMGwBfI4UWrWxM08LW/weC4iOMNTog4g0awKjBG827GxBwC3996NHizAifsSrTRmlsZm23CT9adktyXSq6ZUPdxgiXnZzW8CLcLuC3lvqA/gCt5NtjlPQL7TP0Wu1HtRRu4PO3T4TKTz2kG+AG9IN6CR/Su9iojBw69egfghWgL/pGCp+JFVPUqTjWjlsuqeAo1o6rt2C8QcNiV0UxoHPMieojmz0CfMKyhl1hN84xbI3gnz5Ftp7kH3iT5LsFdDUf6pzSJ6r2glIFDbuDNhqRH4I7Pvv4EvG/QqocP2Jh/xzzX/zUAAAAASUVORK5CYII=",
"width": 16,
}
}
style={
Array [
Object {
"marginEnd": 4,
"tintColor": "rgba(255, 255, 255, 0.4)",
},
Object {
"tintColor": "rgba(250, 186, 48, 1)",
},
null,
]
}
/>
<Text
style={
Array [
Object {
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 16,
},
Object {
"color": "rgba(250, 186, 48, 1)",
},
]
}
>
Source Map
</Text>
</LogBoxButton>
`;
exports[`LogBoxInspectorSourceMapStatus should render null for complete 1`] = `null`;