mirror of
https://github.com/status-im/react-native.git
synced 2025-02-04 13:44:04 +00:00
Adding JS hierarchy information when a StackOverflowException is thrown in Dev mode
Reviewed By: achen1 Differential Revision: D6716309 fbshipit-source-id: 23458cd126d13fec3aa9c09420f7cdd230ec8dd0
This commit is contained in:
parent
e8893a021f
commit
4d3519cc6a
@ -203,6 +203,7 @@ BatchedBridge.registerLazyCallableModule('RCTLog', () => require('RCTLog'));
|
||||
BatchedBridge.registerLazyCallableModule('RCTDeviceEventEmitter', () => require('RCTDeviceEventEmitter'));
|
||||
BatchedBridge.registerLazyCallableModule('RCTNativeAppEventEmitter', () => require('RCTNativeAppEventEmitter'));
|
||||
BatchedBridge.registerLazyCallableModule('PerformanceLogger', () => require('PerformanceLogger'));
|
||||
BatchedBridge.registerLazyCallableModule('JSDevSupportModule', () => require('JSDevSupportModule'));
|
||||
|
||||
global.fetchSegment = function(
|
||||
segmentId: number,
|
||||
|
28
Libraries/Utilities/JSDevSupportModule.js
Normal file
28
Libraries/Utilities/JSDevSupportModule.js
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule JSDevSupportModule
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var JSDevSupportModule = {
|
||||
getJSHierarchy: function (tag: string) {
|
||||
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
const renderers = hook._renderers;
|
||||
const keys = Object.keys(renderers);
|
||||
const renderer = renderers[keys[0]];
|
||||
|
||||
var result = renderer.getInspectorDataForViewTag(tag);
|
||||
var path = result.hierarchy.map( (item) => item.name).join(' -> ');
|
||||
console.error('StackOverflowException rendering JSComponent: ' + path);
|
||||
require('NativeModules').JSDevSupport.setResult(path, null);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = JSDevSupportModule;
|
@ -12,8 +12,9 @@ package com.facebook.react;
|
||||
import com.facebook.react.bridge.ModuleSpec;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.devsupport.JSCHeapCapture;
|
||||
import com.facebook.react.devsupport.JSCSamplingProfiler;
|
||||
import com.facebook.react.devsupport.JSDevSupport;
|
||||
import com.facebook.react.devsupport.JSCHeapCapture;
|
||||
import com.facebook.react.module.annotations.ReactModuleList;
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||
import java.util.ArrayList;
|
||||
@ -29,6 +30,7 @@ import javax.inject.Provider;
|
||||
nativeModules = {
|
||||
JSCHeapCapture.class,
|
||||
JSCSamplingProfiler.class,
|
||||
JSDevSupport.class,
|
||||
}
|
||||
)
|
||||
/* package */ class DebugCorePackage extends LazyReactPackage {
|
||||
@ -48,6 +50,15 @@ import javax.inject.Provider;
|
||||
return new JSCHeapCapture(reactContext);
|
||||
}
|
||||
}));
|
||||
moduleSpecList.add(
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
JSDevSupport.class,
|
||||
new Provider<NativeModule>() {
|
||||
@Override
|
||||
public NativeModule get() {
|
||||
return new JSDevSupport(reactContext);
|
||||
}
|
||||
}));
|
||||
moduleSpecList.add(
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
JSCSamplingProfiler.class,
|
||||
|
@ -210,7 +210,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout
|
||||
} catch (StackOverflowError e) {
|
||||
// Adding special exception management for StackOverflowError for logging purposes.
|
||||
// This will be removed in the future.
|
||||
handleException(new IllegalViewOperationException("StackOverflowError", e));
|
||||
handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -510,12 +510,19 @@ public class ReactRootView extends SizeMonitoringFrameLayout
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Exception e) {
|
||||
if (mReactInstanceManager != null && mReactInstanceManager.getCurrentReactContext() != null) {
|
||||
mReactInstanceManager.getCurrentReactContext().handleException(e);
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
public void handleException(Throwable t) {
|
||||
if (mReactInstanceManager == null
|
||||
|| mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
|
||||
// Adding special exception management for StackOverflowError for logging purposes.
|
||||
// This will be removed in the future.
|
||||
Exception e = (t instanceof StackOverflowError) ?
|
||||
new IllegalViewOperationException("StackOverflowException", this, t) :
|
||||
t instanceof Exception ? (Exception) t : new RuntimeException(t);
|
||||
|
||||
mReactInstanceManager.getCurrentReactContext().handleException(e);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -14,6 +14,7 @@ android_library(
|
||||
react_native_dep("third-party/java/okhttp:okhttp3"),
|
||||
react_native_dep("third-party/java/okio:okio"),
|
||||
react_native_target("java/com/facebook/debug/holder:holder"),
|
||||
react_native_target("java/com/facebook/react/uimanager:uimanager"),
|
||||
react_native_target("java/com/facebook/debug/tags:tags"),
|
||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||
react_native_target("java/com/facebook/react/common:common"),
|
||||
|
@ -24,6 +24,8 @@ import android.hardware.SensorManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
@ -55,14 +57,17 @@ import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
|
||||
import com.facebook.react.packagerconnection.RequestHandler;
|
||||
import com.facebook.react.packagerconnection.Responder;
|
||||
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@ -120,6 +125,8 @@ public class DevSupportManagerImpl implements
|
||||
public static final String EMOJI_HUNDRED_POINTS_SYMBOL = " \uD83D\uDCAF";
|
||||
public static final String EMOJI_FACE_WITH_NO_GOOD_GESTURE = " \uD83D\uDE45";
|
||||
|
||||
private final List<ExceptionLogger> mExceptionLoggers = new ArrayList<>();
|
||||
|
||||
private final Context mApplicationContext;
|
||||
private final ShakeDetector mShakeDetector;
|
||||
private final BroadcastReceiver mReloadAppBroadcastReceiver;
|
||||
@ -252,11 +259,32 @@ public class DevSupportManagerImpl implements
|
||||
mRedBoxHandler = redBoxHandler;
|
||||
mDevLoadingViewController =
|
||||
new DevLoadingViewController(applicationContext, reactInstanceManagerHelper);
|
||||
|
||||
mExceptionLoggers.add(new JSExceptionLogger());
|
||||
mExceptionLoggers.add(new StackOverflowExceptionLogger());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Exception e) {
|
||||
if (mIsDevSupportEnabled) {
|
||||
|
||||
for (ExceptionLogger logger : mExceptionLoggers) {
|
||||
logger.log(e);
|
||||
}
|
||||
|
||||
} else {
|
||||
mDefaultNativeModuleCallExceptionHandler.handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private interface ExceptionLogger {
|
||||
void log(Exception ex);
|
||||
}
|
||||
|
||||
private class JSExceptionLogger implements ExceptionLogger {
|
||||
|
||||
@Override
|
||||
public void log(Exception e) {
|
||||
StringBuilder message = new StringBuilder(e.getMessage());
|
||||
Throwable cause = e.getCause();
|
||||
while (cause != null) {
|
||||
@ -270,12 +298,74 @@ public class DevSupportManagerImpl implements
|
||||
message.append("\n\n").append(stack);
|
||||
|
||||
// TODO #11638796: convert the stack into something useful
|
||||
showNewError(message.toString(), new StackFrame[] {}, JSEXCEPTION_ERROR_COOKIE, ErrorType.JS);
|
||||
showNewError(
|
||||
message.toString(),
|
||||
new StackFrame[]{},
|
||||
JSEXCEPTION_ERROR_COOKIE,
|
||||
ErrorType.JS);
|
||||
} else {
|
||||
showNewJavaError(message.toString(), e);
|
||||
}
|
||||
} else {
|
||||
mDefaultNativeModuleCallExceptionHandler.handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private class StackOverflowExceptionLogger implements ExceptionLogger {
|
||||
|
||||
@Override
|
||||
public void log(Exception e) {
|
||||
if (e instanceof IllegalViewOperationException
|
||||
&& e.getCause() instanceof StackOverflowError) {
|
||||
IllegalViewOperationException ivoe = (IllegalViewOperationException) e;
|
||||
View view = ivoe.getView();
|
||||
if (view != null)
|
||||
logDeepestJSHierarchy(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void logDeepestJSHierarchy(View view) {
|
||||
if (mCurrentContext == null || view == null) return;
|
||||
|
||||
final Pair<View, Integer> deepestPairView = getDeepestNativeView(view);
|
||||
|
||||
View deepestView = deepestPairView.first;
|
||||
Integer tagId = deepestView.getId();
|
||||
final int depth = deepestPairView.second;
|
||||
JSDevSupport JSDevSupport = mCurrentContext.getNativeModule(JSDevSupport.class);
|
||||
JSDevSupport.getJSHierarchy(tagId.toString(), new JSDevSupport.DevSupportCallback() {
|
||||
@Override
|
||||
public void onSuccess(String hierarchy) {
|
||||
FLog.e(ReactConstants.TAG,
|
||||
"StackOverflowError when rendering JS Hierarchy (depth of native hierarchy = " +
|
||||
depth + "): \n" + hierarchy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception ex) {
|
||||
FLog.e(ReactConstants.TAG, ex,
|
||||
"Error retrieving JS Hierarchy (depth of native hierarchy = " + depth + ").");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Pair<View, Integer> getDeepestNativeView(View root) {
|
||||
Queue<Pair<View, Integer>> queue = new LinkedList<>();
|
||||
Pair<View, Integer> maxPair = new Pair<>(root, 1);
|
||||
|
||||
queue.add(maxPair);
|
||||
while (!queue.isEmpty()) {
|
||||
Pair<View, Integer> current = queue.poll();
|
||||
if (current.second > maxPair.second) {
|
||||
maxPair = current;
|
||||
}
|
||||
if (current.first instanceof ViewGroup) {
|
||||
ViewGroup viewGroup = (ViewGroup) current.first;
|
||||
Integer depth = current.second + 1;
|
||||
for (int i = 0 ; i < viewGroup.getChildCount() ; i++) {
|
||||
queue.add(new Pair<>(viewGroup.getChildAt(i), depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxPair;
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,7 +476,7 @@ public class DevSupportManagerImpl implements
|
||||
Activity context = mReactInstanceManagerHelper.getCurrentActivity();
|
||||
if (context == null || context.isFinishing()) {
|
||||
FLog.e(ReactConstants.TAG, "Unable to launch redbox because react activity " +
|
||||
"is not available, here is the error that redbox would've displayed: " + message);
|
||||
"is not available, here is the error that redbox would've displayed: " + message);
|
||||
return;
|
||||
}
|
||||
mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler);
|
||||
|
@ -0,0 +1,70 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ReactModule(name = "JSDevSupport", needsEagerInit = true)
|
||||
public class JSDevSupport extends ReactContextBaseJavaModule {
|
||||
|
||||
static final String MODULE_NAME = "JSDevSupport";
|
||||
|
||||
@Nullable
|
||||
private volatile DevSupportCallback mCurrentCallback = null;
|
||||
|
||||
public interface JSDevSupportModule extends JavaScriptModule {
|
||||
void getJSHierarchy(String reactTag);
|
||||
}
|
||||
|
||||
public JSDevSupport(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
public interface DevSupportCallback {
|
||||
|
||||
void onSuccess(String data);
|
||||
|
||||
void onFailure(Exception error);
|
||||
}
|
||||
|
||||
public synchronized void getJSHierarchy(String reactTag, DevSupportCallback callback) {
|
||||
if (mCurrentCallback != null) {
|
||||
callback.onFailure(new RuntimeException("JS Hierarchy download already in progress."));
|
||||
return;
|
||||
}
|
||||
|
||||
JSDevSupportModule
|
||||
jsDevSupportModule = getReactApplicationContext().getJSModule(JSDevSupportModule.class);
|
||||
if (jsDevSupportModule == null) {
|
||||
callback.onFailure(new JSCHeapCapture.CaptureException(MODULE_NAME +
|
||||
" module not registered."));
|
||||
return;
|
||||
}
|
||||
mCurrentCallback = callback;
|
||||
jsDevSupportModule.getJSHierarchy(reactTag);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ReactMethod
|
||||
public synchronized void setResult(String data, String error) {
|
||||
if (mCurrentCallback != null) {
|
||||
if (error == null) {
|
||||
mCurrentCallback.onSuccess(data);
|
||||
} else {
|
||||
mCurrentCallback.onFailure(new RuntimeException(error));
|
||||
}
|
||||
}
|
||||
mCurrentCallback = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "JSDevSupport";
|
||||
}
|
||||
|
||||
}
|
@ -9,6 +9,8 @@
|
||||
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import com.facebook.react.bridge.JSApplicationCausedNativeException;
|
||||
|
||||
/**
|
||||
@ -16,11 +18,19 @@ import com.facebook.react.bridge.JSApplicationCausedNativeException;
|
||||
*/
|
||||
public class IllegalViewOperationException extends JSApplicationCausedNativeException {
|
||||
|
||||
@Nullable private View mView;
|
||||
|
||||
public IllegalViewOperationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public IllegalViewOperationException(String msg, Throwable cause) {
|
||||
public IllegalViewOperationException(String msg, @Nullable View view, Throwable cause) {
|
||||
super(msg, cause);
|
||||
mView = view;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public View getView() {
|
||||
return mView;
|
||||
}
|
||||
}
|
||||
|
@ -22,5 +22,5 @@ public interface RootView {
|
||||
*/
|
||||
void onChildStartedNativeGesture(MotionEvent androidEvent);
|
||||
|
||||
void handleException(Exception e);
|
||||
void handleException(Throwable t);
|
||||
}
|
||||
|
@ -9,10 +9,6 @@
|
||||
|
||||
package com.facebook.react.views.modal;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
@ -24,7 +20,6 @@ import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.GuardedRunnable;
|
||||
@ -36,6 +31,8 @@ import com.facebook.react.uimanager.RootView;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.views.view.ReactViewGroup;
|
||||
import java.util.ArrayList;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* ReactModalHostView is a view that sits in the view hierarchy representing a Modal view.
|
||||
@ -328,8 +325,8 @@ public class ReactModalHostView extends ViewGroup implements LifecycleEventListe
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Exception e) {
|
||||
getReactContext().handleException(e);
|
||||
public void handleException(Throwable t) {
|
||||
getReactContext().handleException(new RuntimeException(t));
|
||||
}
|
||||
|
||||
private ReactContext getReactContext() {
|
||||
|
@ -667,12 +667,10 @@ public class ReactViewGroup extends ViewGroup implements
|
||||
// Adding special exception management for StackOverflowError for logging purposes.
|
||||
// This will be removed in the future.
|
||||
RootView rootView = RootViewUtil.getRootView(ReactViewGroup.this);
|
||||
IllegalViewOperationException wrappedException =
|
||||
new IllegalViewOperationException("StackOverflowError", e);
|
||||
if (rootView != null) {
|
||||
rootView.handleException(wrappedException);
|
||||
rootView.handleException(e);
|
||||
} else {
|
||||
throw wrappedException;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user