Skip to content

Commit 022f82e

Browse files
Fix perf regression in DiffableSnapshot init (#138)
Reverts some changes from #136. When attempting to run the latest `main` in our app, I noticed that a large list would take several seconds to appear. Instruments shows that `DiffableSnapshot.init(viewModel:)` is the culprit: <img width="1047" alt="Screenshot 2024-10-14 at 15 07 25" src="https://github.com/user-attachments/assets/e729033c-cc63-467c-80be-2678985c3050"> It turns out that appending items one by one is much slower than appending them all at once. This reverts the change to this method introduced in #136. I also added a simple performance regression test, which generates 10 sections of 10,000 items each and runs some trivial assertion. On the current `main`, this takes a very long time to complete. With the reverted change on this branch it takes about ~200-300ms on my laptop. (This still seems pretty slow to me, but I can't see any other obvious ways of optimizing this.) While running the test, I noticed some flaky failures, and the following error being logged: > Diffable data source detected an attempt to insert or append 1 item identifier that already exists in the snapshot. The existing item identifier will be moved into place instead, but this operation will be more expensive. For best performance, inserted item identifiers should always be unique. Set a symbolic breakpoint on BUG_IN_CLIENT_OF_DIFFABLE_DATA_SOURCE__IDENTIFIER_ALREADY_EXISTS to catch this in the debugger. Item identifier that already exists: 289A50E9 This is caused by the random ID generated for each cell by default – I guess I got pretty (un)lucky here! I feel that it makes more sense generally to just use the section/item indexes as ids instead. I think this would also make it easier to debug any off-by-one errors or similar issues in tests. I have separated the fix and test changes into separate commits so you can have a look yourself. (Or if you don't like the changes to the test, it would make sense to just merge the fix itself.) Nice one! --------- Co-authored-by: Jesse Squires <[email protected]>
1 parent e0acffd commit 022f82e

File tree

4 files changed

+29
-12
lines changed

4 files changed

+29
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ NEXT
1212

1313
- Allow setting a `UICollectionViewDelegateFlowLayout` object to receive flow layout events from the collection view. ([@jessesquires](https://github.com/jessesquires), [#134](https://github.com/jessesquires/ReactiveCollectionsKit/pull/134))
1414
- Swift Concurrency improvements: `@MainActor` annotations have been removed from most top-level types and protocols, instead opting to apply `@MainActor` to individual members only where necessary. The goal is to impose fewer restrictions/burdens on clients. ([@jessesquires](https://github.com/jessesquires), [#135](https://github.com/jessesquires/ReactiveCollectionsKit/pull/135))
15-
- Various performance improvements. ([@jessesquires](https://github.com/jessesquires), [#136](https://github.com/jessesquires/ReactiveCollectionsKit/pull/136))
15+
- Various performance improvements. ([@jessesquires](https://github.com/jessesquires), [#136](https://github.com/jessesquires/ReactiveCollectionsKit/pull/136), [@lachenmayer](https://github.com/lachenmayer), [#138](https://github.com/jessesquires/ReactiveCollectionsKit/pull/138))
1616

1717
0.1.7
1818
-----

Sources/DiffableSnapshot.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ extension DiffableSnapshot {
2121
init(viewModel: CollectionViewModel) {
2222
self.init()
2323

24-
for section in viewModel.sections {
25-
self.appendSections([section.id])
24+
let allSectionIdentifiers = viewModel.sections.map(\.id)
25+
self.appendSections(allSectionIdentifiers)
2626

27-
for cell in section {
28-
self.appendItems([cell.id], toSection: section.id)
29-
}
27+
viewModel.sections.forEach {
28+
let allCellIdentifiers = $0.cells.map(\.id)
29+
self.appendItems(allCellIdentifiers, toSection: $0.id)
3030
}
3131
}
3232
}

Tests/Fakes/FakeCollectionViewModel.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ extension XCTestCase {
2020
@MainActor
2121
func fakeCollectionViewModel(
2222
id: String = .random,
23-
sectionId: (Int) -> String = { _ in .random },
24-
cellId: (Int, Int) -> String = { _, _ in .random },
25-
supplementaryViewId: (Int, Int) -> String = { _, _ in .random },
23+
sectionId: (Int) -> String = defaultSectionId,
24+
cellId: (Int, Int) -> String = defaultCellId,
25+
supplementaryViewId: (Int, Int) -> String = defaultCellId,
2626
numSections: Int = Int.random(in: 2...15),
2727
numCells: Int? = nil,
2828
useCellNibs: Bool = false,
@@ -53,8 +53,8 @@ extension XCTestCase {
5353
@MainActor
5454
func fakeSectionViewModel(
5555
id: String = .random,
56-
cellId: (Int, Int) -> String = { _, _ in .random },
57-
supplementaryViewId: (Int, Int) -> String = { _, _ in .random },
56+
cellId: (Int, Int) -> String = defaultCellId,
57+
supplementaryViewId: (Int, Int) -> String = defaultCellId,
5858
sectionIndex: Int = 0,
5959
numCells: Int = Int.random(in: 1...20),
6060
useCellNibs: Bool = false,
@@ -108,7 +108,7 @@ extension XCTestCase {
108108

109109
@MainActor
110110
func fakeCellViewModels(
111-
id: (Int, Int) -> String = { _, _ in .random },
111+
id: (Int, Int) -> String = defaultCellId,
112112
sectionIndex: Int = 0,
113113
count: Int = Int.random(in: 3...20),
114114
useNibs: Bool = false,
@@ -182,3 +182,11 @@ extension XCTestCase {
182182
fields.contains(target) ? self.expectation(field: target, id: id, function: function) : nil
183183
}
184184
}
185+
186+
private func defaultSectionId(_ index: Int) -> String {
187+
"\(index)"
188+
}
189+
190+
private func defaultCellId(_ sectionIndex: Int, _ itemIndex: Int) -> String {
191+
"\(sectionIndex)-\(itemIndex)"
192+
}

Tests/TestDiffableSnapshot.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,13 @@ final class TestDiffableSnapshot: UnitTestCase, @unchecked Sendable {
4444
XCTAssertTrue(snapshot.sectionIdentifiers.isEmpty)
4545
XCTAssertTrue(snapshot.itemIdentifiers.isEmpty)
4646
}
47+
48+
@MainActor
49+
func test_init_perf() {
50+
let model = self.fakeCollectionViewModel(numSections: 10, numCells: 10_000)
51+
measure {
52+
let snapshot = DiffableSnapshot(viewModel: model)
53+
XCTAssertEqual(snapshot.numberOfItems, 100_000)
54+
}
55+
}
4756
}

0 commit comments

Comments
 (0)