From ff9b3c6517d6427b8ca0cec2660555152be3d028 Mon Sep 17 00:00:00 2001 From: "Andrew Chen (Eng)" Date: Tue, 17 Apr 2018 17:17:12 -0700 Subject: [PATCH] Display JS component stack in native view exceptions Reviewed By: mdvacca Differential Revision: D7578033 fbshipit-source-id: 4dc393cddf8487db58cc3a9fefbff220983ba9da --- Libraries/Renderer/shims/ReactNativeTypes.js | 1 + Libraries/Utilities/JSDevSupportModule.js | 27 ++++--- .../com/facebook/react/DebugCorePackage.java | 9 --- .../com/facebook/react/ReactRootView.java | 13 ++-- .../devsupport/DevSupportManagerImpl.java | 71 ------------------- .../react/devsupport/JSDevSupport.java | 59 ++++++++++----- .../react/devsupport/ViewHierarchyUtil.java | 37 ++++++++++ 7 files changed, 102 insertions(+), 115 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/ViewHierarchyUtil.java diff --git a/Libraries/Renderer/shims/ReactNativeTypes.js b/Libraries/Renderer/shims/ReactNativeTypes.js index fdee974ae..1f7f5d001 100644 --- a/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/Libraries/Renderer/shims/ReactNativeTypes.js @@ -72,6 +72,7 @@ export type NativeMethodsMixinType = { type SecretInternalsType = { NativeMethodsMixin: NativeMethodsMixinType, ReactNativeComponentTree: any, + computeComponentStackForErrorReporting(tag: number): string, // TODO (bvaughn) Decide which additional types to expose here? // And how much information to fill in for the above types. }; diff --git a/Libraries/Utilities/JSDevSupportModule.js b/Libraries/Utilities/JSDevSupportModule.js index a33e91e88..dc45030bf 100644 --- a/Libraries/Utilities/JSDevSupportModule.js +++ b/Libraries/Utilities/JSDevSupportModule.js @@ -9,16 +9,25 @@ */ '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]]; +const JSDevSupport = require('NativeModules').JSDevSupport; +const ReactNative = require('ReactNative'); - var result = renderer.getInspectorDataForViewTag(tag); - var path = result.hierarchy.map( (item) => item.name).join(' -> '); - require('NativeModules').JSDevSupport.setResult(path, null); +const JSDevSupportModule = { + getJSHierarchy: function (tag: number) { + try { + const {computeComponentStackForErrorReporting} = + ReactNative.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + const componentStack = computeComponentStackForErrorReporting(tag); + if (!componentStack) { + JSDevSupport.onFailure( + JSDevSupport.ERROR_CODE_VIEW_NOT_FOUND, + 'Component stack doesn\'t exist for tag ' + tag); + } else { + JSDevSupport.onSuccess(componentStack); + } + } catch (e) { + JSDevSupport.onFailure(JSDevSupport.ERROR_CODE_EXCEPTION, e.message); + } }, }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java b/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java index 290082e54..1de05ef89 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java @@ -48,15 +48,6 @@ 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 c1d74a4ae..baa2a6e30 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -42,11 +42,11 @@ import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.JSTouchDispatcher; -import com.facebook.react.uimanager.common.MeasureSpecProvider; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.RootView; -import com.facebook.react.uimanager.common.SizeMonitoringFrameLayout; import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.common.MeasureSpecProvider; +import com.facebook.react.uimanager.common.SizeMonitoringFrameLayout; import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.systrace.Systrace; import javax.annotation.Nullable; @@ -572,18 +572,13 @@ public class ReactRootView extends SizeMonitoringFrameLayout } @Override - public void handleException(Throwable t) { + public void handleException(final 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); - + Exception e = new IllegalViewOperationException(t.getMessage(), this, t); mReactInstanceManager.getCurrentReactContext().handleException(e); } 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 c04211a98..ea25bf86c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -22,10 +22,7 @@ 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; import com.facebook.debug.holder.PrinterHolder; import com.facebook.debug.tags.ReactDebugOverlayTags; @@ -45,7 +42,6 @@ import com.facebook.react.common.ReactConstants; import com.facebook.react.common.ShakeDetector; import com.facebook.react.common.futures.SimpleSettableFuture; import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener; -import com.facebook.react.devsupport.InspectorPackagerConnection; import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; import com.facebook.react.devsupport.interfaces.DevOptionHandler; import com.facebook.react.devsupport.interfaces.DevSupportManager; @@ -55,24 +51,18 @@ import com.facebook.react.devsupport.interfaces.StackFrame; 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; - import javax.annotation.Nullable; - import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -274,7 +264,6 @@ public class DevSupportManagerImpl implements new DevLoadingViewController(applicationContext, reactInstanceManagerHelper); mExceptionLoggers.add(new JSExceptionLogger()); - mExceptionLoggers.add(new StackOverflowExceptionLogger()); } @Override @@ -322,66 +311,6 @@ public class DevSupportManagerImpl implements } } - 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; - } - } - @Override public void showNewJavaError(String message, Throwable e) { FLog.e(ReactConstants.TAG, "Exception in native call", e); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java index b5596d7f3..4e306ba10 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSDevSupport.java @@ -2,23 +2,31 @@ package com.facebook.react.devsupport; +import android.util.Pair; +import android.view.View; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.module.annotations.ReactModule; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; -@ReactModule(name = "JSDevSupport", needsEagerInit = true) +@ReactModule(name = JSDevSupport.MODULE_NAME) public class JSDevSupport extends ReactContextBaseJavaModule { static final String MODULE_NAME = "JSDevSupport"; + public static final int ERROR_CODE_EXCEPTION = 0; + public static final int ERROR_CODE_VIEW_NOT_FOUND = 1; + @Nullable private volatile DevSupportCallback mCurrentCallback = null; public interface JSDevSupportModule extends JavaScriptModule { - void getJSHierarchy(String reactTag); + void getJSHierarchy(int reactTag); } public JSDevSupport(ReactApplicationContext reactContext) { @@ -29,19 +37,25 @@ public class JSDevSupport extends ReactContextBaseJavaModule { void onSuccess(String data); - void onFailure(Exception error); + void onFailure(int errorCode, Exception error); } - public synchronized void getJSHierarchy(String reactTag, DevSupportCallback callback) { - if (mCurrentCallback != null) { - callback.onFailure(new RuntimeException("JS Hierarchy download already in progress.")); - return; - } + /** + * Notifies the callback with either the JS hierarchy of the deepest leaf from the given root view + * or with an error. + */ + public synchronized void computeDeepestJSHierarchy(View root, DevSupportCallback callback) { + final Pair deepestPairView = ViewHierarchyUtil.getDeepestLeaf(root); + View deepestView = deepestPairView.first; + Integer tagId = deepestView.getId(); + getJSHierarchy(tagId, callback); + } + public synchronized void getJSHierarchy(int reactTag, DevSupportCallback callback) { JSDevSupportModule jsDevSupportModule = getReactApplicationContext().getJSModule(JSDevSupportModule.class); if (jsDevSupportModule == null) { - callback.onFailure(new JSCHeapCapture.CaptureException(MODULE_NAME + + callback.onFailure(ERROR_CODE_EXCEPTION, new JSCHeapCapture.CaptureException(MODULE_NAME + " module not registered.")); return; } @@ -51,20 +65,31 @@ public class JSDevSupport extends ReactContextBaseJavaModule { @SuppressWarnings("unused") @ReactMethod - public synchronized void setResult(String data, String error) { + public synchronized void onSuccess(String data) { if (mCurrentCallback != null) { - if (error == null) { - mCurrentCallback.onSuccess(data); - } else { - mCurrentCallback.onFailure(new RuntimeException(error)); - } + mCurrentCallback.onSuccess(data); } - mCurrentCallback = null; + } + + @SuppressWarnings("unused") + @ReactMethod + public synchronized void onFailure(int errorCode, String error) { + if (mCurrentCallback != null) { + mCurrentCallback.onFailure(errorCode, new RuntimeException(error)); + } + } + + @Override + public Map getConstants() { + HashMap constants = new HashMap<>(); + constants.put("ERROR_CODE_EXCEPTION", ERROR_CODE_EXCEPTION); + constants.put("ERROR_CODE_VIEW_NOT_FOUND", ERROR_CODE_VIEW_NOT_FOUND); + return constants; } @Override public String getName() { - return "JSDevSupport"; + return MODULE_NAME; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ViewHierarchyUtil.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ViewHierarchyUtil.java new file mode 100644 index 000000000..1aee0099e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ViewHierarchyUtil.java @@ -0,0 +1,37 @@ +package com.facebook.react.devsupport; + +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; +import java.util.LinkedList; +import java.util.Queue; + +/** + * Helper for computing information about the view hierarchy + */ +public class ViewHierarchyUtil { + + /** + * Returns the view instance and depth of the deepest leaf view from the given root view. + */ + public static Pair getDeepestLeaf(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; + } +}