Skip to content

Commit de033da

Browse files
feat: Add frames delay to transactions (#3487)
Add frames delay as span data to transactions.
1 parent f25febb commit de033da

17 files changed

+847
-61
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## Features
66

7+
- Add frames delay to transactions (#3487)
78
- Add slow and frozen frames to spans (#3450, #3478)
89

910
## 8.17.1

Sentry.xcodeproj/project.pbxproj

+16
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
622C08DB29E554B9002571D4 /* SentrySpanContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */; };
8282
623C45B02A651D8200D9E88B /* SentryCoreDataTracker+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = 623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */; };
8383
627E7589299F6FE40085504D /* SentryInternalDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 627E7588299F6FE40085504D /* SentryInternalDefines.h */; };
84+
62862B1C2B1DDBC8009B16E3 /* SentryDelayedFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */; };
85+
62862B1E2B1DDC35009B16E3 /* SentryDelayedFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */; };
8486
62885DA729E946B100554F38 /* TestConncurrentModifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62885DA629E946B100554F38 /* TestConncurrentModifications.swift */; };
8587
62950F1029E7FE0100A42624 /* SentryTransactionContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */; };
8688
629690532AD3E060000185FA /* SentryReachabilitySwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629690522AD3E060000185FA /* SentryReachabilitySwiftTests.swift */; };
@@ -91,6 +93,8 @@
9193
62B86CFC29F052BB008F3947 /* SentryTestLogConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */; };
9294
62C25C862B075F4900C68CBD /* TestOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C25C852B075F4900C68CBD /* TestOptions.swift */; };
9395
62C3168B2B1F865A000D7031 /* SentryTimeSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C3168A2B1F865A000D7031 /* SentryTimeSwiftTests.swift */; };
96+
62C316812B1F2E93000D7031 /* SentryDelayedFramesTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 62C316802B1F2E93000D7031 /* SentryDelayedFramesTracker.h */; };
97+
62C316832B1F2EA1000D7031 /* SentryDelayedFramesTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 62C316822B1F2EA1000D7031 /* SentryDelayedFramesTracker.m */; };
9498
62E081A929ED4260000F69FC /* SentryBreadcrumbDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 62E081A829ED4260000F69FC /* SentryBreadcrumbDelegate.h */; };
9599
62E081AB29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */; };
96100
62F226B729A37C120038080D /* SentryBooleanSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F226B629A37C120038080D /* SentryBooleanSerialization.m */; };
@@ -995,6 +999,8 @@
995999
623C45AE2A651C4500D9E88B /* SentryCoreDataTracker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCoreDataTracker+Test.h"; sourceTree = "<group>"; };
9961000
623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryCoreDataTracker+Test.m"; sourceTree = "<group>"; };
9971001
627E7588299F6FE40085504D /* SentryInternalDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalDefines.h; path = include/SentryInternalDefines.h; sourceTree = "<group>"; };
1002+
62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDelayedFrame.h; path = include/SentryDelayedFrame.h; sourceTree = "<group>"; };
1003+
62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDelayedFrame.m; sourceTree = "<group>"; };
9981004
62885DA629E946B100554F38 /* TestConncurrentModifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConncurrentModifications.swift; sourceTree = "<group>"; };
9991005
62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionContextTests.swift; sourceTree = "<group>"; };
10001006
629690522AD3E060000185FA /* SentryReachabilitySwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReachabilitySwiftTests.swift; sourceTree = "<group>"; };
@@ -1004,6 +1010,8 @@
10041010
62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTestLogConfig.m; sourceTree = "<group>"; };
10051011
62C25C852B075F4900C68CBD /* TestOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestOptions.swift; sourceTree = "<group>"; };
10061012
62C3168A2B1F865A000D7031 /* SentryTimeSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTimeSwiftTests.swift; sourceTree = "<group>"; };
1013+
62C316802B1F2E93000D7031 /* SentryDelayedFramesTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDelayedFramesTracker.h; path = include/SentryDelayedFramesTracker.h; sourceTree = "<group>"; };
1014+
62C316822B1F2EA1000D7031 /* SentryDelayedFramesTracker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDelayedFramesTracker.m; sourceTree = "<group>"; };
10071015
62E081A829ED4260000F69FC /* SentryBreadcrumbDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBreadcrumbDelegate.h; path = include/SentryBreadcrumbDelegate.h; sourceTree = "<group>"; };
10081016
62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBreadcrumbTestDelegate.swift; sourceTree = "<group>"; };
10091017
62F226B629A37C120038080D /* SentryBooleanSerialization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBooleanSerialization.m; sourceTree = "<group>"; };
@@ -2943,6 +2951,10 @@
29432951
7B6C5EDD264E8DF00010D138 /* SentryFramesTracker.m */,
29442952
7B7A599426B692540060A676 /* SentryScreenFrames.h */,
29452953
7B7A599626B692F00060A676 /* SentryScreenFrames.m */,
2954+
62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */,
2955+
62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */,
2956+
62C316802B1F2E93000D7031 /* SentryDelayedFramesTracker.h */,
2957+
62C316822B1F2EA1000D7031 /* SentryDelayedFramesTracker.m */,
29462958
);
29472959
name = FramesTracking;
29482960
sourceTree = "<group>";
@@ -3573,6 +3585,7 @@
35733585
63B818F91EC34639002FDF4C /* SentryDebugMeta.h in Headers */,
35743586
6360850D1ED2AFE100E8599E /* SentryBreadcrumb.h in Headers */,
35753587
7BAF3DD92440AEC8008A5414 /* SentryRequestManager.h in Headers */,
3588+
62862B1C2B1DDBC8009B16E3 /* SentryDelayedFrame.h in Headers */,
35763589
627E7589299F6FE40085504D /* SentryInternalDefines.h in Headers */,
35773590
7BE3C77B2446111500A38442 /* SentryRateLimitParser.h in Headers */,
35783591
84A888FD28D9B11700C51DFD /* SentryProfiler+Private.h in Headers */,
@@ -3628,6 +3641,7 @@
36283641
63FE718720DA4C1100CDBAE8 /* SentryCrashReportVersion.h in Headers */,
36293642
0A56DA5F28ABA01B00C400D5 /* SentryTransactionContext+Private.h in Headers */,
36303643
8E7C98312693E1CC00E6336C /* SentryTraceHeader.h in Headers */,
3644+
62C316812B1F2E93000D7031 /* SentryDelayedFramesTracker.h in Headers */,
36313645
7BBD18912449BE9000427C76 /* SentryDefaultRateLimits.h in Headers */,
36323646
7B8713AE26415ADF006D6004 /* SentryAppStartTrackingIntegration.h in Headers */,
36333647
7B7D873224864BB900D2ECFF /* SentryCrashMachineContextWrapper.h in Headers */,
@@ -4136,6 +4150,7 @@
41364150
63FE712920DA4C1000CDBAE8 /* SentryCrashCPU_arm.c in Sources */,
41374151
03F84D3427DD4191008FE43F /* SentryThreadMetadataCache.cpp in Sources */,
41384152
7B88F30024BC5A7D00ADF90A /* SentrySdkInfo.m in Sources */,
4153+
62862B1E2B1DDC35009B16E3 /* SentryDelayedFrame.m in Sources */,
41394154
84AC61D729F75A98009EEF61 /* SentryDispatchFactory.m in Sources */,
41404155
15360CD62432832400112302 /* SentryAutoSessionTrackingIntegration.m in Sources */,
41414156
7B63459F280EBA7200CFA05A /* SentryUIEventTracker.m in Sources */,
@@ -4229,6 +4244,7 @@
42294244
03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */,
42304245
63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */,
42314246
7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */,
4247+
62C316832B1F2EA1000D7031 /* SentryDelayedFramesTracker.m in Sources */,
42324248
D8BFE37329A3782F002E73F3 /* SentryTimeToDisplayTracker.m in Sources */,
42334249
15360CCF2432777500112302 /* SentrySessionTracker.m in Sources */,
42344250
6334314320AD9AE40077E581 /* SentryMechanism.m in Sources */,

