-
-
Notifications
You must be signed in to change notification settings - Fork 323
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add frames delay to transactions (#3487)
Add frames delay as span data to transactions.
- Loading branch information
1 parent
f25febb
commit de033da
Showing
17 changed files
with
847 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
#import "SentryDelayedFrame.h" | ||
|
||
#if SENTRY_HAS_UIKIT | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@implementation SentryDelayedFrame | ||
|
||
- (instancetype)initWithStartTimestamp:(uint64_t)startSystemTimestamp | ||
expectedDuration:(CFTimeInterval)expectedDuration | ||
actualDuration:(CFTimeInterval)actualDuration | ||
{ | ||
if (self = [super init]) { | ||
_startSystemTimestamp = startSystemTimestamp; | ||
_expectedDuration = expectedDuration; | ||
_actualDuration = actualDuration; | ||
} | ||
return self; | ||
} | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END | ||
|
||
#endif // SENTRY_HAS_UIKIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
#import "SentryDelayedFramesTracker.h" | ||
|
||
#if SENTRY_HAS_UIKIT | ||
|
||
# import "SentryCurrentDateProvider.h" | ||
# import "SentryDelayedFrame.h" | ||
# import "SentryLog.h" | ||
# import "SentryTime.h" | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@interface | ||
SentryDelayedFramesTracker () | ||
|
||
@property (nonatomic, assign) CFTimeInterval keepDelayedFramesDuration; | ||
@property (nonatomic, strong, readonly) SentryCurrentDateProvider *dateProvider; | ||
@property (nonatomic, strong) NSMutableArray<SentryDelayedFrame *> *delayedFrames; | ||
|
||
@end | ||
|
||
@implementation SentryDelayedFramesTracker | ||
|
||
- (instancetype)initWithKeepDelayedFramesDuration:(CFTimeInterval)keepDelayedFramesDuration | ||
dateProvider:(SentryCurrentDateProvider *)dateProvider | ||
{ | ||
if (self = [super init]) { | ||
_keepDelayedFramesDuration = keepDelayedFramesDuration; | ||
_dateProvider = dateProvider; | ||
[self resetDelayedFramesTimeStamps]; | ||
} | ||
return self; | ||
} | ||
|
||
- (void)resetDelayedFramesTimeStamps | ||
{ | ||
_delayedFrames = [NSMutableArray array]; | ||
SentryDelayedFrame *initialFrame = | ||
[[SentryDelayedFrame alloc] initWithStartTimestamp:[self.dateProvider systemTime] | ||
expectedDuration:0 | ||
actualDuration:0]; | ||
[_delayedFrames addObject:initialFrame]; | ||
} | ||
|
||
- (void)recordDelayedFrame:(uint64_t)startSystemTimestamp | ||
expectedDuration:(CFTimeInterval)expectedDuration | ||
actualDuration:(CFTimeInterval)actualDuration | ||
{ | ||
@synchronized(self.delayedFrames) { | ||
[self removeOldDelayedFrames]; | ||
|
||
SentryDelayedFrame *delayedFrame = | ||
[[SentryDelayedFrame alloc] initWithStartTimestamp:startSystemTimestamp | ||
expectedDuration:expectedDuration | ||
actualDuration:actualDuration]; | ||
[self.delayedFrames addObject:delayedFrame]; | ||
} | ||
} | ||
|
||
/** | ||
* Removes delayed frame that are older than current time minus `keepDelayedFramesDuration`. | ||
* @note Make sure to call this in a @synchronized block. | ||
*/ | ||
- (void)removeOldDelayedFrames | ||
{ | ||
u_int64_t transactionMaxDurationNS = timeIntervalToNanoseconds(_keepDelayedFramesDuration); | ||
|
||
uint64_t removeFramesBeforeSystemTimeStamp | ||
= _dateProvider.systemTime - transactionMaxDurationNS; | ||
if (_dateProvider.systemTime < transactionMaxDurationNS) { | ||
removeFramesBeforeSystemTimeStamp = 0; | ||
} | ||
|
||
NSUInteger left = 0; | ||
NSUInteger right = self.delayedFrames.count; | ||
|
||
while (left < right) { | ||
NSUInteger mid = (left + right) / 2; | ||
SentryDelayedFrame *midFrame = self.delayedFrames[mid]; | ||
|
||
uint64_t frameEndSystemTimeStamp | ||
= midFrame.startSystemTimestamp + timeIntervalToNanoseconds(midFrame.actualDuration); | ||
if (frameEndSystemTimeStamp >= removeFramesBeforeSystemTimeStamp) { | ||
right = mid; | ||
} else { | ||
left = mid + 1; | ||
} | ||
} | ||
|
||
[self.delayedFrames removeObjectsInRange:NSMakeRange(0, left)]; | ||
} | ||
|
||
- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp | ||
endSystemTimestamp:(uint64_t)endSystemTimestamp | ||
isRunning:(BOOL)isRunning | ||
thisFrameTimestamp:(CFTimeInterval)thisFrameTimestamp | ||
previousFrameTimestamp:(CFTimeInterval)previousFrameTimestamp | ||
slowFrameThreshold:(CFTimeInterval)slowFrameThreshold | ||
{ | ||
CFTimeInterval cantCalculateFrameDelayReturnValue = -1.0; | ||
|
||
if (isRunning == NO) { | ||
SENTRY_LOG_DEBUG(@"Not calculating frames delay because frames tracker isn't running."); | ||
return cantCalculateFrameDelayReturnValue; | ||
} | ||
|
||
if (startSystemTimestamp >= endSystemTimestamp) { | ||
SENTRY_LOG_DEBUG(@"Not calculating frames delay because startSystemTimestamp is before " | ||
@"endSystemTimestamp"); | ||
return cantCalculateFrameDelayReturnValue; | ||
} | ||
|
||
if (endSystemTimestamp > self.dateProvider.systemTime) { | ||
SENTRY_LOG_DEBUG( | ||
@"Not calculating frames delay because endSystemTimestamp is in the future."); | ||
return cantCalculateFrameDelayReturnValue; | ||
} | ||
|
||
if (previousFrameTimestamp < 0) { | ||
SENTRY_LOG_DEBUG(@"Not calculating frames delay because no frames yet recorded."); | ||
return cantCalculateFrameDelayReturnValue; | ||
} | ||
|
||
NSArray<SentryDelayedFrame *> *frames; | ||
@synchronized(self.delayedFrames) { | ||
uint64_t oldestDelayedFrameStartTimestamp = UINT64_MAX; | ||
SentryDelayedFrame *oldestDelayedFrame = self.delayedFrames.firstObject; | ||
if (oldestDelayedFrame != nil) { | ||
oldestDelayedFrameStartTimestamp = oldestDelayedFrame.startSystemTimestamp; | ||
} | ||
|
||
if (oldestDelayedFrameStartTimestamp > startSystemTimestamp) { | ||
SENTRY_LOG_DEBUG(@"Not calculating frames delay because the record of delayed frames " | ||
@"doesn't go back enough in time."); | ||
return cantCalculateFrameDelayReturnValue; | ||
} | ||
|
||
// Copy as late as possible to avoid allocating unnecessary memory. | ||
frames = self.delayedFrames.copy; | ||
} | ||
|
||
// Check if there is an delayed frame going on but not recorded yet. | ||
CFTimeInterval frameDuration = thisFrameTimestamp - previousFrameTimestamp; | ||
CFTimeInterval ongoingDelayedFrame = 0.0; | ||
if (frameDuration > slowFrameThreshold) { | ||
ongoingDelayedFrame = frameDuration - slowFrameThreshold; | ||
} | ||
|
||
CFTimeInterval delay = ongoingDelayedFrame; | ||
|
||
// We need to calculate the intersections of the queried TimestampInterval | ||
// (startSystemTimestamp - endSystemTimestamp) with the recorded frame delays. Doing that | ||
// with NSDateInterval makes things easier. Therefore, we convert the system timestamps to | ||
// NSDate objects, although they don't represent the correct dates. We only need to know how | ||
// long the intersections are to calculate the frame delay and not precisely when. | ||
|
||
NSDate *startDate = [NSDate | ||
dateWithTimeIntervalSinceReferenceDate:nanosecondsToTimeInterval(startSystemTimestamp)]; | ||
NSDate *endDate = [NSDate | ||
dateWithTimeIntervalSinceReferenceDate:nanosecondsToTimeInterval(endSystemTimestamp)]; | ||
NSDateInterval *queryDateInterval = [[NSDateInterval alloc] initWithStartDate:startDate | ||
endDate:endDate]; | ||
|
||
// Iterate in reverse order, as younger frame delays are more likely to match the queried | ||
// period. | ||
for (SentryDelayedFrame *frame in frames.reverseObjectEnumerator) { | ||
|
||
uint64_t frameEndSystemTimeStamp | ||
= frame.startSystemTimestamp + timeIntervalToNanoseconds(frame.actualDuration); | ||
if (frameEndSystemTimeStamp < startSystemTimestamp) { | ||
break; | ||
} | ||
|
||
CFTimeInterval delayStartTime | ||
= nanosecondsToTimeInterval(frame.startSystemTimestamp) + frame.expectedDuration; | ||
NSDate *frameDelayStartDate = | ||
[NSDate dateWithTimeIntervalSinceReferenceDate:delayStartTime]; | ||
|
||
NSDateInterval *frameDelayDateInterval = [[NSDateInterval alloc] | ||
initWithStartDate:frameDelayStartDate | ||
duration:(frame.actualDuration - frame.expectedDuration)]; | ||
|
||
if ([queryDateInterval intersectsDateInterval:frameDelayDateInterval]) { | ||
NSDateInterval *intersection = | ||
[queryDateInterval intersectionWithDateInterval:frameDelayDateInterval]; | ||
delay = delay + intersection.duration; | ||
} | ||
} | ||
|
||
return delay; | ||
} | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END | ||
|
||
#endif // SENTRY_HAS_UIKIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.