-
-
Notifications
You must be signed in to change notification settings - Fork 352
fix(session-replay): improve multi-threading of session replay processing #5018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
676b276
2fda9c4
aa26915
a27d74d
23c6556
8634126
3ff7c84
a3d948b
44c4af7
c0e828d
773fcad
0e9c3e2
c4ba77c
c5c78ca
ef3d279
51beda1
1787110
8614798
bdfd4bc
e63bbeb
0cfab38
015e2ee
e1ef89d
d723f27
168bb3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,6 +57,9 @@ @implementation SentrySessionReplayIntegration { | |
// This is the easiest way to ensure segment 0 will always reach the server, because session | ||
// replay absolutely needs segment 0 to make replay work. | ||
BOOL _rateLimited; | ||
id<SentryCurrentDateProvider> _dateProvider; | ||
SentryDispatchQueueWrapper *_replayProcessingQueue; | ||
SentryDispatchQueueWrapper *_replayAssetWorkerQueue; | ||
} | ||
|
||
- (instancetype)init | ||
|
@@ -120,6 +123,19 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions | |
} | ||
|
||
_notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; | ||
_dateProvider = SentryDependencyContainer.sharedInstance.dateProvider; | ||
|
||
// The asset worker queue is used to work on video and frames data. | ||
// Use a relative priority of -1 to make it lower than the default background priority. | ||
_replayAssetWorkerQueue = [SentryDispatchQueueWrapper | ||
createBackgroundDispatchQueueWithName:"io.sentry.session-replay.asset-worker" | ||
relativePriority:-1]; | ||
// The dispatch queue is used to asynchronously wait for the asset worker queue to finish its | ||
// work. To avoid a deadlock, the priority of the processing queue must be lower than the asset | ||
// worker queue. Use a relative priority of -2 to make it lower than the asset worker queue. | ||
_replayProcessingQueue = [SentryDispatchQueueWrapper | ||
createBackgroundDispatchQueueWithName:"io.sentry.session-replay.processing" | ||
Comment on lines
+130
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
relativePriority:-2]; | ||
|
||
[self moveCurrentReplay]; | ||
[self cleanUp]; | ||
|
@@ -189,30 +205,54 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event | |
} | ||
|
||
SentryOnDemandReplay *resumeReplayMaker = | ||
[[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; | ||
[[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path | ||
processingQueue:_replayProcessingQueue | ||
assetWorkerQueue:_replayAssetWorkerQueue | ||
dateProvider:_dateProvider]; | ||
resumeReplayMaker.bitRate = _replayOptions.replayBitRate; | ||
resumeReplayMaker.videoScale = _replayOptions.sizeScale; | ||
|
||
NSDate *beginning = hasCrashInfo | ||
? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] | ||
: [resumeReplayMaker oldestFrameDate]; | ||
|
||
if (beginning == nil) { | ||
return; // no frames to send | ||
} | ||
|
||
SentryReplayType _type = type; | ||
int _segmentId = segmentId; | ||
|
||
NSError *error; | ||
NSArray<SentryVideoInfo *> *videos = | ||
[resumeReplayMaker createVideoWithBeginning:beginning | ||
end:[beginning dateByAddingTimeInterval:duration] | ||
error:&error]; | ||
NSDate *end = [beginning dateByAddingTimeInterval:duration]; | ||
|
||
// This method is called from a background thread, so we can synchronize the creation of the | ||
// video with a dispatch group. | ||
__block NSArray<SentryVideoInfo *> *videos; | ||
__block NSError *_Nullable error; | ||
|
||
dispatch_group_t group = dispatch_group_create(); | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
dispatch_group_enter(group); | ||
[resumeReplayMaker | ||
createVideoAsyncWithBeginning:beginning | ||
end:end | ||
completion:^(NSArray<SentryVideoInfo *> *_Nullable resultVideos, | ||
NSError *_Nullable resultError) { | ||
videos = resultVideos; | ||
error = resultError; | ||
dispatch_group_leave(group); | ||
}]; | ||
// Wait for the video creation to finish without a timeout, because the video creation is | ||
// expected to finish in a reasonable time frame. | ||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); | ||
|
||
// Either error or videos should be set. | ||
if (error != nil) { | ||
SENTRY_LOG_ERROR(@"Could not create replay video, reason: %@", error); | ||
return; | ||
} | ||
if (videos == nil) { | ||
SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); | ||
SENTRY_LOG_ERROR(@"Could not create replay video, reason: no videos available"); | ||
return; | ||
} | ||
|
||
// For each segment we need to create a new event with the video. | ||
int _segmentId = segmentId; | ||
SentryReplayType _type = type; | ||
for (SentryVideoInfo *video in videos) { | ||
[self captureVideo:video replayId:replayId segmentId:_segmentId++ type:_type]; | ||
// type buffer is only for the first segment | ||
|
@@ -224,8 +264,10 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event | |
[NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil]; | ||
event.context = eventContext; | ||
|
||
if ([NSFileManager.defaultManager removeItemAtURL:lastReplayURL error:&error] == NO) { | ||
SENTRY_LOG_ERROR(@"Can`t delete '%@': %@", SENTRY_LAST_REPLAY, error); | ||
NSError *_Nullable removeError; | ||
BOOL result = [NSFileManager.defaultManager removeItemAtURL:lastReplayURL error:&removeError]; | ||
if (result == NO) { | ||
SENTRY_LOG_ERROR(@"Can`t delete '%@': %@", SENTRY_LAST_REPLAY, removeError); | ||
} | ||
} | ||
|
||
|
@@ -316,30 +358,27 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions | |
error:nil]; | ||
} | ||
|
||
SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; | ||
SentryOnDemandReplay *replayMaker = | ||
[[SentryOnDemandReplay alloc] initWithOutputPath:docs.path | ||
processingQueue:_replayProcessingQueue | ||
assetWorkerQueue:_replayAssetWorkerQueue | ||
dateProvider:_dateProvider]; | ||
replayMaker.bitRate = replayOptions.replayBitRate; | ||
replayMaker.videoScale = replayOptions.sizeScale; | ||
replayMaker.cacheMaxSize | ||
= (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1 | ||
: replayOptions.errorReplayDuration + 1); | ||
|
||
dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_LOW, 0); | ||
SentryDispatchQueueWrapper *dispatchQueue = | ||
[[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.session-replay" | ||
attributes:attributes]; | ||
|
||
self.sessionReplay = [[SentrySessionReplay alloc] | ||
initWithReplayOptions:replayOptions | ||
replayFolderPath:docs | ||
screenshotProvider:screenshotProvider | ||
replayMaker:replayMaker | ||
breadcrumbConverter:breadcrumbConverter | ||
touchTracker:_touchTracker | ||
dateProvider:SentryDependencyContainer.sharedInstance.dateProvider | ||
delegate:self | ||
dispatchQueue:dispatchQueue | ||
displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; | ||
SentryDisplayLinkWrapper *displayLinkWrapper = [[SentryDisplayLinkWrapper alloc] init]; | ||
self.sessionReplay = [[SentrySessionReplay alloc] initWithReplayOptions:replayOptions | ||
replayFolderPath:docs | ||
screenshotProvider:screenshotProvider | ||
replayMaker:replayMaker | ||
breadcrumbConverter:breadcrumbConverter | ||
touchTracker:_touchTracker | ||
dateProvider:_dateProvider | ||
delegate:self | ||
displayLinkWrapper:displayLinkWrapper]; | ||
|
||
[self.sessionReplay | ||
startWithRootView:SentryDependencyContainer.sharedInstance.application.windows.firstObject | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
h
: That's so easy to break. If we ever change the priority of the default background priority here, we could end up with the same problem again. And when I change the priority in theSentryDispatchQueueWrapper
how should I know that I I need to change it here too?sentry-cocoa/Sources/Sentry/SentryDispatchQueueWrapper.m
Lines 10 to 13 in 8856e5a
A test should fail if these priorities don't match our requirements here. If you need ideas for testing this properly, please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would highly appreciate an example how you would test the priorities of queues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a factory method that I can swap out in the tests or something like that. Then I can assert that I call the factory with the correct parameters.