Skip to content

Bk/performance optimizations 2#159

Merged
bryankeller merged 1 commit into
masterfrom
bk/performance-optimizations-2
Jun 9, 2026
Merged

Bk/performance optimizations 2#159
bryankeller merged 1 commit into
masterfrom
bk/performance-optimizations-2

Conversation

@bryankeller

Copy link
Copy Markdown
Contributor

Details

Reduce some array overhead by using UnsafeMutableBufferPointer when batch-updating the sectionModels and itemModels arrays.

How Has This Been Tested

Tested in demo app and Airbnb app. Additionally, all unit tests pass.

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Perf fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Comment on lines -513 to -519
private func mutateSectionModels(
withUnsafeMutableBufferPointer body: (inout UnsafeMutableBufferPointer<SectionModel>) -> Void)
{
// Accessing these arrays using unsafe, untyped (raw) pointers
// avoids expensive copy-on-writes and Swift retain / release calls.
sectionModels.withUnsafeMutableBufferPointer(body)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no reason for this helper

Comment on lines +867 to +897
if MagazineLayout._enableExperimentalOptimizations {
sectionModels.withUnsafeMutableBufferPointer { sectionModels in
for (itemModel, insertIndexPath) in (itemModelInsertIndexPathPairs.sorted { $0.insertIndexPath < $1.insertIndexPath }) {
let sectionIndex = insertIndexPath.section
let itemIndex = insertIndexPath.item
let section = sectionModels[sectionIndex]
if itemIndex < section.numberOfItems, itemModel.id == section.idForItemModel(atIndex: itemIndex) {
// If the `itemModel` to insert already exists at the destination index, then there's no need to insert it again. This
// happens if item move updates are generated in addition to section move updates, which appears to be the case when using
// `UICollectionViewDiffableDataSource`. Other diffing approaches, like Paul Heckel's, do not produce item moves when
// their containing sections move.
continue
} else {
sectionModels[insertIndexPath.section].insert(itemModel, atIndex: itemIndex)
}
}
}
} else {
for (itemModel, insertIndexPath) in (itemModelInsertIndexPathPairs.sorted { $0.insertIndexPath < $1.insertIndexPath }) {
let sectionIndex = insertIndexPath.section
let itemIndex = insertIndexPath.item
let section = sectionModels[sectionIndex]
if itemIndex < section.numberOfItems, itemModel.id == section.idForItemModel(atIndex: itemIndex) {
// If the `itemModel` to insert already exists at the destination index, then there's no need to insert it again. This
// happens if item move updates are generated in addition to section move updates, which appears to be the case when using
// `UICollectionViewDiffableDataSource`. Other diffing approaches, like Paul Heckel's, do not produce item moves when
// their containing sections move.
continue
} else {
sectionModels[insertIndexPath.section].insert(itemModel, atIndex: itemIndex)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nothing new here, just copy and pasted into two branches so we can safely test

}

let numberOfItems = modelState.numberOfItems(inSectionAtIndex: sectionIndex)
for itemIndex in 0..<numberOfItems {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this diff is kind of annoying - this else branch is just the existing code but indented.

Comment on lines +123 to +166
modelState.forEachSectionModel { sectionIndex, sectionModel in
reusableIndexPath.section = sectionIndex
}

let sectionMetrics = metricsForSection(atIndex: sectionIndex)
modelState.updateMetrics(to: sectionMetrics, forSectionAtIndex: sectionIndex)
modelState.updateMetrics(
to: metricsForSection(atIndex: sectionIndex),
forSectionAtIndex: sectionIndex,
sectionModel: &sectionModel)

if let headerModel = headerModelForHeader(inSectionAtIndex: sectionIndex) {
modelState.setHeader(headerModel, forSectionAtIndex: sectionIndex)
} else {
modelState.removeHeader(forSectionAtIndex: sectionIndex)
}
if let headerModel = headerModelForHeader(inSectionAtIndex: sectionIndex) {
modelState.setHeader(
headerModel,
forSectionAtIndex: sectionIndex,
sectionModel: &sectionModel)
} else {
modelState.removeHeader(forSectionAtIndex: sectionIndex, sectionModel: &sectionModel)
}

if let footerModel = footerModelForFooter(inSectionAtIndex: sectionIndex) {
modelState.setFooter(footerModel, forSectionAtIndex: sectionIndex)
} else {
modelState.removeFooter(forSectionAtIndex: sectionIndex)
}
if let footerModel = footerModelForFooter(inSectionAtIndex: sectionIndex) {
modelState.setFooter(
footerModel,
forSectionAtIndex: sectionIndex,
sectionModel: &sectionModel)
} else {
modelState.removeFooter(forSectionAtIndex: sectionIndex, sectionModel: &sectionModel)
}

if let backgroundModel = backgroundModelForBackground(inSectionAtIndex: sectionIndex) {
modelState.setBackground(backgroundModel, forSectionAtIndex: sectionIndex)
} else {
modelState.removeBackground(forSectionAtIndex: sectionIndex)
}
if let backgroundModel = backgroundModelForBackground(inSectionAtIndex: sectionIndex) {
modelState.setBackground(
backgroundModel,
forSectionAtIndex: sectionIndex,
sectionModel: &sectionModel)
} else {
modelState.removeBackground(
forSectionAtIndex: sectionIndex,
sectionModel: &sectionModel)
}

let numberOfItems = modelState.numberOfItems(inSectionAtIndex: sectionIndex)
for itemIndex in 0..<numberOfItems {
if Self._enableExperimentalOptimizations {
modelState.updateItemSizeModes(
forSectionAtIndex: sectionIndex,
sectionModel: &sectionModel)
{ itemIndex in
reusableIndexPath.item = itemIndex
modelState.updateItemSizeMode(to: sizeModeForItem(at: reusableIndexPath), forItemAt: reusableIndexPath)
return sizeModeForItem(at: reusableIndexPath)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is the same code as the else branch, but using the batch-mutators - shaves ~10ms off of this work for 100k items.

@bryankeller bryankeller requested a review from brynbodayle June 9, 2026 00:52
@bryankeller bryankeller added the enhancement New feature or request label Jun 9, 2026
@bryankeller bryankeller merged commit a51c384 into master Jun 9, 2026
1 check passed
@bryankeller bryankeller deleted the bk/performance-optimizations-2 branch June 9, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants