11package io .sentry .react ;
22
3+ import static java .util .concurrent .TimeUnit .SECONDS ;
34import static io .sentry .android .core .internal .util .ScreenshotUtils .takeScreenshot ;
5+ import static io .sentry .vendor .Base64 .NO_PADDING ;
6+ import static io .sentry .vendor .Base64 .NO_WRAP ;
47
58import android .app .Activity ;
69import android .content .Context ;
2831
2932import java .io .BufferedInputStream ;
3033import java .io .BufferedReader ;
34+ import java .io .ByteArrayOutputStream ;
3135import java .io .File ;
36+ import java .io .FileInputStream ;
3237import java .io .FileNotFoundException ;
3338import java .io .FileReader ;
39+ import java .io .IOException ;
3440import java .io .InputStream ;
3541import java .nio .charset .Charset ;
3642import java .util .HashMap ;
3743import java .util .List ;
3844import java .util .Map ;
45+ import java .util .Properties ;
3946import java .util .concurrent .CountDownLatch ;
40- import java .util .concurrent .TimeUnit ;
4147
4248import io .sentry .Breadcrumb ;
4349import io .sentry .DateUtils ;
4450import io .sentry .HubAdapter ;
4551import io .sentry .ILogger ;
52+ import io .sentry .ISentryExecutorService ;
4653import io .sentry .IScope ;
4754import io .sentry .ISerializer ;
4855import io .sentry .Integration ;
4956import io .sentry .Sentry ;
5057import io .sentry .SentryDate ;
5158import io .sentry .SentryEvent ;
59+ import io .sentry .SentryExecutorService ;
5260import io .sentry .SentryLevel ;
5361import io .sentry .SentryOptions ;
5462import io .sentry .UncaughtExceptionHandlerIntegration ;
5563import io .sentry .android .core .AndroidLogger ;
64+ import io .sentry .android .core .AndroidProfiler ;
5665import io .sentry .android .core .AnrIntegration ;
5766import io .sentry .android .core .BuildConfig ;
5867import io .sentry .android .core .BuildInfoProvider ;
6271import io .sentry .android .core .SentryAndroid ;
6372import io .sentry .android .core .SentryAndroidOptions ;
6473import io .sentry .android .core .ViewHierarchyEventProcessor ;
74+ import io .sentry .android .core .internal .debugmeta .AssetsDebugMetaLoader ;
75+ import io .sentry .android .core .internal .util .SentryFrameMetricsCollector ;
6576import io .sentry .android .core .performance .AppStartMetrics ;
6677import io .sentry .protocol .SdkVersion ;
6778import io .sentry .protocol .SentryException ;
6879import io .sentry .protocol .SentryPackage ;
6980import io .sentry .protocol .User ;
7081import io .sentry .protocol .ViewHierarchy ;
82+ import io .sentry .util .DebugMetaPropertiesApplier ;
7183import io .sentry .util .JsonSerializationUtils ;
7284import io .sentry .vendor .Base64 ;
85+ import io .sentry .util .FileUtils ;
7386
7487public class RNSentryModuleImpl {
7588
@@ -96,6 +109,23 @@ public class RNSentryModuleImpl {
96109
97110 private static final int SCREENSHOT_TIMEOUT_SECONDS = 2 ;
98111
112+ /**
113+ * Profiling traces rate. 101 hz means 101 traces in 1 second. Defaults to 101 to avoid possible
114+ * lockstep sampling. More on
115+ * https://stackoverflow.com/questions/45470758/what-is-lockstep-sampling
116+ */
117+ private int profilingTracesHz = 101 ;
118+
119+ private AndroidProfiler androidProfiler = null ;
120+
121+ private boolean isProguardDebugMetaLoaded = false ;
122+ private @ Nullable String proguardUuid = null ;
123+ private String cacheDirPath = null ;
124+ private ISentryExecutorService executorService = null ;
125+
126+ /** Max trace file size in bytes. */
127+ private long maxTraceFileSize = 5 * 1024 * 1024 ;
128+
99129 public RNSentryModuleImpl (ReactApplicationContext reactApplicationContext ) {
100130 packageInfo = getPackageInfo (reactApplicationContext );
101131 this .reactApplicationContext = reactApplicationContext ;
@@ -393,7 +423,7 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) {
393423 }
394424
395425 try {
396- doneSignal .await (SCREENSHOT_TIMEOUT_SECONDS , TimeUnit . SECONDS );
426+ doneSignal .await (SCREENSHOT_TIMEOUT_SECONDS , SECONDS );
397427 } catch (InterruptedException e ) {
398428 logger .log (SentryLevel .ERROR , "Screenshot process was interrupted." );
399429 return null ;
@@ -611,10 +641,41 @@ public void disableNativeFramesTracking() {
611641 }
612642 }
613643
644+ private String getProfilingTracesDirPath () {
645+ if (cacheDirPath == null ) {
646+ cacheDirPath = new File (getReactApplicationContext ().getCacheDir (), "sentry/react" ).getAbsolutePath ();
647+ }
648+ File profilingTraceDir = new File (cacheDirPath , "profiling_trace" );
649+ profilingTraceDir .mkdirs ();
650+ return profilingTraceDir .getAbsolutePath ();
651+ }
652+
653+ private void initializeAndroidProfiler () {
654+ if (executorService == null ) {
655+ executorService = new SentryExecutorService ();
656+ }
657+ final String tracesFilesDirPath = getProfilingTracesDirPath ();
658+
659+ androidProfiler = new AndroidProfiler (
660+ tracesFilesDirPath ,
661+ (int ) SECONDS .toMicros (1 ) / profilingTracesHz ,
662+ new SentryFrameMetricsCollector (reactApplicationContext , logger , buildInfo ),
663+ executorService ,
664+ logger ,
665+ buildInfo
666+ );
667+ }
668+
614669 public WritableMap startProfiling () {
615670 final WritableMap result = new WritableNativeMap ();
671+ if (androidProfiler == null ) {
672+ initializeAndroidProfiler ();
673+ }
674+
616675 try {
617676 HermesSamplingProfiler .enable ();
677+ androidProfiler .start ();
678+
618679 result .putBoolean ("started" , true );
619680 } catch (Throwable e ) {
620681 result .putBoolean ("started" , false );
@@ -628,27 +689,26 @@ public WritableMap stopProfiling() {
628689 final WritableMap result = new WritableNativeMap ();
629690 File output = null ;
630691 try {
692+ AndroidProfiler .ProfileEndData end = androidProfiler .endAndCollect (false , null );
631693 HermesSamplingProfiler .disable ();
632694
633695 output = File .createTempFile (
634696 "sampling-profiler-trace" , ".cpuprofile" , reactApplicationContext .getCacheDir ());
635-
636697 if (isDebug ) {
637698 logger .log (SentryLevel .INFO , "Profile saved to: " + output .getAbsolutePath ());
638699 }
639700
640- try ( final BufferedReader br = new BufferedReader ( new FileReader ( output ));) {
641- HermesSamplingProfiler . dumpSampledTraceToFile ( output . getPath ( ));
701+ HermesSamplingProfiler . dumpSampledTraceToFile ( output . getPath ( ));
702+ result . putString ( "profile" , readStringFromFile ( output ));
642703
643- final StringBuilder text = new StringBuilder ();
644- String line ;
645- while ((line = br .readLine ()) != null ) {
646- text .append (line );
647- text .append ('\n' );
648- }
704+ WritableMap androidProfile = new WritableNativeMap ();
705+ byte [] androidProfileBytes = FileUtils .readBytesFromFile (end .traceFile .getPath (), maxTraceFileSize );
706+ String base64AndroidProfile = Base64 .encodeToString (androidProfileBytes , NO_WRAP | NO_PADDING );
649707
650- result .putString ("profile" , text .toString ());
651- }
708+ androidProfile .putString ("sampled_profile" , base64AndroidProfile );
709+ androidProfile .putInt ("android_api_level" , buildInfo .getSdkInfoVersion ());
710+ androidProfile .putString ("build_id" , getProguardUuid ());
711+ result .putMap ("androidProfile" , androidProfile );
652712 } catch (Throwable e ) {
653713 result .putString ("error" , e .toString ());
654714 } finally {
@@ -666,6 +726,32 @@ public WritableMap stopProfiling() {
666726 return result ;
667727 }
668728
729+ private @ Nullable String getProguardUuid () {
730+ if (isProguardDebugMetaLoaded ) {
731+ return proguardUuid ;
732+ }
733+ isProguardDebugMetaLoaded = true ;
734+ final @ Nullable Properties debugMeta = (new AssetsDebugMetaLoader (this .getReactApplicationContext (), logger )).loadDebugMeta ();
735+ if (debugMeta != null ) {
736+ proguardUuid = DebugMetaPropertiesApplier .getProguardUuid (debugMeta );
737+ return proguardUuid ;
738+ }
739+ return null ;
740+ }
741+
742+ private String readStringFromFile (File path ) throws IOException {
743+ try (final BufferedReader br = new BufferedReader (new FileReader (path ));) {
744+
745+ final StringBuilder text = new StringBuilder ();
746+ String line ;
747+ while ((line = br .readLine ()) != null ) {
748+ text .append (line );
749+ text .append ('\n' );
750+ }
751+ return text .toString ();
752+ }
753+ }
754+
669755 public void fetchNativeDeviceContexts (Promise promise ) {
670756 final @ NotNull SentryOptions options = HubAdapter .getInstance ().getOptions ();
671757 if (!(options instanceof SentryAndroidOptions )) {
0 commit comments