Skip to content

Commit e630bf9

Browse files
fix(tracing-ttd): Implement fallback system to screens that aren't reporting on the native layer the time to display. (#4042)
1 parent ac41368 commit e630bf9

18 files changed

+516
-77
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Enhanced accuracy of time-to-display spans. ([#4042](https://github.com/getsentry/sentry-react-native/pull/4042))
78
- TimetoTisplay correctly warns about not supporting the new React Native architecture ([#4160](https://github.com/getsentry/sentry-react-native/pull/4160))
89
- Native Wrapper method `setContext` ensures only values convertible to NativeMap are passed ([#4168](https://github.com/getsentry/sentry-react-native/pull/4168))
910
- Native Wrapper method `setExtra` ensures only stringified values are passed ([#4168](https://github.com/getsentry/sentry-react-native/pull/4168))

android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,11 @@
4141
import java.io.InputStream;
4242
import java.nio.charset.Charset;
4343
import java.util.HashMap;
44-
import java.util.HashSet;
4544
import java.util.List;
4645
import java.util.Map;
4746
import java.util.Properties;
48-
import java.util.Set;
4947
import java.util.concurrent.CountDownLatch;
5048

51-
import io.sentry.Breadcrumb;
52-
import io.sentry.DateUtils;
5349
import io.sentry.HubAdapter;
5450
import io.sentry.ILogger;
5551
import io.sentry.ISentryExecutorService;
@@ -135,10 +131,13 @@ public class RNSentryModuleImpl {
135131
/** Max trace file size in bytes. */
136132
private long maxTraceFileSize = 5 * 1024 * 1024;
137133

134+
private final @NotNull SentryDateProvider dateProvider;
135+
138136
public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) {
139137
packageInfo = getPackageInfo(reactApplicationContext);
140138
this.reactApplicationContext = reactApplicationContext;
141139
this.emitNewFrameEvent = createEmitNewFrameEvent();
140+
this.dateProvider = new SentryAndroidDateProvider();
142141
}
143142

144143
private ReactApplicationContext getReactApplicationContext() {
@@ -150,8 +149,6 @@ private ReactApplicationContext getReactApplicationContext() {
150149
}
151150

152151
private @NotNull Runnable createEmitNewFrameEvent() {
153-
final @NotNull SentryDateProvider dateProvider = new SentryAndroidDateProvider();
154-
155152
return () -> {
156153
final SentryDate endDate = dateProvider.now();
157154
WritableMap event = Arguments.createMap();
@@ -712,6 +709,10 @@ public void disableNativeFramesTracking() {
712709
}
713710
}
714711

712+
public void getNewScreenTimeToDisplay(Promise promise) {
713+
RNSentryTimeToDisplay.GetTimeToDisplay(promise, dateProvider);
714+
}
715+
715716
private String getProfilingTracesDirPath() {
716717
if (cacheDirPath == null) {
717718
cacheDirPath = new File(getReactApplicationContext().getCacheDir(), "sentry/react").getAbsolutePath();

android/src/main/java/io/sentry/react/RNSentryPackage.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,4 @@ public List<ViewManager> createViewManagers(
5555
new RNSentryOnDrawReporterManager(reactContext)
5656
);
5757
}
58-
5958
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.sentry.react;
2+
3+
import com.facebook.react.bridge.Promise;
4+
5+
import android.os.Handler;
6+
import android.os.Looper;
7+
import android.view.Choreographer;
8+
9+
import org.jetbrains.annotations.NotNull;
10+
import io.sentry.SentryDate;
11+
import io.sentry.SentryDateProvider;
12+
import io.sentry.android.core.SentryAndroidDateProvider;
13+
14+
public class RNSentryTimeToDisplay {
15+
public static void GetTimeToDisplay(Promise promise, SentryDateProvider dateProvider) {
16+
Looper mainLooper = Looper.getMainLooper();
17+
18+
if (mainLooper == null) {
19+
promise.reject("GetTimeToDisplay is not able to measure the time to display: Main looper not available.");
20+
return;
21+
}
22+
23+
// Ensure the code runs on the main thread
24+
new Handler(mainLooper)
25+
.post(() -> {
26+
try {
27+
Choreographer choreographer = Choreographer.getInstance();
28+
29+
// Invoke the callback after the frame is rendered
30+
choreographer.postFrameCallback(frameTimeNanos -> {
31+
final SentryDate endDate = dateProvider.now();
32+
promise.resolve(endDate.nanoTimestamp() / 1e9);
33+
});
34+
} catch (Exception exception) {
35+
promise.reject("Failed to receive the instance of Choreographer", exception);
36+
}
37+
});
38+
}
39+
}

android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,9 @@ public String getCurrentReplayId() {
173173
public void crashedLastRun(Promise promise) {
174174
this.impl.crashedLastRun(promise);
175175
}
176+
177+
@Override
178+
public void getNewScreenTimeToDisplay(Promise promise) {
179+
this.impl.getNewScreenTimeToDisplay(promise);
180+
}
176181
}

android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,9 @@ public String getCurrentReplayId() {
173173
public void crashedLastRun(Promise promise) {
174174
this.impl.crashedLastRun(promise);
175175
}
176+
177+
@ReactMethod()
178+
public void getNewScreenTimeToDisplay(Promise promise) {
179+
this.impl.getNewScreenTimeToDisplay(promise);
180+
}
176181
}

ios/RNSentry.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import <dlfcn.h>
22
#import "RNSentry.h"
3+
#import "RNSentryTimeToDisplay.h"
34

45
#if __has_include(<React/RCTConvert.h>)
56
#import <React/RCTConvert.h>
@@ -62,6 +63,7 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope;
6263
@implementation RNSentry {
6364
bool sentHybridSdkDidBecomeActive;
6465
bool hasListeners;
66+
RNSentryTimeToDisplay *_timeToDisplay;
6567
}
6668

6769
- (dispatch_queue_t)methodQueue
@@ -106,6 +108,8 @@ + (BOOL)requiresMainQueueSetup {
106108
sentHybridSdkDidBecomeActive = true;
107109
}
108110

111+
_timeToDisplay = [[RNSentryTimeToDisplay alloc] init];
112+
109113
#if SENTRY_TARGET_REPLAY_SUPPORTED
110114
[RNSentryReplay postInit];
111115
#endif
@@ -786,4 +790,9 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
786790
}
787791
#endif
788792

793+
RCT_EXPORT_METHOD(getNewScreenTimeToDisplay:(RCTPromiseResolveBlock)resolve
794+
rejecter:(RCTPromiseRejectBlock)reject) {
795+
[_timeToDisplay getTimeToDisplay:resolve];
796+
}
797+
789798
@end

ios/RNSentryTimeToDisplay.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <React/RCTBridgeModule.h>
2+
3+
@interface RNSentryTimeToDisplay : NSObject
4+
5+
- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback;
6+
7+
@end

ios/RNSentryTimeToDisplay.m

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#import "RNSentryTimeToDisplay.h"
2+
#import <QuartzCore/QuartzCore.h>
3+
#import <React/RCTLog.h>
4+
5+
@implementation RNSentryTimeToDisplay
6+
{
7+
CADisplayLink *displayLink;
8+
RCTResponseSenderBlock resolveBlock;
9+
}
10+
11+
// Rename requestAnimationFrame to getTimeToDisplay
12+
- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback
13+
{
14+
// Store the resolve block to use in the callback.
15+
resolveBlock = callback;
16+
17+
#if TARGET_OS_IOS
18+
// Create and add a display link to get the callback after the screen is rendered.
19+
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
20+
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
21+
#else
22+
resolveBlock(@[]); // Return nothing if not iOS.
23+
#endif
24+
}
25+
26+
#if TARGET_OS_IOS
27+
- (void)handleDisplayLink:(CADisplayLink *)link {
28+
// Get the current time
29+
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000.0; // Convert to milliseconds
30+
31+
// Ensure the callback is valid and pass the current time back
32+
if (resolveBlock) {
33+
resolveBlock(@[@(currentTime)]); // Call the callback with the current time
34+
resolveBlock = nil; // Clear the block after it's called
35+
}
36+
37+
// Invalidate the display link to stop future callbacks
38+
[displayLink invalidate];
39+
displayLink = nil;
40+
}
41+
#endif
42+
43+
@end

src/js/NativeRNSentry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import type { UnsafeObject } from './utils/rnlibrariesinterface';
99
export interface Spec extends TurboModule {
1010
addListener: (eventType: string) => void;
1111
removeListeners: (id: number) => void;
12+
getNewScreenTimeToDisplay(): Promise<number | undefined | null>;
1213
addBreadcrumb(breadcrumb: UnsafeObject): void;
1314
captureEnvelope(
1415
bytes: string,
1516
options: {
1617
hardCrashed: boolean;
1718
},
1819
): Promise<boolean>;
20+
1921
captureScreenshot(): Promise<NativeScreenshot[] | undefined | null>;
2022
clearBreadcrumbs(): void;
2123
crash(): void;

0 commit comments

Comments
 (0)