Adopt JournalingOrderedDictionary. (#5405)

This change also moves mutation to the ordered
dictionary into a separate queue. There is no
change to behavior yet because the queue is only
dispatched to synchronously so far. Doing this
forces changes to how database transactions are
acquired since they must be used on the same queue
where they orignated.
This commit is contained in:
george-signal 2022-11-16 17:31:23 -08:00 committed by GitHub
parent d57e0d35b8
commit 7a49a0fa82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 705 additions and 570 deletions

2
Pods

@ -1 +1 @@
Subproject commit de19768f9502f2570a1a357d71b944c798f32450
Subproject commit 003ae542e2bd1b9545d7de3bfaefcd40a23475bd

View File

@ -53,7 +53,6 @@ public struct JournalingOrderedDictionary<KeyType: Hashable, ValueType, ChangeTy
}
public mutating func append(key: KeyType, value: ValueType) {
let i = orderedDictionary.count
journal.append(.append)
orderedDictionary.append(key: key, value: value)
}
@ -76,8 +75,11 @@ public struct JournalingOrderedDictionary<KeyType: Hashable, ValueType, ChangeTy
orderedDictionary.remove(at: index)
}
public mutating func eraseJournal() {
journal = []
public mutating func takeJournal() -> [Change] {
defer {
journal = []
}
return journal
}
public mutating func removeAll() {

View File

@ -249,26 +249,8 @@ class MediaGallery: Dependencies {
return
}
var sectionIndexesNeedingUpdate = IndexSet()
var sectionsToDelete = IndexSet()
let (sectionIndexesNeedingUpdate, sectionsToDelete) = sections.reloadSections(for: sectionsNeedingUpdate)
databaseStorage.read { transaction in
for sectionDate in sectionsNeedingUpdate {
// Scan backwards; newer items are more likely to be modified.
// (We could use a binary search here as well.)
guard let sectionIndex = sections.sectionDates.lastIndex(of: sectionDate) else {
continue
}
// Refresh the section.
let newCount = sections.reloadSection(for: sectionDate, transaction: transaction)
sectionIndexesNeedingUpdate.insert(sectionIndex)
if newCount == 0 {
sectionsToDelete.insert(sectionIndex)
}
}
}
delegates.forEach {
$0.mediaGallery(self, didReloadItemsInSections: sectionIndexesNeedingUpdate)
}
@ -295,49 +277,17 @@ class MediaGallery: Dependencies {
}
Logger.debug("")
var sectionsNeedingUpdate = IndexSet()
var didAddSectionAtEnd = false
var didReset = false
databaseStorage.read { transaction in
for attachmentInfo in relevantAttachments {
let sectionDate = GalleryDate(date: Date(millisecondsSince1970: attachmentInfo.timestamp))
// Do a backwards search assuming new messages usually arrive at the end.
// Still, this is kept sorted, so we ought to be able to do a binary search instead.
if let lastSectionDate = sections.sectionDates.last, sectionDate > lastSectionDate {
// Only let clients know about the new section if they thought they were at the end;
// otherwise they'll fetch more if they need to.
if sections.hasFetchedMostRecent {
sections.resetHasFetchedMostRecent()
didAddSectionAtEnd = true
}
} else if let sectionIndex = sections.sectionDates.lastIndex(of: sectionDate) {
sectionsNeedingUpdate.insert(sectionIndex)
} else {
// We've loaded the first attachment in a new section that's not at the end. That can't be done
// transparently in MediaGallery's model, so let all our delegates know to refresh *everything*.
// This should be rare, but can happen if someone has automatic attachment downloading off and then
// goes back and downloads an attachment that crosses the month boundary.
sections.reset(transaction: transaction)
didReset = true
return
}
}
for sectionIndex in sectionsNeedingUpdate {
// Throw out everything in that section.
let sectionDate = sections.sectionDates[sectionIndex]
sections.reloadSection(for: sectionDate, transaction: transaction)
}
let dates = relevantAttachments.lazy.map {
GalleryDate(date: Date(millisecondsSince1970: $0.timestamp))
}
if didReset {
let newAttachmentResult = sections.handleNewAttachments(dates)
if newAttachmentResult.didReset {
delegates.forEach { $0.didReloadAllSectionsInMediaGallery(self) }
} else {
if !sectionsNeedingUpdate.isEmpty {
delegates.forEach { $0.mediaGallery(self, didReloadItemsInSections: sectionsNeedingUpdate) }
if !newAttachmentResult.update.isEmpty {
delegates.forEach { $0.mediaGallery(self, didReloadItemsInSections: newAttachmentResult.update) }
}
if didAddSectionAtEnd {
if newAttachmentResult.didAddAtEnd {
delegates.forEach { $0.didAddSectionInMediaGallery(self) }
}
}
@ -467,7 +417,7 @@ class MediaGallery: Dependencies {
if sections.isEmpty {
// Set up the current section only.
sections.loadInitialSection(for: focusedItem.galleryDate, transaction: transaction)
sections.loadInitialSection(for: focusedItem.galleryDate)
}
return sections.getOrReplaceItem(focusedItem, offsetInSection: offsetInSection)
@ -498,8 +448,8 @@ class MediaGallery: Dependencies {
/// Operates in bulk in an attempt to cut down on database traffic, meaning it may measure multiple sections at once.
///
/// Returns the number of new sections loaded, which can be used to update section indexes.
internal func loadEarlierSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int {
return sections.loadEarlierSections(batchSize: batchSize, transaction: transaction)
internal func loadEarlierSections(batchSize: Int) -> Int {
return sections.loadEarlierSections(batchSize: batchSize)
}
/// Loads at least one section after the latest section, though not any of the items in it.
@ -507,8 +457,8 @@ class MediaGallery: Dependencies {
/// Operates in bulk in an attempt to cut down on database traffic, meaning it may measure multiple sections at once.
///
/// Returns the number of new sections loaded.
internal func loadLaterSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int {
return sections.loadLaterSections(batchSize: batchSize, transaction: transaction)
internal func loadLaterSections(batchSize: Int) -> Int {
return sections.loadLaterSections(batchSize: batchSize)
}
// MARK: -
@ -664,9 +614,7 @@ extension MediaGallery: DatabaseChangeDelegate {
func databaseChangesDidUpdateExternally() {
// Conservatively assume anything could have happened.
databaseStorage.read { transaction in
sections.reset(transaction: transaction)
}
sections.reset()
delegates.forEach { $0.didReloadAllSectionsInMediaGallery(self) }
}

View File

@ -62,13 +62,13 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
var item: Item?
}
private struct State: Dependencies {
private struct State {
/// All sections we know about.
///
/// Each section contains an array of possibly-fetched items.
/// The length of the array is always the correct number of items in the section.
/// The keys are kept in sorted order.
var itemsBySection: OrderedDictionary<GalleryDate, [MediaGallerySlot]> = OrderedDictionary()
var itemsBySection: JournalingOrderedDictionary<GalleryDate, [MediaGallerySlot], ItemChange> = JournalingOrderedDictionary()
var hasFetchedOldest = false
var hasFetchedMostRecent = false
var loader: Loader
@ -200,7 +200,10 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
offset: 0,
ascending: true,
transaction: transaction)
itemsBySection.replace(key: date, value: rowids.map { MediaGallerySlot(rowid: $0) })
itemsBySection.replace(key: date) {
return (rowids.map { MediaGallerySlot(rowid: $0) },
[.reloadSection])
}
return rowids.count
}
@ -435,112 +438,122 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
naiveRange.removeLast(countToTrim)
}
/// Loads items in the given range.
internal struct LoadItemsRequest {
let trimmedRange: Range<Int>
let sectionIndex: Int
}
/// Constructs a `LoadItemsRequest` if one is needed. Returns `nil` if no items need to be loaded in the requested range.
///
/// A "naive" range may start at a negative offset, representing a position in a section before `sectionIndex`.
/// Similarly, the endpoint may be in a section that follows `sectionIndex`. However, the range must *cross*
/// `sectionIndex`, or it is not considered valid. `sectionIndex` must refer to a loaded section.
///
/// Will open its own database transaction, in the hopes of not having to open one at all.
///
/// Returns the indexes of newly loaded *sections,* which could shift the indexes of existing sections. These will
/// always be before and/or after the existing sections, never interleaving. If `naiveRange.startIndex` is
/// non-negative, there will never be any sections loaded before the existing sections. That is:
///
/// - When this method starts, we'll have sections like this: [G, H, J].
/// - When this method ends, we'll have sections like this: [C, D, F] [G, H, J] [K, L, N],
/// where the CDF and KLN chunks could be empty. The returned index set will contain the indexes of CF and KN.
internal mutating func ensureItemsLoaded(in naiveRange: Range<Int>,
relativeToSection sectionIndex: Int) -> IndexSet {
/// - Parameters:
/// - naiveRange This may start at a negative offset, representing a position in a section before `sectionIndex`. Similarly, the endpoint may be in a section that follows `sectionIndex`. However, the range must *cross* `sectionIndex`, or it is not considered valid. `sectionIndex` must refer to a loaded section.
/// - sectionIndex: The section relative to which `naiveRange` is interpreted.
internal func loadItemsRequest(in naiveRange: Range<Int>,
relativeToSection sectionIndex: Int) -> LoadItemsRequest? {
var trimmedRange = naiveRange
trimLoadedItemsAtStart(from: &trimmedRange, relativeToSection: sectionIndex)
trimLoadedItemsAtEnd(from: &trimmedRange, relativeToSection: sectionIndex)
if trimmedRange.isEmpty {
return IndexSet()
return nil
}
return LoadItemsRequest(trimmedRange: trimmedRange, sectionIndex: sectionIndex)
}
var numNewlyLoadedEarlierSections: Int = 0
var numNewlyLoadedLaterSections: Int = 0
/// Loads items specified in the request.
///
/// Will open its own database transaction.
///
/// If the start index in the request is non-negative, there will never be any sections loaded before the existing sections. That is:
/// - When this method starts, we'll have sections like this: [G, H, J].
/// - When this method ends, we'll have sections like this: [C, D, F] [G, H, J] [K, L, N],
/// where the CDF and KLN chunks could be empty. The returned index set will contain the indexes of CF and KN.
///
/// Returns the indexes of newly loaded *sections,* which could shift the indexes of existing sections. These will
/// always be before and/or after the existing sections, never interleaving.
internal mutating func ensureItemsLoaded(request: LoadItemsRequest,
transaction: SDSAnyReadTransaction) -> IndexSet {
return Bench(title: "fetching gallery items") {
owsAssert(!request.trimmedRange.isEmpty)
Bench(title: "fetching gallery items") {
self.databaseStorage.read { transaction in
// Figure out the earliest section this request will cross.
// Note that this is a bigger batch size than necessary:
// -trimmedRange.startIndex would be sufficient,
// and resolveNaiveStartIndex(...) could give us the exact offset we need.
// However, the media gallery is a scrolling collection view that most commonly starts at the
// present and scrolls up into the past; if we are ensuring loaded items in earlier sections, we are
// likely actively scrolling. Because of this, we potentially measure some extra sections here so
// that we don't have to on the immediate next call to ensureItemsLoaded(...).
let (requestStartPath, newlyLoadedCount) = resolveNaiveStartIndex(trimmedRange.startIndex,
relativeToSection: sectionIndex,
batchSize: trimmedRange.count,
transaction: transaction)
numNewlyLoadedEarlierSections = newlyLoadedCount
guard let requestStartPath = requestStartPath else {
owsFail("failed to resolve despite loading \(numNewlyLoadedEarlierSections) earlier sections")
var numNewlyLoadedEarlierSections: Int = 0
var numNewlyLoadedLaterSections: Int = 0
// Figure out the earliest section this request will cross.
// Note that this is a bigger batch size than necessary:
// -trimmedRange.startIndex would be sufficient,
// and resolveNaiveStartIndex(...) could give us the exact offset we need.
// However, the media gallery is a scrolling collection view that most commonly starts at the
// present and scrolls up into the past; if we are ensuring loaded items in earlier sections, we are
// likely actively scrolling. Because of this, we potentially measure some extra sections here so
// that we don't have to on the immediate next call to ensureItemsLoaded(...).
let (requestStartPath, newlyLoadedCount) = resolveNaiveStartIndex(request.trimmedRange.startIndex,
relativeToSection: request.sectionIndex,
batchSize: request.trimmedRange.count,
transaction: transaction)
numNewlyLoadedEarlierSections = newlyLoadedCount
guard let requestStartPath = requestStartPath else {
owsFail("failed to resolve despite loading \(numNewlyLoadedEarlierSections) earlier sections")
}
var currentSectionIndex = requestStartPath.section
let interval = DateInterval(start: itemsBySection.orderedKeys[currentSectionIndex].interval.start,
end: .distantFutureForMillisecondTimestamp)
let requestRange = requestStartPath.item ..< (requestStartPath.item + request.trimmedRange.count)
var offset = 0
loader.enumerateItems(in: interval,
range: requestRange,
transaction: transaction) { i, uniqueId, buildItem in
owsAssertDebug(i >= offset, "does not support reverse traversal")
var (date, slots) = itemsBySection[currentSectionIndex]
var itemIndex = i - offset
while itemIndex >= slots.count {
itemIndex -= slots.count
offset += slots.count
currentSectionIndex += 1
if currentSectionIndex >= itemsBySection.count {
if hasFetchedMostRecent {
// Ignore later attachments.
owsAssertDebug(itemsBySection.count == 1, "should only be used in single-album page view")
return
}
numNewlyLoadedLaterSections += loadLaterSections(batchSize: request.trimmedRange.count,
transaction: transaction)
if currentSectionIndex >= itemsBySection.count {
owsFailDebug("attachment #\(i) \(uniqueId) is beyond the last section")
return
}
}
(date, slots) = itemsBySection[currentSectionIndex]
}
var currentSectionIndex = requestStartPath.section
let interval = DateInterval(start: itemsBySection.orderedKeys[currentSectionIndex].interval.start,
end: .distantFutureForMillisecondTimestamp)
let requestRange = requestStartPath.item ..< (requestStartPath.item + trimmedRange.count)
var offset = 0
loader.enumerateItems(in: interval,
range: requestRange,
transaction: transaction) { i, uniqueId, buildItem in
owsAssertDebug(i >= offset, "does not support reverse traversal")
var (date, slots) = itemsBySection[currentSectionIndex]
var itemIndex = i - offset
while itemIndex >= slots.count {
itemIndex -= slots.count
offset += slots.count
currentSectionIndex += 1
if currentSectionIndex >= itemsBySection.count {
if hasFetchedMostRecent {
// Ignore later attachments.
owsAssertDebug(itemsBySection.count == 1, "should only be used in single-album page view")
return
}
numNewlyLoadedLaterSections += loadLaterSections(batchSize: trimmedRange.count,
transaction: transaction)
if currentSectionIndex >= itemsBySection.count {
owsFailDebug("attachment #\(i) \(uniqueId) is beyond the last section")
return
}
}
(date, slots) = itemsBySection[currentSectionIndex]
}
var slot = slots[itemIndex]
if let loadedItem = slot.item {
owsAssert(loadedItem.uniqueId == uniqueId)
return
}
var slot = slots[itemIndex]
if let loadedItem = slot.item {
owsAssert(loadedItem.uniqueId == uniqueId)
return
}
itemsBySection.replace(key: date) {
let item = buildItem()
owsAssertDebug(item.galleryDate == date,
"item from \(item.galleryDate) put into section for \(date)")
// Performance hack: clear out the current 'items' array in 'sections' to avoid copy-on-write.
itemsBySection.replace(key: date, value: [])
slot.item = item
slots[itemIndex] = slot
itemsBySection.replace(key: date, value: slots)
return (slots, [.updateItem(index: itemIndex)])
}
}
}
let firstNewLaterSectionIndex = itemsBySection.count - numNewlyLoadedLaterSections
var newlyLoadedSections = IndexSet()
newlyLoadedSections.insert(integersIn: 0..<numNewlyLoadedEarlierSections)
newlyLoadedSections.insert(integersIn: firstNewLaterSectionIndex..<itemsBySection.count)
return newlyLoadedSections
let firstNewLaterSectionIndex = itemsBySection.count - numNewlyLoadedLaterSections
var newlyLoadedSections = IndexSet()
newlyLoadedSections.insert(integersIn: 0..<numNewlyLoadedEarlierSections)
newlyLoadedSections.insert(integersIn: firstNewLaterSectionIndex..<itemsBySection.count)
return newlyLoadedSections
}
}
internal mutating func getOrReplaceItem(_ newItem: Item, offsetInSection: Int) -> Item? {
@ -559,9 +572,10 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
}
// Swap out the section items to avoid copy-on-write.
itemsBySection.replace(key: newItem.galleryDate, value: [])
items[offsetInSection].item = newItem
itemsBySection.replace(key: newItem.galleryDate, value: items)
itemsBySection.replace(key: newItem.galleryDate) {
items[offsetInSection].item = newItem
return (items, [.updateItem(index: offsetInSection)])
}
return newItem
}
@ -576,17 +590,17 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
for path in paths.sorted().reversed() {
let sectionKey = itemsBySection.orderedKeys[path.section]
// Swap out / swap in to avoid copy-on-write.
var section = itemsBySection.replace(key: sectionKey, value: [])
var section = itemsBySection[sectionKey]!
itemsBySection.replace(key: sectionKey) {
if section.remove(at: path.item).item == nil {
owsFailDebug("removed an item that wasn't loaded, which isn't permitted")
}
if section.remove(at: path.item).item == nil {
owsFailDebug("removed an item that wasn't loaded, which isn't permitted")
}
if section.isEmpty {
itemsBySection.remove(at: path.section)
removedSections.insert(path.section)
} else {
itemsBySection.replace(key: sectionKey, value: section)
if section.isEmpty {
removedSections.insert(path.section)
return nil
}
return (section, [.removeItem(index: path.item)])
}
}
@ -594,47 +608,119 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
}
}
private struct SnapshotManager {
private(set) var state: State
enum ItemChange: CustomDebugStringConvertible, Equatable {
var debugDescription: String {
switch self {
case .reloadSection:
return "reloadSection"
case .updateItem(index: let index):
return "updateItem(index: \(index))"
case .removeItem(index: let index):
return "removeItem(index: \(index))"
}
}
case reloadSection
case updateItem(index: Int)
case removeItem(index: Int)
}
init(_ state: State) {
self.state = state
private struct SnapshotManager: Dependencies {
private let queue: DispatchQueue
private var mutableState: State
private var snapshot: State
/// Called on the main queue after a mutation. The snapshot will have
/// just been updated prior to this call. This is a good place to notify
/// UICollectionView of changes to its data source.
var didChange: (([JournalingOrderedDictionaryChange<ItemChange>]) -> Void)?
var state: State {
dispatchPrecondition(condition: .onQueue(.main))
return snapshot
}
init(_ queue: DispatchQueue, state: State) {
self.queue = queue
self.mutableState = state
self.snapshot = state
}
mutating func mutate<T>(_ block: (inout State) -> (T)) -> T {
return block(&state)
dispatchPrecondition(condition: .onQueue(.main))
var updatedState: State?
let (result, changes) = queue.sync {
let result = block(&mutableState)
updatedState = mutableState
return (result, mutableState.itemsBySection.takeJournal())
}
snapshot = updatedState!
didChange?(changes)
return result
}
mutating func mutate<T>(_ block: (inout State, SDSAnyReadTransaction) -> (T)) -> T {
return mutate { mutableState in
return Self.databaseStorage.read { transaction in
return block(&mutableState, transaction)
}
}
}
}
private var snapshotManager: SnapshotManager
private var state: State { snapshotManager.state }
private let mutationQueue = DispatchQueue(label: "org.signal.media-gallery-sections-mutation")
internal init(loader: Loader) {
snapshotManager = SnapshotManager(State(loader: loader))
snapshotManager = SnapshotManager(mutationQueue, state: State(loader: loader))
}
// MARK: Sections
internal mutating func loadEarlierSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int {
return snapshotManager.mutate { state in
internal mutating func loadEarlierSections(batchSize: Int) -> Int {
return snapshotManager.mutate { state, transaction in
return state.loadEarlierSections(batchSize: batchSize, transaction: transaction)
}
}
internal mutating func loadLaterSections(batchSize: Int, transaction: SDSAnyReadTransaction) -> Int {
return snapshotManager.mutate { state in
internal mutating func loadLaterSections(batchSize: Int) -> Int {
return snapshotManager.mutate { state, transaction in
return state.loadLaterSections(batchSize: batchSize, transaction: transaction)
}
}
internal mutating func loadInitialSection(for date: GalleryDate, transaction: SDSAnyReadTransaction) {
return snapshotManager.mutate { state in
internal mutating func loadInitialSection(for date: GalleryDate) {
return snapshotManager.mutate { state, transaction in
return state.loadInitialSection(for: date, transaction: transaction)
}
}
internal mutating func reloadSections(for dates: Set<GalleryDate>) -> (update: IndexSet, delete: IndexSet) {
return snapshotManager.mutate { state, transaction in
var sectionIndexesNeedingUpdate = IndexSet()
var sectionsToDelete = IndexSet()
for sectionDate in dates {
// Scan backwards; newer items are more likely to be modified.
// (We could use a binary search here as well.)
guard let sectionIndex = state.itemsBySection.orderedKeys.lastIndex(of: sectionDate) else {
continue
}
// Refresh the section.
let newCount = state.reloadSection(for: sectionDate, transaction: transaction)
sectionIndexesNeedingUpdate.insert(sectionIndex)
if newCount == 0 {
sectionsToDelete.insert(sectionIndex)
}
}
return (update: sectionIndexesNeedingUpdate, delete: sectionsToDelete)
}
}
@discardableResult
internal mutating func reloadSection(for date: GalleryDate, transaction: SDSAnyReadTransaction) -> Int {
return snapshotManager.mutate { state in
internal mutating func reloadSection(for date: GalleryDate) -> Int {
return snapshotManager.mutate { state, transaction in
return state.reloadSection(for: date, transaction: transaction)
}
}
@ -651,12 +737,55 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
}
}
internal mutating func reset(transaction: SDSAnyReadTransaction) {
return snapshotManager.mutate { state in
internal mutating func reset() {
return snapshotManager.mutate { state, transaction in
state.reset(transaction: transaction)
}
}
internal struct NewAttachmentHandlingResult: Equatable {
var update = IndexSet()
var didAddAtEnd = false
var didReset = false
}
internal mutating func handleNewAttachments(_ dates: some Sequence<GalleryDate>) -> NewAttachmentHandlingResult {
return snapshotManager.mutate { state, transaction in
var result = NewAttachmentHandlingResult()
for sectionDate in dates {
// Do a backwards search assuming new messages usually arrive at the end.
// Still, this is kept sorted, so we ought to be able to do a binary search instead.
if let lastSectionDate = state.itemsBySection.orderedKeys.last, sectionDate > lastSectionDate {
// Only let clients know about the new section if they thought they were at the end;
// otherwise they'll fetch more if they need to.
if state.hasFetchedMostRecent {
state.resetHasFetchedMostRecent()
result.didAddAtEnd = true
}
} else if let sectionIndex = state.itemsBySection.orderedKeys.lastIndex(of: sectionDate) {
result.update.insert(sectionIndex)
} else {
// We've loaded the first attachment in a new section that's not at the end. That can't be done
// transparently in MediaGallery's model, so let all our delegates know to refresh *everything*.
// This should be rare, but can happen if someone has automatic attachment downloading off and then
// goes back and downloads an attachment that crosses the month boundary.
state.reset(transaction: transaction)
result.didReset = true
return result
}
}
for sectionIndex in result.update {
// Throw out everything in that section.
let sectionDate = state.itemsBySection.orderedKeys[sectionIndex]
state.reloadSection(for: sectionDate, transaction: transaction)
}
return result
}
}
// MARK: Items
/// Returns the item at `path`, which will be `nil` if not yet loaded.
@ -745,10 +874,9 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
internal mutating func resolveNaiveStartIndex(
_ naiveIndex: Int,
relativeToSection initialSectionIndex: Int,
batchSize: Int,
transaction: SDSAnyReadTransaction?
batchSize: Int
) -> (path: IndexPath?, numberOfSectionsLoaded: Int) {
return snapshotManager.mutate { state in
return snapshotManager.mutate { state, transaction in
return state.resolveNaiveStartIndex(naiveIndex,
relativeToSection: initialSectionIndex,
batchSize: batchSize,
@ -772,9 +900,14 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
internal mutating func ensureItemsLoaded(in naiveRange: Range<Int>,
relativeToSection sectionIndex: Int) -> IndexSet {
return snapshotManager.mutate { state in
state.ensureItemsLoaded(in: naiveRange,
relativeToSection: sectionIndex)
return snapshotManager.mutate { state in
guard let request = state.loadItemsRequest(in: naiveRange, relativeToSection: sectionIndex) else {
return IndexSet()
}
return Self.databaseStorage.read { transaction in
return state.ensureItemsLoaded(request: request,
transaction: transaction)
}
}
}
@ -796,5 +929,33 @@ internal struct MediaGallerySections<Loader: MediaGallerySectionLoader>: Depende
internal var sectionDates: [GalleryDate] { state.itemsBySection.orderedKeys }
internal var hasFetchedMostRecent: Bool { state.hasFetchedMostRecent }
internal var hasFetchedOldest: Bool { state.hasFetchedOldest }
internal var itemsBySection: OrderedDictionary<GalleryDate, [MediaGallerySlot]> { state.itemsBySection }
internal var itemsBySection: OrderedDictionary<GalleryDate, [MediaGallerySlot]> { state.itemsBySection.orderedDictionary }
}
fileprivate extension JournalingOrderedDictionary where ValueType: ExpressibleByArrayLiteral {
/// This is a convenience method that provides a performance optimization to delete or set the value of an already-existing key.
/// By using this method, you avoid having two different copies of the value in memory. That could cause a copy-on-write for a value that's about to disappear.
/// A closure rather than a value is used so the value in the underlying dictionary can be removed before forcing the new value to exist.
///
/// Example usage:
///
/// if var value = dict["my key"] { // value is of type [String]
/// dict.replace("my key") {
/// value.append("foo")
/// return (value, [.replace])
/// }
/// }
///
/// If the closure returns `nil` then the key is removed from the dictionary.
mutating func replace(key: KeyType, closure: () -> (ValueType, [ChangeType])?) {
let i = orderedKeys.firstIndex(of: key)!
// Performance hack: clear out the current 'items' array in 'sections' to avoid copy-on-write.
replaceValue(at: i, value: [], changes: [])
if let tuple = closure() {
let (newValue, changes) = tuple
replaceValue(at: i, value: newValue, changes: changes)
} else {
remove(at: i)
}
}
}

View File

@ -106,9 +106,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDe
defer { super.viewWillAppear(animated) }
if mediaGallery.galleryDates.isEmpty {
databaseStorage.read { transaction in
_ = self.mediaGallery.loadEarlierSections(batchSize: kLoadBatchSize, transaction: transaction)
}
_ = self.mediaGallery.loadEarlierSections(batchSize: kLoadBatchSize)
if mediaGallery.galleryDates.isEmpty {
// There must be no media.
return
@ -704,9 +702,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDe
func didAddSectionInMediaGallery(_ mediaGallery: MediaGallery) {
let oldSectionCount = self.numberOfSections(in: collectionView)
databaseStorage.read { transaction in
_ = mediaGallery.loadLaterSections(batchSize: kLoadBatchSize, transaction: transaction)
}
_ = mediaGallery.loadLaterSections(batchSize: kLoadBatchSize)
let newSectionCount = self.numberOfSections(in: collectionView)
collectionView.insertSections(IndexSet(oldSectionCount..<newSectionCount))
}
@ -758,23 +754,19 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDe
UIView.performWithoutAnimation {
collectionView.performBatchUpdates({
databaseStorage.read { transaction in
let newSections: Range<Int>
switch direction {
case .before:
newSections = 0..<mediaGallery.loadEarlierSections(batchSize: kLoadBatchSize,
transaction: transaction)
case .after:
let newSectionCount = mediaGallery.loadLaterSections(batchSize: kLoadBatchSize,
transaction: transaction)
let newEnd = mediaGallery.galleryDates.count
newSections = (newEnd - newSectionCount)..<newEnd
case .around:
preconditionFailure() // unused
}
Logger.debug("found new sections: \(newSections)")
collectionView.insertSections(IndexSet(newSections).shifted(by: 1))
let newSections: Range<Int>
switch direction {
case .before:
newSections = 0..<mediaGallery.loadEarlierSections(batchSize: kLoadBatchSize)
case .after:
let newSectionCount = mediaGallery.loadLaterSections(batchSize: kLoadBatchSize)
let newEnd = mediaGallery.galleryDates.count
newSections = (newEnd - newSectionCount)..<newEnd
case .around:
preconditionFailure() // unused
}
Logger.debug("found new sections: \(newSections)")
collectionView.insertSections(IndexSet(newSections).shifted(by: 1))
}, completion: { finished in
Logger.debug("performBatchUpdates finished: \(finished)")
self.isFetchingMoreData = false

View File

@ -92,11 +92,12 @@ class JournalingOrderedDictionaryTest: SignalBaseTest {
XCTAssertEqual(sut.journal, [.removeAll])
}
func testEraseJournal() {
func testTakeJournal() {
var sut = JournalingOrderedDictionary<Letter, String, StringChange>()
sut.append(key: .a, value: "a")
sut.append(key: .b, value: "b")
sut.eraseJournal()
let journal = sut.takeJournal()
XCTAssertEqual(journal, [.append, .append])
XCTAssertEqual(sut.orderedKeys, [.a, .b])
XCTAssertEqual(sut[.a], "a")
XCTAssertEqual(sut[.b], "b")

File diff suppressed because it is too large Load Diff

2
ThirdParty/RingRTC vendored

@ -1 +1 @@
Subproject commit 965213f6e5cacf1cc2ea23f354061a63a137cd65
Subproject commit a081914ee7fc02bd905a27accea47ad76017c61e

2
ThirdParty/WebRTC vendored

@ -1 +1 @@
Subproject commit d20b610393af19f96f42137f2c044e5bdcc50768
Subproject commit 7550f3bd317002fd77eacebb7f7305f501b4a19a