Skip to content
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

Add collection view batch updates #15

Closed
wants to merge 19 commits into from

Conversation

JosephDuffy
Copy link
Member

@JosephDuffy JosephDuffy commented Dec 13, 2020

The idea of this is that without breaking the API the collection view will batch updates.

Inserting a large number of sections is the main area of performance loss we are currently encountering, because the sections are inserted individually and not batched. This change alone has reduce the initial load time of one our screens (which has 100-150 sections added at once) from 30-45 seconds down to less than a second (at least it is not noticeable).

I had created composed-swift/Composed#17 to try and address this, which has the advantage that it would apply to other view types (e.g. UITableView), but I believe does not offer the same performance improvements and it is restricted to a single ComposedSectionProvider.

This is a draft to collect feedback; as you can see there are some TODOs but I think there's enough implemented to provide an overview of the changes that would be required to implement this.

This does not currently work; there are situations that cause the collection NSInternalInconsistencyException', reason: 'Invalid update error. I have some failing tests that demonstrate what the result should be.

This is only required for the tests, so I don' think the `from` needs to be updated in `Package.swift` since consumers never run tests?
Copy link
Collaborator

@shaps80 shaps80 left a comment

Choose a reason for hiding this comment

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

Ok so its obviously a lot more complex and likely this is necessary. I'm never a big fan of index tracking since its very easy to break.

Could we improve this with perhaps a type that maintains an index. Then include a few accessors like:

stack.push()
stack.pop()
stack.isEmpty

EDIT: I'm obviously referring to the += and -= balance calls in the mapping.

@@ -192,31 +199,40 @@ open class CollectionCoordinator: NSObject {
delegate?.coordinatorDidUpdate(self)
}

fileprivate func debugLog(_ message: String) {
if #available(iOS 12, *) {
os_log("%@", log: OSLog(subsystem: "ComposedUI", category: "CollectionCoordinator"), type: .debug, message)
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

let cell = self.collectionView.cellForItem(at: indexPath) else {
indexPathsToReload.append(indexPath)
continue
!section.prefersReload(forElementAt: indexPath.item),
Copy link
Collaborator

@shaps80 shaps80 Dec 14, 2020

Choose a reason for hiding this comment

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

Not sure what's going on here with the whitespace?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is how Xcode indents when copy/pasting or using re-indent.

I've reverted it, although I think it's clearer when some extra line breaks are added and running re-indent:

guard
    let section = self.sectionProvider.sections[indexPath.section] as? CollectionUpdateHandler,
    !section.prefersReload(forElementAt: indexPath.item),
    let cell = self.collectionView.cellForItem(at: indexPath)
else {
    indexPathsToReload.append(indexPath)
    continue
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that's fine.

@JosephDuffy
Copy link
Member Author

Ok so its obviously a lot more complex and likely this is necessary. I'm never a big fan of index tracking since its very easy to break.

I'm not enjoying it and I'm worried it's fragile, but the (pretty large and growing) test suite makes me feel a little better about it.

Could we improve this with perhaps a type that maintains an index. Then include a few accessors like:

stack.push()
stack.pop()
stack.isEmpty

EDIT: I'm obviously referring to the += and -= balance calls in the mapping.

I thought that tracking some sort of index (I was using the index provided by the mapping) would be useful/required, but the issue is that we don't know the order change could occur so I don't think there's a single index that can be tracked that helps.

At first I was going to create a Change type that contains the data of the change, but always with a var section: Int property to make some of the updates easier, e.g.:

allChanges.map { change in
	guard change.section > removedSection else { return change }

	var change = change
	change.section -= 1
	return change
}

But this isn't actually that useful and could cause more confusion because of differences with how changes are handled (see how sections removes modify other section removes for example)

I have been thinking about some sort of Changeset type that contains this logic though. It would allow for reuse with UITableView and would be a single unit that can be tested, rather than the current kind of hacky testing. The current tests actually apply the changes to a UICollectionView though so they are useful for catching mistakes in the expected results because the collection view will trigger an exception.

reset()
prepareSections()
collectionView.reloadData()
}

public func mappingWillBeginUpdating(_ mapping: SectionProviderMapping) {
reset()
defersUpdate = true
batchedUpdatesCount += 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps I'm misunderstanding but for the index stack I suggested, I meant for this value? Not sure how your response relates? Maybe you can elaborate here.

Also, yeah reuse across other coordinators makes total sense and esp from a testing POV.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah I thought you were referencing the index changes performed in each of the update delegate methods.

Moving batchedUpdatesCount to the Changeset type mentioned is part of the plan for that

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok cool.

Copy link
Collaborator

@shaps80 shaps80 left a comment

Choose a reason for hiding this comment

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

Only one question on the review. Otherwise, looks much better. Purposeful at least and definitely more testable and reusable.

EDIT: Approved by accident hahah!

XCTAssertTrue(changesReducer.changeset.elementsUpdated.isEmpty)
XCTAssertTrue(changesReducer.changeset.elementsMoved.isEmpty)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I assume you plan on adding more test cases later on?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is only the start -- the CollectionCoordinator doesn't use it yet.

More tests will be added to this file before merging, but I think it's going to lean a little on the new CollectionCoordinator tests passing to validate correctness according to UICollectionView.

XCTAssertTrue(changeset!.groupsUpdated.isEmpty)
}

func testRemoveAMovedIndex() {
Copy link
Member Author

Choose a reason for hiding this comment

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

@shaps80 This test, along with testMoveElementAtSameIndexAsRemove below, are currently failing but I'm not 100% sure about the tests themselves.

Can you confirm that the test looks correct? e.g. this should produce removes and inserts, not a remove and moves?

@JosephDuffy
Copy link
Member Author

Moving to main repo with merged libraries: composed-swift/Composed#28

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants