From fb3225c9fb098b432ac8ceebeafc64ef6fbcb392 Mon Sep 17 00:00:00 2001 From: george-signal <98181642+george-signal@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:48:38 -0800 Subject: [PATCH] Run synchronous mutations ahead of all async mutations. --- .../MediaGallery/MediaGallery.swift | 6 ++ .../MediaGallery/MediaGallerySections.swift | 71 ++++++++++++------- .../MediaPageViewController.swift | 2 + .../MediaTileViewController.swift | 4 +- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift b/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift index d5f2d882b9..1ca3d92d5a 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaGallery.swift @@ -425,6 +425,7 @@ class MediaGallery: Dependencies { async: Bool = false, userData: MediaGalleryUpdateUserData? = nil, completion: ((_ newSections: IndexSet) -> Void)? = nil) { + Logger.info("") let anchorItem: MediaGalleryItem? = sections.loadedItem(at: IndexPath(item: itemIndex, section: sectionIndex)) // May include a negative start location. @@ -461,6 +462,7 @@ class MediaGallery: Dependencies { }() if async { + Logger.info("will ensure loaded asynchronously") mutateAsync { sections, callback in sections.asyncEnsureItemsLoaded(in: naiveRequestRange, relativeToSection: sectionIndex, @@ -471,6 +473,7 @@ class MediaGallery: Dependencies { completion?(newlyLoadedSections) } } else { + Logger.info("will ensure loaded synchronously") let newlyLoadedSections = mutate { sections in sections.ensureItemsLoaded(in: naiveRequestRange, relativeToSection: sectionIndex, @@ -497,6 +500,7 @@ class MediaGallery: Dependencies { } internal func ensureLoadedForDetailView(focusedAttachment: TSAttachment) -> MediaGalleryItem? { + Logger.info("") let newItem: MediaGalleryItem? = databaseStorage.read { transaction in guard let focusedItem = buildGalleryItem(attachment: focusedAttachment, transaction: transaction) else { return nil @@ -527,10 +531,12 @@ class MediaGallery: Dependencies { // For a speedy load, we only fetch a few items on either side of // the initial message + Logger.info("ensureGalleryItemsLoaded: will call") ensureGalleryItemsLoaded(.around, item: focusedItem, amount: kGallerySwipeLoadBatchSize * 2, shouldLoadAlbumRemainder: true) + Logger.info("ensureGalleryItemsLoaded: finished") return focusedItem } diff --git a/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift b/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift index 204f30e9ae..e31ffd2893 100644 --- a/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift +++ b/Signal/src/ViewControllers/MediaGallery/MediaGallerySections.swift @@ -499,7 +499,7 @@ internal struct MediaGallerySections IndexSet { - return Bench(title: "fetching gallery items") { + return Bench(title: "ensureItemsLoaded") { guard let naiveRange = resolveLoadItemsRequest(request) else { return IndexSet() } @@ -703,27 +703,29 @@ internal struct MediaGallerySections Void> = AtomicArray() private var lowPriorityStack: AtomicArray<() -> Void> = AtomicArray() + // We use an operation queue because we need to prioritize user-initiated actions over others, + // such as eager fetching. OperationQueue gives us this for free. + private let operationQueue = OperationQueue() + var state: State { dispatchPrecondition(condition: .onQueue(.main)) return snapshot } - init(_ queue: DispatchQueue, state: State) { - self.queue = queue + init(state: State) { + operationQueue.maxConcurrentOperationCount = 1 self.mutableState = state self.snapshot = state } + // Runs on the operation queue. private func _mutate(userData: UpdateUserData?, _ block: (inout State) -> (T)) -> T { - dispatchPrecondition(condition: .onQueue(queue)) - let result = block(&mutableState) let updatedState = mutableState let changes = mutableState.itemsBySection.takeJournal() @@ -737,10 +739,27 @@ internal struct MediaGallerySections(userData: UpdateUserData?, _ block: (inout State) -> (T)) -> T { - dispatchPrecondition(condition: .onQueue(.main)) - return queue.sync { - return _mutate(userData: userData, block) + return Bench(title: "Sync mutation [\(depthReport)]", logInProduction: true) { + dispatchPrecondition(condition: .onQueue(.main)) + var result: T! + withoutActuallyEscaping(block) { escapingClosure in + // This is used to trick Swift into believing that escapingClosure doesn't escape. + var sneakyClosure: ((inout State) -> T)? + sneakyClosure = escapingClosure + let operation = BlockOperation { + result = self._mutate(userData: userData, sneakyClosure!) + } + operation.queuePriority = .veryHigh + operationQueue.addOperation(operation) + operation.waitUntilFinished() + sneakyClosure = nil + } + return result } } @@ -752,7 +771,7 @@ internal struct MediaGallerySections(userData: UpdateUserData?, highPriority: Bool, + title: String?, _ block: @escaping (inout State) -> (T), completion: @escaping (T) -> Void) { dispatchPrecondition(condition: .onQueue(.main)) @@ -767,26 +787,30 @@ internal struct MediaGallerySections(userData: UpdateUserData?, highPriority: Bool, + title: String? = nil, _ block: @escaping (inout State, SDSAnyReadTransaction) -> (T), completion: @escaping (T) -> Void) { - mutateAsync(userData: userData, highPriority: highPriority) { mutableState in + mutateAsync(userData: userData, highPriority: highPriority, title: title) { mutableState in return Self.databaseStorage.read { transaction in block(&mutableState, transaction) } @@ -798,10 +822,9 @@ internal struct MediaGallerySections Void)?) { - snapshotManager.mutateAsync(userData: userData, highPriority: highPriority) { state, transaction in + snapshotManager.mutateAsync(userData: userData, highPriority: highPriority, title: "load earlier section") { state, transaction in return state.loadEarlierSections(batchSize: batchSize, transaction: transaction) } completion: { numberOfSectionsLoaded in completion?(numberOfSectionsLoaded) @@ -846,7 +869,7 @@ internal struct MediaGallerySections Void)?) { - snapshotManager.mutateAsync(userData: userData, highPriority: true) { state, transaction in + snapshotManager.mutateAsync(userData: userData, highPriority: true, title: "load later sections") { state, transaction in return state.loadLaterSections(batchSize: batchSize, transaction: transaction) } completion: { numberOfSectionsLoaded in completion?(numberOfSectionsLoaded) @@ -1100,7 +1123,7 @@ internal struct MediaGallerySections