-
Notifications
You must be signed in to change notification settings - Fork 222
Swift 6: complete concurrency checking (LLC and UIKit) #3661
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: develop
Are you sure you want to change the base?
Conversation
# Conflicts: # Sources/StreamChat/WebSocketClient/BackgroundTaskScheduler.swift
# Conflicts: # Sources/StreamChat/Models/CurrentUser.swift # Sources/StreamChat/Workers/CurrentUserUpdater.swift
Generated by 🚫 Danger |
.github/workflows/smoke-checks.yml
Outdated
if: ${{ github.event.inputs.record_snapshots != 'true' }} | ||
# if: ${{ github.event.inputs.record_snapshots != 'true' }} | ||
if: false # disable Xcode 15 builds |
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.
Disabled Xcode 15 builds (it is hard to support it when complete concurrency checking is enabled). Hard means avoiding compiler crashes.
SDK Size
|
@@ -26,7 +26,7 @@ final class StreamAudioWaveformAnalyser: AudioAnalysing { | |||
private let audioSamplesExtractor: AudioSamplesExtractor | |||
private let audioSamplesProcessor: AudioSamplesProcessor | |||
private let audioSamplesPercentageNormaliser: AudioValuePercentageNormaliser | |||
private let outputSettings: [String: Any] | |||
nonisolated(unsafe) private let outputSettings: [String: Any] |
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.
nonisolated(unsafe)
because value is Any
SDK Performance
|
|
||
import Foundation | ||
|
||
/// Erase type for structs which recursively contain themselves. |
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.
It is easier to use a type instead of a closure when dealing with Sendable.
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.
yes, this is a nice approach
@@ -131,15 +131,19 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { | |||
open func play() { | |||
do { | |||
try audioSessionConfigurator.activatePlaybackSession() | |||
player.play() | |||
MainActor.ensureIsolated { | |||
player.play() |
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.
Note that AVPlayer and some asset related methods require main actor
|
||
// MARK: - | ||
|
||
private static let queue = DispatchQueue(label: "io.getstream.stream-runtime-check", target: .global()) |
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.
Alternative is to skip using queue here and making everything nonisolated(unsafe), especially because these are internal runtime checkes.
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 do that without a queue here actually
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'll revert, makes sense
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.
Looks good in general, left some comments to discuss
|
||
// MARK: - | ||
|
||
private static let queue = DispatchQueue(label: "io.getstream.stream-runtime-check", target: .global()) |
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 do that without a queue here actually
static var maxAttachmentSize: Int64 { 100 * 1024 * 1024 } | ||
|
||
private let decoder: RequestDecoder | ||
private let encoder: RequestEncoder | ||
private let session: URLSession | ||
/// Keeps track of uploading tasks progress | ||
@Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:] |
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.
did it complain about Atomic?
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.
Stored property '_taskProgressObservers' of 'Sendable'-conforming class 'StreamCDNClient' is mutable
Alternative is to use backwards compatible AllocatedUnfairLock
(I added it for mutable static properties).
@Atomic
is OK in many places because the class/type is @unchecked Sendable
. In this particular case it is directly Sendable
.
Property wrappers can't ensure concurrency safeness (there is a long thread about it on Swift forums)
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.
It does look cleaner:
/// Keeps track of uploading tasks progress
- private nonisolated(unsafe) var _taskProgressObservers: [Int: NSKeyValueObservation] = [:]
- private let queue = DispatchQueue(label: "io.getstream.stream-cdn-client", target: .global())
+ private let taskProgressObservers = AllocatedUnfairLock([Int: NSKeyValueObservation]())
init(
encoder: RequestEncoder,
@@ -149,13 +148,13 @@ final class StreamCDNClient: CDNClient, Sendable {
if let progressListener = progress {
let taskID = task.taskIdentifier
- queue.async {
- self._taskProgressObservers[taskID] = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
+ taskProgressObservers.withLock { observers in
+ observers[taskID] = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
progressListener(progress.fractionCompleted)
if progress.isFinished || progress.isCancelled {
- self?.queue.async { [weak self] in
- self?._taskProgressObservers[taskID]?.invalidate()
- self?._taskProgressObservers[taskID] = nil
+ self?.taskProgressObservers.withLock { observers in
+ observers[taskID]?.invalidate()
+ observers[taskID] = nil
}
}
}
WDYT, let's use the snippet above?
@@ -131,15 +131,19 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate { | |||
open func play() { | |||
do { | |||
try audioSessionConfigurator.activatePlaybackSession() | |||
player.play() | |||
MainActor.ensureIsolated { |
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.
mentioned it on the other PR as well - why do we need to use ensureIsolated
?
Sources/StreamChat/Controllers/EventsController/EventsController.swift
Outdated
Show resolved
Hide resolved
import os | ||
|
||
@available(iOS, introduced: 13.0, deprecated: 16.0, message: "Use OSAllocatedUnfairLock instead") | ||
final class AllocatedUnfairLock<State>: @unchecked Sendable { |
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.
where do we use an unfair lock?
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.
|
||
import Foundation | ||
|
||
/// Erase type for structs which recursively contain themselves. |
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.
yes, this is a nice approach
try action() | ||
} else { | ||
try DispatchQueue.main.sync { | ||
return try MainActor.assumeIsolated { |
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.
why is this needed?
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 can remove it, I think it was Xcode 15 which did not understand that this is OK (different compiler version). It is OK now.
_endIndex = { baseCollection.endIndex } | ||
_position = { baseCollection[$0] } | ||
_startIndex = { baseCollection.startIndex } | ||
self.baseCollection = Array(baseCollection) |
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.
why do we change this?
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.
Stored property '_endIndex' of 'Sendable'-conforming generic struct 'StreamCollection' has non-sendable type '() -> StreamCollection<Element>.Index' (aka '() -> Int')
At first I tried to make closures @Sendable
, which requires BaseCollection
to be Sendable
. All good until I saw that StreamCollection is in some cases initialized with a list of MessageDTO
. MessageDTO
can't be Sendable
, therefore I can't do this.
Fortunately we want to remove StreamCollection
in v5 anyway.
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.
Reviewed it and don't have a better alternative to offer here.
pongTimeoutTimer = timerType.schedule(timeInterval: Self.pongTimeoutTimeInterval, queue: timerQueue) { [weak self] in | ||
log.info("WebSocket Pong timeout. Reconnect") | ||
self?.delegate?.disconnectOnNoPongReceived() | ||
queue.sync { |
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.
why is this needed?
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.
Problem here is that connectionDidChange is called from thread X and timer calls sendPing
from thread Y which in turn will also access the _pingTimerControl
(possible threading issue).
AllocatedUnfairLock would be more readable here.
Since we haven't heard about crashes with ping controller, I am open to keep it as is as well and not do these changes.
…here sync is required, use a LLC style DispatchQueue addition
… but where sync is required, use a LLC style DispatchQueue addition" This reverts commit e5b15f1.
# Conflicts: # Sources/StreamChat/Repositories/MessageRepository.swift
Important Review skippedMore than 25% of the files skipped due to max files limit. The review is being skipped to prevent a low-quality review. 193 files out of 300 files are above the max files limit of 100. Please upgrade to Pro plan to get higher limits. You can disable this status message by setting the ✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
# Conflicts: # .github/workflows/smoke-checks.yml # Sources/StreamChat/Repositories/AuthenticationRepository.swift
Important
StreamChatUI changes are in #3660 and will be merged to here. This PR can be reviewed already.
🔗 Issue Links
Resolves IOS-735
🎯 Goal
📝 Summary
Sendable
conformance to types@Sendable
to completion handlers@unchecked Sendable
types (internal classes have manual handling and found many which did not guard that state properly)@Sendable
to completion handlers creates warnings at callsites)🛠 Implementation
🧪 Manual Testing Notes
Manual regression testing round when UIKit changes are merged into this branch.
☑️ Contributor Checklist
docs-content
repo