Skip to content

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

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
676b276
fix(session-replay): change multi-threading of session replay processing
philprime Mar 25, 2025
2fda9c4
small fixes
philprime Mar 25, 2025
aa26915
improvements
philprime Mar 25, 2025
a27d74d
WIP
philprime Mar 25, 2025
23c6556
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 11, 2025
8634126
wip
philprime Apr 11, 2025
3ff7c84
add changelog
philprime Apr 11, 2025
a3d948b
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 11, 2025
44c4af7
Apply suggestions from code review
philprime Apr 11, 2025
c0e828d
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 24, 2025
773fcad
reverting changes
philprime Apr 24, 2025
0e9c3e2
small changes
philprime Apr 24, 2025
c4ba77c
WIP
philprime Apr 24, 2025
c5c78ca
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 25, 2025
ef3d279
fix merge error
philprime Apr 25, 2025
51beda1
small fixes
philprime Apr 25, 2025
1787110
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 25, 2025
8614798
small reverts
philprime Apr 25, 2025
bdfd4bc
small reverts
philprime Apr 25, 2025
e63bbeb
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 30, 2025
0cfab38
fix sync filtering
philprime Apr 30, 2025
015e2ee
Added more logs and tests; moved types to own files due to linter war…
philprime Apr 30, 2025
e1ef89d
removed unused type
philprime Apr 30, 2025
d723f27
fix tests
philprime Apr 30, 2025
168bb3d
Merge remote-tracking branch 'origin/main' into philprime/session-rep…
philprime Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct SentrySDKWrapper {
options.debug = true

if #available(iOS 16.0, *), enableSessionReplay {
options.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true)
options.sessionReplay = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true)
options.sessionReplay.quality = .high
}

Expand Down
8 changes: 8 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,8 @@
D43B26D62D70964C007747FD /* SentrySpanOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D43B26D52D709648007747FD /* SentrySpanOperation.m */; };
D43B26D82D70A550007747FD /* SentryTraceOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = D43B26D72D70A54A007747FD /* SentryTraceOrigin.m */; };
D43B26DA2D70A612007747FD /* SentrySpanDataKey.m in Sources */ = {isa = PBXBuildFile; fileRef = D43B26D92D70A60E007747FD /* SentrySpanDataKey.m */; };
D451ED5D2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D451ED5C2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift */; };
D451ED5F2D92ECDE00C9BEA8 /* SentryReplayFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = D451ED5E2D92ECDE00C9BEA8 /* SentryReplayFrame.swift */; };
D456B4322D706BDF007068CB /* SentrySpanOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4312D706BDD007068CB /* SentrySpanOperation.h */; };
D456B4362D706BF2007068CB /* SentryTraceOrigin.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4352D706BEE007068CB /* SentryTraceOrigin.h */; };
D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4372D706BFB007068CB /* SentrySpanDataKey.h */; };
Expand Down Expand Up @@ -1977,6 +1979,8 @@
D43B26D52D709648007747FD /* SentrySpanOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpanOperation.m; sourceTree = "<group>"; };
D43B26D72D70A54A007747FD /* SentryTraceOrigin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceOrigin.m; sourceTree = "<group>"; };
D43B26D92D70A60E007747FD /* SentrySpanDataKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpanDataKey.m; sourceTree = "<group>"; };
D451ED5C2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayError.swift; sourceTree = "<group>"; };
D451ED5E2D92ECDE00C9BEA8 /* SentryReplayFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayFrame.swift; sourceTree = "<group>"; };
D456B4312D706BDD007068CB /* SentrySpanOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanOperation.h; path = include/SentrySpanOperation.h; sourceTree = "<group>"; };
D456B4352D706BEE007068CB /* SentryTraceOrigin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTraceOrigin.h; path = include/SentryTraceOrigin.h; sourceTree = "<group>"; };
D456B4372D706BFB007068CB /* SentrySpanDataKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanDataKey.h; path = include/SentrySpanDataKey.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4259,6 +4263,8 @@
D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */,
D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */,
D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */,
D451ED5E2D92ECDE00C9BEA8 /* SentryReplayFrame.swift */,
D451ED5C2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift */,
D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */,
D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */,
D8F67B1A2BE9728600C9197B /* SentrySRDefaultBreadcrumbConverter.swift */,
Expand Down Expand Up @@ -5028,6 +5034,7 @@
7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */,
63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */,
844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */,
D451ED5F2D92ECDE00C9BEA8 /* SentryReplayFrame.swift in Sources */,
D8739CF92BECFFB5007D2F66 /* SentryTransactionNameSource.swift in Sources */,
630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */,
6281C5722D3E4F12009D0978 /* DecodeArbitraryData.swift in Sources */,
Expand Down Expand Up @@ -5120,6 +5127,7 @@
D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */,
7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */,
63BE85711ECEC6DE00DC44F5 /* SentryDateUtils.m in Sources */,
D451ED5D2D92ECD200C9BEA8 /* SentryOnDemandReplayError.swift in Sources */,
D4E829D82D75E57900D375AD /* SentryMaskRenderer.swift in Sources */,
7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */,
628308612D50ADAC00EAEF77 /* SentryRequestCodable.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions Sources/Sentry/SentryDispatchQueueWrapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ - (nullable dispatch_block_t)createDispatchBlock:(void (^)(void))block
return dispatch_block_create(0, block);
}

+ (SentryDispatchQueueWrapper *)createBackgroundDispatchQueueWithName:(const char *)name
relativePriority:(int)relativePriority

{
dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, QOS_CLASS_BACKGROUND, relativePriority);
return [[SentryDispatchQueueWrapper alloc] initWithName:name attributes:attributes];
}
@end

NS_ASSUME_NONNULL_END
103 changes: 71 additions & 32 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
Comment on lines +128 to +132
Copy link
Member

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 the SentryDispatchQueueWrapper how should I know that I I need to change it here too?

// DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL is requires iOS 10. Since we are targeting
// iOS 9 we need to manually add the autoreleasepool.
dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h: The comment mentions that setting the wrong priorities here can lead to a deadlock. I think it's essential to have a test fail when using the wrong priorities to avoid regressions. We could assert that the SentryDispatchQueueWrapper is initialized with the correct attributes, which tests implementation details, and has the downside of having to change the test when the implementation details change. We could also have a test causing a deadlock when interacting with this class when the priorities are wrongly set, which could be a bit harder to achieve, but has the advantage of not testing implementation details.

relativePriority:-2];

[self moveCurrentReplay];
[self cleanUp];
Expand Down Expand Up @@ -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();
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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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(
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
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryDispatchQueueWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ NS_ASSUME_NONNULL_BEGIN

- (nullable dispatch_block_t)createDispatchBlock:(void (^)(void))block;

+ (SentryDispatchQueueWrapper *)createBackgroundDispatchQueueWithName:(const char *)name
relativePriority:(int)relativePriority;
@end

NS_ASSUME_NONNULL_END
Loading
Loading