SentryTestUtils/TestDisplayLinkWrapper.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import Foundation
22

33
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
4-
/// The smallest magnitude of time that is significant to how frames are classified as normal/slow/frozen.
5-
let timeEpsilon = 0.001
64

75
public enum GPUFrame {
86
case normal
@@ -25,10 +23,12 @@ public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper {
2523
public var currentFrameRate: FrameRate = .low
2624

2725
private let frozenFrameThreshold = 0.7
28-
private let slowestSlowFrameDuration: Double
29-
private let fastestFrozenFrameDuration: Double
26+
public let slowestSlowFrameDuration: Double
27+
public let fastestFrozenFrameDuration: Double
3028

3129
public var dateProvider: TestCurrentDateProvider
30+
/// The smallest magnitude of time that is significant to how frames are classified as normal/slow/frozen.
31+
public let timeEpsilon = 0.001
3232

3333
public init(dateProvider: TestCurrentDateProvider = TestCurrentDateProvider()) {
3434
self.dateProvider = dateProvider

Sources/Sentry/SentryDelayedFrame.m

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#import "SentryDelayedFrame.h"
2+
3+
#if SENTRY_HAS_UIKIT
4+
5+
NS_ASSUME_NONNULL_BEGIN
6+
7+
@implementation SentryDelayedFrame
8+
9+
- (instancetype)initWithStartTimestamp:(uint64_t)startSystemTimestamp
10+
expectedDuration:(CFTimeInterval)expectedDuration
11+
actualDuration:(CFTimeInterval)actualDuration
12+
{
13+
if (self = [super init]) {
14+
_startSystemTimestamp = startSystemTimestamp;
15+
_expectedDuration = expectedDuration;
16+
_actualDuration = actualDuration;
17+
}
18+
return self;
19+
}
20+
21+
@end
22+
23+
NS_ASSUME_NONNULL_END
24+
25+
#endif // SENTRY_HAS_UIKIT
+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#import "SentryDelayedFramesTracker.h"
2+
3+
#if SENTRY_HAS_UIKIT
4+
5+
# import "SentryCurrentDateProvider.h"
6+
# import "SentryDelayedFrame.h"
7+
# import "SentryLog.h"
8+
# import "SentryTime.h"
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
@interface
13+
SentryDelayedFramesTracker ()
14+
15+
@property (nonatomic, assign) CFTimeInterval keepDelayedFramesDuration;
16+
@property (nonatomic, strong, readonly) SentryCurrentDateProvider *dateProvider;
17+
@property (nonatomic, strong) NSMutableArray<SentryDelayedFrame *> *delayedFrames;
18+
19+
@end
20+
21+
@implementation SentryDelayedFramesTracker
22+
23+
- (instancetype)initWithKeepDelayedFramesDuration:(CFTimeInterval)keepDelayedFramesDuration
24+
dateProvider:(SentryCurrentDateProvider *)dateProvider
25+
{
26+
if (self = [super init]) {
27+
_keepDelayedFramesDuration = keepDelayedFramesDuration;
28+
_dateProvider = dateProvider;
29+
[self resetDelayedFramesTimeStamps];
30+
}
31+
return self;
32+
}
33+
34+
- (void)resetDelayedFramesTimeStamps
35+
{
36+
_delayedFrames = [NSMutableArray array];
37+
SentryDelayedFrame *initialFrame =
38+
[[SentryDelayedFrame alloc] initWithStartTimestamp:[self.dateProvider systemTime]
39+
expectedDuration:0
40+
actualDuration:0];
41+
[_delayedFrames addObject:initialFrame];
42+
}
43+
44+
- (void)recordDelayedFrame:(uint64_t)startSystemTimestamp
45+
expectedDuration:(CFTimeInterval)expectedDuration
46+
actualDuration:(CFTimeInterval)actualDuration
47+
{
48+
@synchronized(self.delayedFrames) {
49+
[self removeOldDelayedFrames];
50+
51+
SentryDelayedFrame *delayedFrame =
52+
[[SentryDelayedFrame alloc] initWithStartTimestamp:startSystemTimestamp
53+
expectedDuration:expectedDuration
54+
actualDuration:actualDuration];
55+
[self.delayedFrames addObject:delayedFrame];
56+
}
57+
}
58+
59+
/**
60+
* Removes delayed frame that are older than current time minus `keepDelayedFramesDuration`.
61+
* @note Make sure to call this in a @synchronized block.
62+
*/
63+
- (void)removeOldDelayedFrames
64+
{
65+
u_int64_t transactionMaxDurationNS = timeIntervalToNanoseconds(_keepDelayedFramesDuration);
66+
67+
uint64_t removeFramesBeforeSystemTimeStamp
68+
= _dateProvider.systemTime - transactionMaxDurationNS;
69+
if (_dateProvider.systemTime < transactionMaxDurationNS) {
70+
removeFramesBeforeSystemTimeStamp = 0;
71+
}
72+
73+
NSUInteger left = 0;
74+
NSUInteger right = self.delayedFrames.count;
75+
76+
while (left < right) {
77+
NSUInteger mid = (left + right) / 2;
78+
SentryDelayedFrame *midFrame = self.delayedFrames[mid];
79+
80+
uint64_t frameEndSystemTimeStamp
81+
= midFrame.startSystemTimestamp + timeIntervalToNanoseconds(midFrame.actualDuration);
82+
if (frameEndSystemTimeStamp >= removeFramesBeforeSystemTimeStamp) {
83+
right = mid;
84+
} else {
85+
left = mid + 1;
86+
}
87+
}
88+
89+
[self.delayedFrames removeObjectsInRange:NSMakeRange(0, left)];
90+
}
91+
92+
- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
93+
endSystemTimestamp:(uint64_t)endSystemTimestamp
94+
isRunning:(BOOL)isRunning
95+
thisFrameTimestamp:(CFTimeInterval)thisFrameTimestamp
96+
previousFrameTimestamp:(CFTimeInterval)previousFrameTimestamp
97+
slowFrameThreshold:(CFTimeInterval)slowFrameThreshold
98+
{
99+
CFTimeInterval cantCalculateFrameDelayReturnValue = -1.0;
100+
101+
if (isRunning == NO) {
102+
SENTRY_LOG_DEBUG(@"Not calculating frames delay because frames tracker isn't running.");
103+
return cantCalculateFrameDelayReturnValue;
104+
}
105+
106+
if (startSystemTimestamp >= endSystemTimestamp) {
107+
SENTRY_LOG_DEBUG(@"Not calculating frames delay because startSystemTimestamp is before "
108+
@"endSystemTimestamp");
109+
return cantCalculateFrameDelayReturnValue;
110+
}
111+
112+
if (endSystemTimestamp > self.dateProvider.systemTime) {
113+
SENTRY_LOG_DEBUG(
114+
@"Not calculating frames delay because endSystemTimestamp is in the future.");
115+
return cantCalculateFrameDelayReturnValue;
116+
}
117+
118+
if (previousFrameTimestamp < 0) {
119+
SENTRY_LOG_DEBUG(@"Not calculating frames delay because no frames yet recorded.");
120+
return cantCalculateFrameDelayReturnValue;
121+
}
122+
123+
NSArray<SentryDelayedFrame *> *frames;
124+
@synchronized(self.delayedFrames) {
125+
uint64_t oldestDelayedFrameStartTimestamp = UINT64_MAX;
126+
SentryDelayedFrame *oldestDelayedFrame = self.delayedFrames.firstObject;
127+
if (oldestDelayedFrame != nil) {
128+
oldestDelayedFrameStartTimestamp = oldestDelayedFrame.startSystemTimestamp;
129+
}
130+
131+
if (oldestDelayedFrameStartTimestamp > startSystemTimestamp) {
132+
SENTRY_LOG_DEBUG(@"Not calculating frames delay because the record of delayed frames "
133+
@"doesn't go back enough in time.");
134+
return cantCalculateFrameDelayReturnValue;
135+
}
136+
137+
// Copy as late as possible to avoid allocating unnecessary memory.
138+
frames = self.delayedFrames.copy;
139+
}
140+
141+
// Check if there is an delayed frame going on but not recorded yet.
142+
CFTimeInterval frameDuration = thisFrameTimestamp - previousFrameTimestamp;
143+
CFTimeInterval ongoingDelayedFrame = 0.0;
144+
if (frameDuration > slowFrameThreshold) {
145+
ongoingDelayedFrame = frameDuration - slowFrameThreshold;
146+
}
147+
148+
CFTimeInterval delay = ongoingDelayedFrame;
149+
150+
// We need to calculate the intersections of the queried TimestampInterval
151+
// (startSystemTimestamp - endSystemTimestamp) with the recorded frame delays. Doing that
152+
// with NSDateInterval makes things easier. Therefore, we convert the system timestamps to
153+
// NSDate objects, although they don't represent the correct dates. We only need to know how
154+
// long the intersections are to calculate the frame delay and not precisely when.
155+
156+
NSDate *startDate = [NSDate
157+
dateWithTimeIntervalSinceReferenceDate:nanosecondsToTimeInterval(startSystemTimestamp)];
158+
NSDate *endDate = [NSDate
159+
dateWithTimeIntervalSinceReferenceDate:nanosecondsToTimeInterval(endSystemTimestamp)];
160+
NSDateInterval *queryDateInterval = [[NSDateInterval alloc] initWithStartDate:startDate
161+
endDate:endDate];
162+
163+
// Iterate in reverse order, as younger frame delays are more likely to match the queried
164+
// period.
165+
for (SentryDelayedFrame *frame in frames.reverseObjectEnumerator) {
166+
167+
uint64_t frameEndSystemTimeStamp
168+
= frame.startSystemTimestamp + timeIntervalToNanoseconds(frame.actualDuration);
169+
if (frameEndSystemTimeStamp < startSystemTimestamp) {
170+
break;
171+
}
172+
173+
CFTimeInterval delayStartTime
174+
= nanosecondsToTimeInterval(frame.startSystemTimestamp) + frame.expectedDuration;
175+
NSDate *frameDelayStartDate =
176+
[NSDate dateWithTimeIntervalSinceReferenceDate:delayStartTime];
177+
178+
NSDateInterval *frameDelayDateInterval = [[NSDateInterval alloc]
179+
initWithStartDate:frameDelayStartDate
180+
duration:(frame.actualDuration - frame.expectedDuration)];
181+
182+
if ([queryDateInterval intersectsDateInterval:frameDelayDateInterval]) {
183+
NSDateInterval *intersection =
184+
[queryDateInterval intersectionWithDateInterval:frameDelayDateInterval];
185+
delay = delay + intersection.duration;
186+
}
187+
}
188+
189+
return delay;
190+
}
191+
192+
@end
193+
194+
NS_ASSUME_NONNULL_END
195+
196+
#endif // SENTRY_HAS_UIKIT

Sources/Sentry/SentryDependencyContainer.m

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#import <SentrySwizzleWrapper.h>
2727
#import <SentrySysctl.h>
2828
#import <SentryThreadWrapper.h>
29+
#import <SentryTracer.h>
2930

3031
#if SENTRY_HAS_UIKIT
3132
# import "SentryFramesTracker.h"
@@ -268,7 +269,9 @@ - (SentryFramesTracker *)framesTracker
268269
@synchronized(sentryDependencyContainerLock) {
269270
if (_framesTracker == nil) {
270271
_framesTracker = [[SentryFramesTracker alloc]
271-
initWithDisplayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]];
272+
initWithDisplayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]
273+
dateProvider:self.dateProvider
274+
keepDelayedFramesDuration:SENTRY_AUTO_TRANSACTION_MAX_DURATION];
272275
}
273276
}
274277
}

0 commit comments

Comments
 (0)