Conversation
|
Could you inspect the CI build errors? @dfed Hint: the key is binary distribution (it was downgraded for a reason); the closest comment I could find was apple/swift-collections#546 (comment) Haven't tested it tho |
|
After a short test, I see 2 obstacles:
I'm happy to merge it once we resolve the latter. |
|
Got it. @pblazej would y'all be interested in a PR that copy/pasted the existing implementation of |
Replace DequeModule and OrderedCollections with minimal internal implementations covering only the APIs actually used (~14 call sites). This unblocks consumers from being pinned to old swift-collections versions due to library evolution mode compatibility constraints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,43 @@ | |||
| /* | |||
| * Copyright 2026 LiveKit | |||
There was a problem hiding this comment.
less sure about this. happy to put whatever you want up here
There was a problem hiding this comment.
just run swiftformat over it, I think it's fine (checked on CI)
Array.removeFirst() is O(n), making the processSendQueue drain loop O(n²). Switch to a circular buffer backed by [Element?] with head pointer tracking for O(1) amortized append and removeFirst. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| /// A double-ended queue backed by a circular buffer. | ||
| /// Provides O(1) amortized `append` and `removeFirst`. | ||
| struct Deque<Element>: ExpressibleByArrayLiteral { | ||
| private var buffer: [Element?] = [] |
There was a problem hiding this comment.
Minimal internal Deque using a circular buffer ([Element?] + head pointer). Provides O(1) amortized append and removeFirst, which matters because processSendQueue in DataChannelPair drains this in a tight while loop — an Array-backed implementation would make that O(n²).
Only implements the 4 APIs used: isEmpty, first, append, removeFirst, plus ExpressibleByArrayLiteral for = [] initialization.
There was a problem hiding this comment.
For removeFirst you can leverage the % thing mentioned below, it was >40% faster in my case.
|
@dfed thank you for going the extra mile here! The problem with binary distribution is pretty common across Apple libs e.g. apple/swift-nio#2897 and already happened to use with I think porting the minimal API is better than carrying "a big part of the lib" under Apache as our usage is pretty narrow, the impact of future optimizations (leveraging ownership etc.) is probably negligible. 2 things:
|
| @discardableResult | ||
| mutating func removeFirst() -> Element { | ||
| guard count_ > 0 else { | ||
| fatalError("Cannot removeFirst from an empty Deque") |
There was a problem hiding this comment.
| fatalError("Cannot removeFirst from an empty Deque") | |
| preconditionFailure("Cannot removeFirst from an empty Deque") |
matches Array
| } | ||
| } | ||
|
|
||
| init(uniqueKeysWithValues keysAndValues: some Sequence<(Key, Value)>) { |
There was a problem hiding this comment.
I think the original does precondition here, I'm fine with in-place update as well, e.g.
init(uniqueKeysWithValues keysAndValues: some Sequence<(Key, Value)>) {
for (key, value) in keysAndValues {
if let i = index[key] {
pairs[i] = (key, value)
} else {
index[key] = pairs.count
pairs.append((key, value))
}
}
}| if count_ == buffer.count { | ||
| grow() | ||
| } | ||
| let tail = (head + count_) % buffer.count |
There was a problem hiding this comment.
Since capacity is always a power of two (starts at 4, doubles)...
| let tail = (head + count_) % buffer.count | |
| let tail = (head + count_) & (buffer.count - 1) |
| } | ||
| let element = buffer[head]! | ||
| buffer[head] = nil | ||
| head = (head + 1) % buffer.count |
There was a problem hiding this comment.
| head = (head + 1) % buffer.count | |
| head = (head + 1) & (buffer.count - 1) |
| let newCapacity = max(buffer.count * 2, 4) | ||
| var newBuffer = [Element?](repeating: nil, count: newCapacity) | ||
| for i in 0 ..< count_ { | ||
| newBuffer[i] = buffer[(head + i) % buffer.count] | ||
| } |
There was a problem hiding this comment.
| let newCapacity = max(buffer.count * 2, 4) | |
| var newBuffer = [Element?](repeating: nil, count: newCapacity) | |
| for i in 0 ..< count_ { | |
| newBuffer[i] = buffer[(head + i) % buffer.count] | |
| } | |
| let newCapacity = max(buffer.count * 2, 4) | |
| var newBuffer = [Element?](repeating: nil, count: newCapacity) | |
| let mask = buffer.count - 1 | |
| for i in 0 ..< count_ { | |
| newBuffer[i] = buffer[(head + i) & mask] |

Summary
DequeModuleandOrderedCollectionsimports with minimal internal implementations (Deque,OrderedDictionary,OrderedSet) inSources/LiveKit/Support/DataStructures/swift-collectionsfromPackage.swift,Package@swift-6.0.swift, andLiveKitClient.podspecDetails
The actual API surface used was tiny — 3 types, ~14 call sites, all internal. The new implementations are ~180 lines total, matching the existing pattern of lightweight data structures in the repo (
RingBuffer,MapTable,TTLDictionary).Test plan
xcodebuild build -scheme LiveKit -destination 'platform=macOS'succeedsxcodebuild test -scheme LiveKit -only-testing LiveKitCoreTests -destination 'platform=macOS'— all non-E2E tests pass (E2E tests require local server)🤖 Generated with Claude Code