diff --git a/Libraries/Core/InitializeCore.js b/Libraries/Core/InitializeCore.js index f0fc67235..f1e2d51b0 100644 --- a/Libraries/Core/InitializeCore.js +++ b/Libraries/Core/InitializeCore.js @@ -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, diff --git a/Libraries/Utilities/JSDevSupportModule.js b/Libraries/Utilities/JSDevSupportModule.js new file mode 100644 index 000000000..3aa91255f --- /dev/null +++ b/Libraries/Utilities/JSDevSupportModule.js @@ -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; diff --git a/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java b/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java index f847c5cd8..3a83bd330 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java @@ -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() { + @Override + public NativeModule get() { + return new JSDevSupport(reactContext); + } + })); moduleSpecList.add( ModuleSpec.nativeModuleSpec( JSCSamplingProfiler.class, diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index d96dfc93e..c67cab536 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -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 diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK index e64e27de3..168a8d1eb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK @@ -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"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 76c03be47..2610dba6d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -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 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 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 getDeepestNativeView(View root) { + Queue> queue = new LinkedList<>(); + Pair maxPair = new Pair<>(root, 1); + + queue.add(maxPair); + while (!queue.isEmpty()) { + Pair 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); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java new file mode 100644 index 000000000..b5596d7f3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java @@ -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"; + } + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java index d23735264..3f5899e55 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java @@ -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; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java index 1e57ae1ff..6863740ea 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java @@ -22,5 +22,5 @@ public interface RootView { */ void onChildStartedNativeGesture(MotionEvent androidEvent); - void handleException(Exception e); + void handleException(Throwable t); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java index afd3d3f3f..b191afd02 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.java @@ -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() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index c31eb66bd..4ab4e9119 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -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; } } }