Skip to content

Commit 4d3519c

Browse files
mdvaccafacebook-github-bot
authored andcommitted
Adding JS hierarchy information when a StackOverflowException is thrown in Dev mode
Reviewed By: achen1 Differential Revision: D6716309 fbshipit-source-id: 23458cd126d13fec3aa9c09420f7cdd230ec8dd0
1 parent e8893a0 commit 4d3519c

File tree

11 files changed

+237
-24
lines changed

11 files changed

+237
-24
lines changed

Libraries/Core/InitializeCore.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ BatchedBridge.registerLazyCallableModule('RCTLog', () => require('RCTLog'));
203203
BatchedBridge.registerLazyCallableModule('RCTDeviceEventEmitter', () => require('RCTDeviceEventEmitter'));
204204
BatchedBridge.registerLazyCallableModule('RCTNativeAppEventEmitter', () => require('RCTNativeAppEventEmitter'));
205205
BatchedBridge.registerLazyCallableModule('PerformanceLogger', () => require('PerformanceLogger'));
206+
BatchedBridge.registerLazyCallableModule('JSDevSupportModule', () => require('JSDevSupportModule'));
206207

207208
global.fetchSegment = function(
208209
segmentId: number,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule JSDevSupportModule
10+
* @flow
11+
*/
12+
'use strict';
13+
14+
var JSDevSupportModule = {
15+
getJSHierarchy: function (tag: string) {
16+
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
17+
const renderers = hook._renderers;
18+
const keys = Object.keys(renderers);
19+
const renderer = renderers[keys[0]];
20+
21+
var result = renderer.getInspectorDataForViewTag(tag);
22+
var path = result.hierarchy.map( (item) => item.name).join(' -> ');
23+
console.error('StackOverflowException rendering JSComponent: ' + path);
24+
require('NativeModules').JSDevSupport.setResult(path, null);
25+
},
26+
};
27+
28+
module.exports = JSDevSupportModule;

ReactAndroid/src/main/java/com/facebook/react/DebugCorePackage.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
import com.facebook.react.bridge.ModuleSpec;
1313
import com.facebook.react.bridge.NativeModule;
1414
import com.facebook.react.bridge.ReactApplicationContext;
15-
import com.facebook.react.devsupport.JSCHeapCapture;
1615
import com.facebook.react.devsupport.JSCSamplingProfiler;
16+
import com.facebook.react.devsupport.JSDevSupport;
17+
import com.facebook.react.devsupport.JSCHeapCapture;
1718
import com.facebook.react.module.annotations.ReactModuleList;
1819
import com.facebook.react.module.model.ReactModuleInfoProvider;
1920
import java.util.ArrayList;
@@ -29,6 +30,7 @@
2930
nativeModules = {
3031
JSCHeapCapture.class,
3132
JSCSamplingProfiler.class,
33+
JSDevSupport.class,
3234
}
3335
)
3436
/* package */ class DebugCorePackage extends LazyReactPackage {
@@ -48,6 +50,15 @@ public NativeModule get() {
4850
return new JSCHeapCapture(reactContext);
4951
}
5052
}));
53+
moduleSpecList.add(
54+
ModuleSpec.nativeModuleSpec(
55+
JSDevSupport.class,
56+
new Provider<NativeModule>() {
57+
@Override
58+
public NativeModule get() {
59+
return new JSDevSupport(reactContext);
60+
}
61+
}));
5162
moduleSpecList.add(
5263
ModuleSpec.nativeModuleSpec(
5364
JSCSamplingProfiler.class,

ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ protected void dispatchDraw(Canvas canvas) {
210210
} catch (StackOverflowError e) {
211211
// Adding special exception management for StackOverflowError for logging purposes.
212212
// This will be removed in the future.
213-
handleException(new IllegalViewOperationException("StackOverflowError", e));
213+
handleException(e);
214214
}
215215
}
216216

@@ -510,12 +510,19 @@ public void setRootViewTag(int rootViewTag) {
510510
}
511511

512512
@Override
513-
public void handleException(Exception e) {
514-
if (mReactInstanceManager != null && mReactInstanceManager.getCurrentReactContext() != null) {
515-
mReactInstanceManager.getCurrentReactContext().handleException(e);
516-
} else {
517-
throw new RuntimeException(e);
513+
public void handleException(Throwable t) {
514+
if (mReactInstanceManager == null
515+
|| mReactInstanceManager.getCurrentReactContext() == null) {
516+
throw new RuntimeException(t);
518517
}
518+
519+
// Adding special exception management for StackOverflowError for logging purposes.
520+
// This will be removed in the future.
521+
Exception e = (t instanceof StackOverflowError) ?
522+
new IllegalViewOperationException("StackOverflowException", this, t) :
523+
t instanceof Exception ? (Exception) t : new RuntimeException(t);
524+
525+
mReactInstanceManager.getCurrentReactContext().handleException(e);
519526
}
520527

521528
@Nullable

ReactAndroid/src/main/java/com/facebook/react/devsupport/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ android_library(
1414
react_native_dep("third-party/java/okhttp:okhttp3"),
1515
react_native_dep("third-party/java/okio:okio"),
1616
react_native_target("java/com/facebook/debug/holder:holder"),
17+
react_native_target("java/com/facebook/react/uimanager:uimanager"),
1718
react_native_target("java/com/facebook/debug/tags:tags"),
1819
react_native_target("java/com/facebook/react/bridge:bridge"),
1920
react_native_target("java/com/facebook/react/common:common"),

ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import android.net.Uri;
2525
import android.os.AsyncTask;
2626
import android.util.Pair;
27+
import android.view.View;
28+
import android.view.ViewGroup;
2729
import android.widget.Toast;
2830

2931
import com.facebook.common.logging.FLog;
@@ -55,14 +57,17 @@
5557
import com.facebook.react.packagerconnection.RequestHandler;
5658
import com.facebook.react.packagerconnection.Responder;
5759

60+
import com.facebook.react.uimanager.IllegalViewOperationException;
5861
import java.io.File;
5962
import java.io.IOException;
6063
import java.net.MalformedURLException;
6164
import java.net.URL;
6265
import java.util.ArrayList;
6366
import java.util.LinkedHashMap;
67+
import java.util.LinkedList;
6468
import java.util.List;
6569
import java.util.Locale;
70+
import java.util.Queue;
6671
import java.util.concurrent.ExecutionException;
6772
import java.util.concurrent.TimeUnit;
6873
import java.util.concurrent.TimeoutException;
@@ -120,6 +125,8 @@ private enum ErrorType {
120125
public static final String EMOJI_HUNDRED_POINTS_SYMBOL = " \uD83D\uDCAF";
121126
public static final String EMOJI_FACE_WITH_NO_GOOD_GESTURE = " \uD83D\uDE45";
122127

128+
private final List<ExceptionLogger> mExceptionLoggers = new ArrayList<>();
129+
123130
private final Context mApplicationContext;
124131
private final ShakeDetector mShakeDetector;
125132
private final BroadcastReceiver mReloadAppBroadcastReceiver;
@@ -252,11 +259,32 @@ public void onReceive(Context context, Intent intent) {
252259
mRedBoxHandler = redBoxHandler;
253260
mDevLoadingViewController =
254261
new DevLoadingViewController(applicationContext, reactInstanceManagerHelper);
262+
263+
mExceptionLoggers.add(new JSExceptionLogger());
264+
mExceptionLoggers.add(new StackOverflowExceptionLogger());
255265
}
256266

257267
@Override
258268
public void handleException(Exception e) {
259269
if (mIsDevSupportEnabled) {
270+
271+
for (ExceptionLogger logger : mExceptionLoggers) {
272+
logger.log(e);
273+
}
274+
275+
} else {
276+
mDefaultNativeModuleCallExceptionHandler.handleException(e);
277+
}
278+
}
279+
280+
private interface ExceptionLogger {
281+
void log(Exception ex);
282+
}
283+
284+
private class JSExceptionLogger implements ExceptionLogger {
285+
286+
@Override
287+
public void log(Exception e) {
260288
StringBuilder message = new StringBuilder(e.getMessage());
261289
Throwable cause = e.getCause();
262290
while (cause != null) {
@@ -270,12 +298,74 @@ public void handleException(Exception e) {
270298
message.append("\n\n").append(stack);
271299

272300
// TODO #11638796: convert the stack into something useful
273-
showNewError(message.toString(), new StackFrame[] {}, JSEXCEPTION_ERROR_COOKIE, ErrorType.JS);
301+
showNewError(
302+
message.toString(),
303+
new StackFrame[]{},
304+
JSEXCEPTION_ERROR_COOKIE,
305+
ErrorType.JS);
274306
} else {
275307
showNewJavaError(message.toString(), e);
276308
}
277-
} else {
278-
mDefaultNativeModuleCallExceptionHandler.handleException(e);
309+
}
310+
}
311+
312+
private class StackOverflowExceptionLogger implements ExceptionLogger {
313+
314+
@Override
315+
public void log(Exception e) {
316+
if (e instanceof IllegalViewOperationException
317+
&& e.getCause() instanceof StackOverflowError) {
318+
IllegalViewOperationException ivoe = (IllegalViewOperationException) e;
319+
View view = ivoe.getView();
320+
if (view != null)
321+
logDeepestJSHierarchy(view);
322+
}
323+
}
324+
325+
private void logDeepestJSHierarchy(View view) {
326+
if (mCurrentContext == null || view == null) return;
327+
328+
final Pair<View, Integer> deepestPairView = getDeepestNativeView(view);
329+
330+
View deepestView = deepestPairView.first;
331+
Integer tagId = deepestView.getId();
332+
final int depth = deepestPairView.second;
333+
JSDevSupport JSDevSupport = mCurrentContext.getNativeModule(JSDevSupport.class);
334+
JSDevSupport.getJSHierarchy(tagId.toString(), new JSDevSupport.DevSupportCallback() {
335+
@Override
336+
public void onSuccess(String hierarchy) {
337+
FLog.e(ReactConstants.TAG,
338+
"StackOverflowError when rendering JS Hierarchy (depth of native hierarchy = " +
339+
depth + "): \n" + hierarchy);
340+
}
341+
342+
@Override
343+
public void onFailure(Exception ex) {
344+
FLog.e(ReactConstants.TAG, ex,
345+
"Error retrieving JS Hierarchy (depth of native hierarchy = " + depth + ").");
346+
}
347+
});
348+
}
349+
350+
private Pair<View, Integer> getDeepestNativeView(View root) {
351+
Queue<Pair<View, Integer>> queue = new LinkedList<>();
352+
Pair<View, Integer> maxPair = new Pair<>(root, 1);
353+
354+
queue.add(maxPair);
355+
while (!queue.isEmpty()) {
356+
Pair<View, Integer> current = queue.poll();
357+
if (current.second > maxPair.second) {
358+
maxPair = current;
359+
}
360+
if (current.first instanceof ViewGroup) {
361+
ViewGroup viewGroup = (ViewGroup) current.first;
362+
Integer depth = current.second + 1;
363+
for (int i = 0 ; i < viewGroup.getChildCount() ; i++) {
364+
queue.add(new Pair<>(viewGroup.getChildAt(i), depth));
365+
}
366+
}
367+
}
368+
return maxPair;
279369
}
280370
}
281371

@@ -386,7 +476,7 @@ public void run() {
386476
Activity context = mReactInstanceManagerHelper.getCurrentActivity();
387477
if (context == null || context.isFinishing()) {
388478
FLog.e(ReactConstants.TAG, "Unable to launch redbox because react activity " +
389-
"is not available, here is the error that redbox would've displayed: " + message);
479+
"is not available, here is the error that redbox would've displayed: " + message);
390480
return;
391481
}
392482
mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2004-present Facebook. All Rights Reserved.
2+
3+
package com.facebook.react.devsupport;
4+
5+
import com.facebook.react.bridge.JavaScriptModule;
6+
import com.facebook.react.bridge.ReactApplicationContext;
7+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
8+
import com.facebook.react.bridge.ReactMethod;
9+
import com.facebook.react.module.annotations.ReactModule;
10+
import javax.annotation.Nullable;
11+
12+
@ReactModule(name = "JSDevSupport", needsEagerInit = true)
13+
public class JSDevSupport extends ReactContextBaseJavaModule {
14+
15+
static final String MODULE_NAME = "JSDevSupport";
16+
17+
@Nullable
18+
private volatile DevSupportCallback mCurrentCallback = null;
19+
20+
public interface JSDevSupportModule extends JavaScriptModule {
21+
void getJSHierarchy(String reactTag);
22+
}
23+
24+
public JSDevSupport(ReactApplicationContext reactContext) {
25+
super(reactContext);
26+
}
27+
28+
public interface DevSupportCallback {
29+
30+
void onSuccess(String data);
31+
32+
void onFailure(Exception error);
33+
}
34+
35+
public synchronized void getJSHierarchy(String reactTag, DevSupportCallback callback) {
36+
if (mCurrentCallback != null) {
37+
callback.onFailure(new RuntimeException("JS Hierarchy download already in progress."));
38+
return;
39+
}
40+
41+
JSDevSupportModule
42+
jsDevSupportModule = getReactApplicationContext().getJSModule(JSDevSupportModule.class);
43+
if (jsDevSupportModule == null) {
44+
callback.onFailure(new JSCHeapCapture.CaptureException(MODULE_NAME +
45+
" module not registered."));
46+
return;
47+
}
48+
mCurrentCallback = callback;
49+
jsDevSupportModule.getJSHierarchy(reactTag);
50+
}
51+
52+
@SuppressWarnings("unused")
53+
@ReactMethod
54+
public synchronized void setResult(String data, String error) {
55+
if (mCurrentCallback != null) {
56+
if (error == null) {
57+
mCurrentCallback.onSuccess(data);
58+
} else {
59+
mCurrentCallback.onFailure(new RuntimeException(error));
60+
}
61+
}
62+
mCurrentCallback = null;
63+
}
64+
65+
@Override
66+
public String getName() {
67+
return "JSDevSupport";
68+
}
69+
70+
}

ReactAndroid/src/main/java/com/facebook/react/uimanager/IllegalViewOperationException.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,28 @@
99

1010
package com.facebook.react.uimanager;
1111

12+
import android.support.annotation.Nullable;
13+
import android.view.View;
1214
import com.facebook.react.bridge.JSApplicationCausedNativeException;
1315

1416
/**
1517
* An exception caused by JS requesting the UI manager to perform an illegal view operation.
1618
*/
1719
public class IllegalViewOperationException extends JSApplicationCausedNativeException {
1820

21+
@Nullable private View mView;
22+
1923
public IllegalViewOperationException(String msg) {
2024
super(msg);
2125
}
2226

23-
public IllegalViewOperationException(String msg, Throwable cause) {
27+
public IllegalViewOperationException(String msg, @Nullable View view, Throwable cause) {
2428
super(msg, cause);
29+
mView = view;
30+
}
31+
32+
@Nullable
33+
public View getView() {
34+
return mView;
2535
}
2636
}

ReactAndroid/src/main/java/com/facebook/react/uimanager/RootView.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ public interface RootView {
2222
*/
2323
void onChildStartedNativeGesture(MotionEvent androidEvent);
2424

25-
void handleException(Exception e);
25+
void handleException(Throwable t);
2626
}

0 commit comments

Comments
 (0)