Skip to content

Commit

Permalink
Adding JS hierarchy information when a StackOverflowException is thro…
Browse files Browse the repository at this point in the history
…wn in Dev mode

Reviewed By: achen1

Differential Revision: D6716309

fbshipit-source-id: 23458cd126d13fec3aa9c09420f7cdd230ec8dd0
  • Loading branch information
mdvacca authored and facebook-github-bot committed Jan 19, 2018
1 parent e8893a0 commit 4d3519c
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 24 deletions.
1 change: 1 addition & 0 deletions Libraries/Core/InitializeCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions Libraries/Utilities/JSDevSupportModule.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
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;
Expand All @@ -29,6 +30,7 @@
nativeModules = {
JSCHeapCapture.class,
JSCSamplingProfiler.class,
JSDevSupport.class,
}
)
/* package */ class DebugCorePackage extends LazyReactPackage {
Expand All @@ -48,6 +50,15 @@ public NativeModule get() {
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,
Expand Down
19 changes: 13 additions & 6 deletions ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ protected void dispatchDraw(Canvas canvas) {
} 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);
}
}

Expand Down Expand Up @@ -510,12 +510,19 @@ public void setRootViewTag(int rootViewTag) {
}

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
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;
Expand Down Expand Up @@ -55,14 +57,17 @@
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;
Expand Down Expand Up @@ -120,6 +125,8 @@ private enum ErrorType {
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;
Expand Down Expand Up @@ -252,11 +259,32 @@ public void onReceive(Context context, Intent intent) {
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) {
Expand All @@ -270,12 +298,74 @@ public void handleException(Exception e) {
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;
}
}

Expand Down Expand Up @@ -386,7 +476,7 @@ public void run() {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@

package com.facebook.react.uimanager;

import android.support.annotation.Nullable;
import android.view.View;
import com.facebook.react.bridge.JSApplicationCausedNativeException;

/**
* An exception caused by JS requesting the UI manager to perform an illegal view operation.
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ public interface RootView {
*/
void onChildStartedNativeGesture(MotionEvent androidEvent);

void handleException(Exception e);
void handleException(Throwable t);
}
Loading

0 comments on commit 4d3519c

Please sign in to comment.