diff --git a/Libraries/Utilities/HeapCapture.js b/Libraries/Utilities/HeapCapture.js index 264a84e42..d686a903c 100644 --- a/Libraries/Utilities/HeapCapture.js +++ b/Libraries/Utilities/HeapCapture.js @@ -12,7 +12,7 @@ 'use strict'; var HeapCapture = { - captureHeap: function (token: number, path: string) { + captureHeap: function (path: string) { var error = null; try { global.nativeCaptureHeap(path); @@ -21,7 +21,7 @@ var HeapCapture = { console.log('HeapCapture.captureHeap error: ' + e.toString()); error = e.toString(); } - require('NativeModules').JSCHeapCapture.operationComplete(token, error); + require('NativeModules').JSCHeapCapture.captureComplete(path, error); }, }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 389e48519..8e0a1054e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -16,6 +16,7 @@ import java.util.List; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.devsupport.JSCHeapCapture; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; @@ -73,7 +74,7 @@ import com.facebook.systrace.Systrace; Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } - return Arrays.asList( + List nativeModulesList = new ArrayList<>(Arrays.asList( new AnimationsDebugModule( catalystApplicationContext, mReactInstanceManager.getDevSupportManager().getDevSettings()), @@ -83,13 +84,18 @@ import com.facebook.systrace.Systrace; new Timing(catalystApplicationContext, mReactInstanceManager.getDevSupportManager()), new SourceCodeModule(mReactInstanceManager.getSourceUrl()), uiManagerModule, - new JSCHeapCapture(catalystApplicationContext), - new DebugComponentOwnershipModule(catalystApplicationContext)); + new DebugComponentOwnershipModule(catalystApplicationContext))); + + if (ReactBuildConfig.DEBUG) { + nativeModulesList.add(new JSCHeapCapture(catalystApplicationContext)); + } + + return nativeModulesList; } @Override public List> createJSModules() { - return Arrays.asList( + List> jsModules = new ArrayList<>(Arrays.asList( DeviceEventManagerModule.RCTDeviceEventEmitter.class, JSTimersExecution.class, RCTEventEmitter.class, @@ -97,8 +103,13 @@ import com.facebook.systrace.Systrace; AppRegistry.class, com.facebook.react.bridge.Systrace.class, HMRClient.class, - JSCHeapCapture.HeapCapture.class, - DebugComponentOwnershipModule.RCTDebugComponentOwnership.class); + DebugComponentOwnershipModule.RCTDebugComponentOwnership.class)); + + if (ReactBuildConfig.DEBUG) { + jsModules.add(JSCHeapCapture.HeapCapture.class); + } + + return jsModules; } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index 7e0976d49..8c684e308 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -62,6 +62,7 @@ public class DevServerHelper { private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client"; private static final String PACKAGER_CONNECTION_URL_FORMAT = "ws://%s/message?role=shell"; private static final String PACKAGER_STATUS_URL_FORMAT = "http://%s/status"; + private static final String HEAP_CAPTURE_UPLOAD_URL_FORMAT = "http://%s/jscheapcaptureupload"; private static final String PACKAGER_OK_STATUS = "packager-status:running"; @@ -130,6 +131,10 @@ public class DevServerHelper { return String.format(Locale.US, PACKAGER_CONNECTION_URL_FORMAT, getDebugServerHost()); } + public String getHeapCaptureUploadUrl() { + return String.format(Locale.US, HEAP_CAPTURE_UPLOAD_URL_FORMAT, getDebugServerHost()); + } + /** * @return the host to use when connecting to the bundle server from the host itself. */ @@ -383,7 +388,7 @@ public class DevServerHelper { } private String createLaunchJSDevtoolsCommandUrl() { - return String.format(LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT, getDebugServerHost()); + return String.format(Locale.US, LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT, getDebugServerHost()); } public void launchJSDevtools() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java index 629a3f12b..b919e2408 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -39,6 +39,7 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { String getSourceUrl(); String getJSBundleURLForRemoteDebugging(); String getDownloadedJSBundleFile(); + String getHeapCaptureUploadUrl(); boolean hasUpToDateJSBundleInCache(); void reloadSettings(); void handleReloadJS(); 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 dfb826fb9..c89551f01 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -370,18 +370,9 @@ public class DevSupportManagerImpl implements DevSupportManager { new DevOptionHandler() { @Override public void onOptionSelected() { - try { - String heapDumpPath = mApplicationContext.getCacheDir().getPath(); - List captureFiles = JSCHeapCapture.captureHeap(heapDumpPath, 60000); - for (String captureFile : captureFiles) { - Toast.makeText( - mCurrentContext, - "Heap captured to " + captureFile, - Toast.LENGTH_LONG).show(); - } - } catch (JSCHeapCapture.CaptureException e) { - showNewJavaError(e.getMessage(), e); - } + JSCHeapCapture.captureHeap( + mApplicationContext.getCacheDir().getPath(), + JSCHeapUpload.captureCallback(mDevServerHelper.getHeapCaptureUploadUrl())); } }); options.put( @@ -486,6 +477,11 @@ public class DevSupportManagerImpl implements DevSupportManager { return mJSBundleTempFile.getAbsolutePath(); } + @Override + public String getHeapCaptureUploadUrl() { + return mDevServerHelper.getHeapCaptureUploadUrl(); + } + /** * @return {@code true} if {@link com.facebook.react.ReactInstanceManager} should use downloaded JS bundle file * instead of using JS file from assets. This may happen when app has not been updated since diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index 9ec71be18..1f9539f7a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -104,6 +104,11 @@ public class DisabledDevSupportManager implements DevSupportManager { return null; } + @Override + public String getHeapCaptureUploadUrl() { + return null; + } + @Override public boolean hasUpToDateJSBundleInCache() { return false; diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapCapture.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapCapture.java index 4080b5d07..a8824fd79 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapCapture.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapCapture.java @@ -23,20 +23,29 @@ import com.facebook.react.bridge.ReactMethod; public class JSCHeapCapture extends ReactContextBaseJavaModule { public interface HeapCapture extends JavaScriptModule { - void captureHeap(int token, String path); - void setAllocationTracking(int token, boolean enabled); + void captureHeap(String path); } public static class CaptureException extends Exception { CaptureException(String message) { super(message); } + CaptureException(String message, Throwable cause) { + super(message, cause); + } + } + + public interface CaptureCallback { + void onComplete(List captures, List failures); + } + + private interface PerCaptureCallback { + void onSuccess(File capture); + void onFailure(CaptureException cause); } private @Nullable HeapCapture mHeapCapture; - private boolean mOperationInProgress; - private int mOperationToken; - private @Nullable String mOperationError; + private @Nullable PerCaptureCallback mCaptureInProgress; private static final HashSet sRegisteredDumpers = new HashSet<>(); @@ -52,11 +61,14 @@ public class JSCHeapCapture extends ReactContextBaseJavaModule { sRegisteredDumpers.remove(dumper); } - public static synchronized List captureHeap(String path, long timeout) - throws CaptureException { - LinkedList captureFiles = new LinkedList<>(); + public static synchronized void captureHeap(String path, final CaptureCallback callback) { + final LinkedList captureFiles = new LinkedList<>(); + final LinkedList captureFailures = new LinkedList<>(); + if (sRegisteredDumpers.isEmpty()) { - throw new CaptureException("No JSC registered"); + captureFailures.add(new CaptureException("No JSC registered")); + callback.onComplete(captureFiles, captureFailures); + return; } int disambiguate = 0; @@ -66,64 +78,57 @@ public class JSCHeapCapture extends ReactContextBaseJavaModule { f = new File(path + "/capture" + Integer.toString(disambiguate) + ".json"); } + final int numRegisteredDumpers = sRegisteredDumpers.size(); disambiguate = 0; for (JSCHeapCapture dumper : sRegisteredDumpers) { - String file = path + "/capture" + Integer.toString(disambiguate) + ".json"; - dumper.captureHeapHelper(file, timeout); - captureFiles.add(file); + File file = new File(path + "/capture" + Integer.toString(disambiguate) + ".json"); + dumper.captureHeapHelper(file, new PerCaptureCallback() { + @Override + public void onSuccess(File capture) { + captureFiles.add(capture); + if (captureFiles.size() + captureFailures.size() == numRegisteredDumpers) { + callback.onComplete(captureFiles, captureFailures); + } + } + @Override + public void onFailure(CaptureException cause) { + captureFailures.add(cause); + if (captureFiles.size() + captureFailures.size() == numRegisteredDumpers) { + callback.onComplete(captureFiles, captureFailures); + } + } + }); } - return captureFiles; } public JSCHeapCapture(ReactApplicationContext reactContext) { super(reactContext); mHeapCapture = null; - mOperationInProgress = false; - mOperationToken = 0; - mOperationError = null; + mCaptureInProgress = null; } - private synchronized void captureHeapHelper(String path, long timeout) throws CaptureException { + private synchronized void captureHeapHelper(File file, PerCaptureCallback callback) { if (mHeapCapture == null) { - throw new CaptureException("HeapCapture.js module not connected"); + callback.onFailure(new CaptureException("HeapCapture.js module not connected")); + return; } - mHeapCapture.captureHeap(getOperationToken(), path); - waitForOperation(timeout); - } - - private int getOperationToken() throws CaptureException { - if (mOperationInProgress) { - throw new CaptureException("Another operation already in progress."); - } - mOperationInProgress = true; - return ++mOperationToken; - } - - private void waitForOperation(long timeout) throws CaptureException { - try { - wait(timeout); - } catch (InterruptedException e) { - throw new CaptureException("Waiting for heap capture failed: " + e.getMessage()); - } - - if (mOperationInProgress) { - mOperationInProgress = false; - throw new CaptureException("heap capture timed out."); - } - - if (mOperationError != null) { - throw new CaptureException(mOperationError); + if (mCaptureInProgress != null) { + callback.onFailure(new CaptureException("Heap capture already in progress")); + return; } + mCaptureInProgress = callback; + mHeapCapture.captureHeap(file.getPath()); } @ReactMethod - public synchronized void operationComplete(int token, String error) { - if (token == mOperationToken) { - mOperationInProgress = false; - mOperationError = error; - this.notify(); - } else { - throw new RuntimeException("Completed operation is not in progress."); + public synchronized void captureComplete(String path, String error) { + if (mCaptureInProgress != null) { + if (error == null) { + mCaptureInProgress.onSuccess(new File(path)); + } else { + mCaptureInProgress.onFailure(new CaptureException(error)); + } + mCaptureInProgress = null; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java new file mode 100644 index 000000000..240ed580e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCHeapUpload.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2016-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. + */ + +package com.facebook.react.devsupport; + +import android.util.Log; +import java.io.File; +import java.io.IOException; +import java.util.List; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * Created by cwdick on 7/22/16. + */ +public class JSCHeapUpload { + public static JSCHeapCapture.CaptureCallback captureCallback(final String uploadUrl) { + return new JSCHeapCapture.CaptureCallback() { + @Override + public void onComplete( + List captures, + List failures) { + for (JSCHeapCapture.CaptureException e : failures) { + Log.e("JSCHeapCapture", e.getMessage()); + } + + OkHttpClient httpClient = new OkHttpClient.Builder().build(); + + for (File path : captures) { + RequestBody body = RequestBody.create(MediaType.parse("application/json"), path); + Request request = new Request.Builder() + .url(uploadUrl) + .method("POST", body) + .build(); + Call call = httpClient.newCall(request); + call.enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + Log.e("JSCHeapCapture", "Upload of heap capture failed: " + e.toString()); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) { + Log.e("JSCHeapCapture", "Upload of heap capture failed with code: " + Integer.toString(response.code())); + } + } + }); + } + } + }; + } +} diff --git a/local-cli/server/middleware/heapCaptureMiddleware.js b/local-cli/server/middleware/heapCaptureMiddleware.js new file mode 100644 index 000000000..cd1dcfc22 --- /dev/null +++ b/local-cli/server/middleware/heapCaptureMiddleware.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2016-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. + */ +'use strict'; + +const exec = require('child_process').exec; +const fs = require('fs'); +const path = require('path'); + +module.exports = function(req, res, next) { + if (req.url !== '/jscheapcaptureupload') { + next(); + return; + } + + console.log('Receiving heap capture...'); + var captureName = '/tmp/capture_' + Date.now() + '.json'; + fs.writeFileSync(captureName, req.rawBody); + console.log('Capture written to ' + captureName); + res.end(); +}; diff --git a/local-cli/server/runServer.js b/local-cli/server/runServer.js index fb3cec33f..6d3536a98 100644 --- a/local-cli/server/runServer.js +++ b/local-cli/server/runServer.js @@ -22,6 +22,7 @@ const ReactPackager = require('../../packager/react-packager'); const statusPageMiddleware = require('./middleware/statusPageMiddleware.js'); const indexPageMiddleware = require('./middleware/indexPage'); const systraceProfileMiddleware = require('./middleware/systraceProfileMiddleware.js'); +const heapCaptureMiddleware = require('./middleware/heapCaptureMiddleware.js'); const webSocketProxy = require('./util/webSocketProxy.js'); function runServer(args, config, readyCallback) { @@ -37,6 +38,7 @@ function runServer(args, config, readyCallback) { .use(copyToClipBoardMiddleware) .use(statusPageMiddleware) .use(systraceProfileMiddleware) + .use(heapCaptureMiddleware) .use(cpuProfilerMiddleware) .use(indexPageMiddleware) .use(packagerServer.processRequest.bind(packagerServer));