diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js index f1450ed4b3cacf..8e67cf902e3d52 100644 --- a/Libraries/BatchedBridge/BatchedBridge.js +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -27,6 +27,7 @@ const JSTimersExecution = require('JSTimersExecution'); BatchedBridge.registerCallableModule('Systrace', Systrace); BatchedBridge.registerCallableModule('JSTimersExecution', JSTimersExecution); BatchedBridge.registerCallableModule('HeapCapture', require('HeapCapture')); +BatchedBridge.registerCallableModule('SamplingProfiler', require('SamplingProfiler')); if (__DEV__) { BatchedBridge.registerCallableModule('HMRClient', require('HMRClient')); diff --git a/Libraries/Utilities/SamplingProfiler.js b/Libraries/Utilities/SamplingProfiler.js new file mode 100644 index 00000000000000..75982f51bd624b --- /dev/null +++ b/Libraries/Utilities/SamplingProfiler.js @@ -0,0 +1,35 @@ +/** + * 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 SamplingProfiler + * @flow + */ +'use strict'; + +var SamplingProfiler = { + poke: function (token: number): void { + var error = null; + var result = null; + try { + result = global.pokeSamplingProfiler(); + if (result === null) { + console.log('The JSC Sampling Profiler has started'); + } else { + console.log('The JSC Sampling Profiler has stopped'); + } + } catch (e) { + console.log( + 'Error occured when restarting Sampling Profiler: ' + e.toString()); + error = e.toString(); + } + require('NativeModules').JSCSamplingProfiler.operationComplete( + token, result, error); + }, +}; + +module.exports = SamplingProfiler; diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 8e0a1054e2ff6a..1377c13ae21f66 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -18,6 +18,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.devsupport.JSCHeapCapture; +import com.facebook.react.devsupport.JSCSamplingProfiler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.ExceptionsManagerModule; @@ -84,6 +85,7 @@ public List createNativeModules( new Timing(catalystApplicationContext, mReactInstanceManager.getDevSupportManager()), new SourceCodeModule(mReactInstanceManager.getSourceUrl()), uiManagerModule, + new JSCSamplingProfiler(catalystApplicationContext), new DebugComponentOwnershipModule(catalystApplicationContext))); if (ReactBuildConfig.DEBUG) { @@ -103,6 +105,7 @@ public List> createJSModules() { AppRegistry.class, com.facebook.react.bridge.Systrace.class, HMRClient.class, + JSCSamplingProfiler.SamplingProfiler.class, DebugComponentOwnershipModule.RCTDebugComponentOwnership.class)); if (ReactBuildConfig.DEBUG) { 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 c89551f01cc964..db1907d6d0462f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -375,6 +375,26 @@ public void onOptionSelected() { JSCHeapUpload.captureCallback(mDevServerHelper.getHeapCaptureUploadUrl())); } }); + options.put( + mApplicationContext.getString(R.string.catalyst_poke_sampling_profiler), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + try { + List pokeResults = JSCSamplingProfiler.poke(60000); + for (String result : pokeResults) { + Toast.makeText( + mCurrentContext, + result == null + ? "Started JSC Sampling Profiler" + : "Stopped JSC Sampling Profiler", + Toast.LENGTH_LONG).show(); + } + } catch (JSCSamplingProfiler.ProfilerException e) { + showNewJavaError(e.getMessage(), e); + } + } + }); options.put( mApplicationContext.getString(R.string.catalyst_settings), new DevOptionHandler() { @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCSamplingProfiler.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCSamplingProfiler.java new file mode 100644 index 00000000000000..89a03e8020c4c4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/JSCSamplingProfiler.java @@ -0,0 +1,147 @@ +/** + * 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. + */ + +package com.facebook.react.devsupport; + +import javax.annotation.Nullable; + +import java.io.File; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class JSCSamplingProfiler extends ReactContextBaseJavaModule { + public interface SamplingProfiler extends JavaScriptModule { + void poke(int token); + } + + public static class ProfilerException extends Exception { + ProfilerException(String message) { + super(message); + } + } + + private @Nullable SamplingProfiler mSamplingProfiler; + private boolean mOperationInProgress; + private int mOperationToken; + private @Nullable String mOperationError; + private @Nullable String mSamplingProfilerResult; + + private static final HashSet sRegisteredDumpers = + new HashSet<>(); + + private static synchronized void registerSamplingProfiler( + JSCSamplingProfiler dumper) { + if (sRegisteredDumpers.contains(dumper)) { + throw new RuntimeException( + "a JSCSamplingProfiler registered more than once"); + } + sRegisteredDumpers.add(dumper); + } + + private static synchronized void unregisterSamplingProfiler( + JSCSamplingProfiler dumper) { + sRegisteredDumpers.remove(dumper); + } + + public static synchronized List poke(long timeout) + throws ProfilerException { + LinkedList results = new LinkedList<>(); + if (sRegisteredDumpers.isEmpty()) { + throw new ProfilerException("No JSC registered"); + } + + for (JSCSamplingProfiler dumper : sRegisteredDumpers) { + dumper.pokeHelper(timeout); + results.add(dumper.mSamplingProfilerResult); + } + return results; + } + + public JSCSamplingProfiler(ReactApplicationContext reactContext) { + super(reactContext); + mSamplingProfiler = null; + mOperationInProgress = false; + mOperationToken = 0; + mOperationError = null; + mSamplingProfilerResult = null; + } + + private synchronized void pokeHelper(long timeout) throws ProfilerException { + if (mSamplingProfiler == null) { + throw new ProfilerException("SamplingProfiler.js module not connected"); + } + mSamplingProfiler.poke(getOperationToken()); + waitForOperation(timeout); + } + + private int getOperationToken() throws ProfilerException { + if (mOperationInProgress) { + throw new ProfilerException("Another operation already in progress."); + } + mOperationInProgress = true; + return ++mOperationToken; + } + + private void waitForOperation(long timeout) throws ProfilerException { + try { + wait(timeout); + } catch (InterruptedException e) { + throw new ProfilerException( + "Waiting for heap capture failed: " + e.getMessage()); + } + + if (mOperationInProgress) { + mOperationInProgress = false; + throw new ProfilerException("heap capture timed out."); + } + + if (mOperationError != null) { + throw new ProfilerException(mOperationError); + } + } + + @ReactMethod + public synchronized void operationComplete( + int token, String result, String error) { + if (token == mOperationToken) { + mOperationInProgress = false; + mSamplingProfilerResult = result; + mOperationError = error; + this.notify(); + } else { + throw new RuntimeException("Completed operation is not in progress."); + } + } + + @Override + public String getName() { + return "JSCSamplingProfiler"; + } + + @Override + public void initialize() { + super.initialize(); + mSamplingProfiler = + getReactApplicationContext().getJSModule(SamplingProfiler.class); + registerSamplingProfiler(this); + } + + @Override + public void onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy(); + unregisterSamplingProfiler(this); + mSamplingProfiler = null; + } +} diff --git a/ReactAndroid/src/main/res/devsupport/values/strings.xml b/ReactAndroid/src/main/res/devsupport/values/strings.xml index f5b190ff82687f..dbc3343ca28445 100644 --- a/ReactAndroid/src/main/res/devsupport/values/strings.xml +++ b/ReactAndroid/src/main/res/devsupport/values/strings.xml @@ -20,6 +20,7 @@ Capture Heap Dismiss\n(ESC) Reload\n(R,\u00A0R) + Start/Stop Sampling Profiler Copy Report diff --git a/ReactCommon/cxxreact/JSCSamplingProfiler.cpp b/ReactCommon/cxxreact/JSCSamplingProfiler.cpp index 936ee41e028fe3..ddc4e9309aea5c 100644 --- a/ReactCommon/cxxreact/JSCSamplingProfiler.cpp +++ b/ReactCommon/cxxreact/JSCSamplingProfiler.cpp @@ -4,13 +4,30 @@ #include "JSCSamplingProfiler.h" +#include +#include #include +#include "JSCHelpers.h" + +#include "Value.h" namespace facebook { namespace react { +namespace { +static JSValueRef pokeSamplingProfiler( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return JSPokeSamplingProfiler(ctx); +} +} void initSamplingProfilerOnMainJSCThread(JSGlobalContextRef ctx) { JSStartSamplingProfilingOnMainJSCThread(ctx); + installGlobalFunction(ctx, "pokeSamplingProfiler", pokeSamplingProfiler); } }