Skip to content

Commit

Permalink
fix(crash): introduced throttling for UI updates to avoid UI freeze (#8)
Browse files Browse the repository at this point in the history
* fix(freeze): introduce throttler to updating UI to avoid crashes
  • Loading branch information
AugustDev authored Dec 29, 2023
1 parent 974f317 commit b31e229
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 66 deletions.
8 changes: 6 additions & 2 deletions Enchanted.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
FF10027A2B27B6070011A4DC /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = FF1002792B27B6070011A4DC /* MarkdownUI */; };
FF5FA0D62B35169400BC471D /* Binding+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5FA0D52B35169400BC471D /* Binding+Extension.swift */; };
FF61C9632B2A7DC6003CD1CB /* OllamaKit in Frameworks */ = {isa = PBXBuildFile; productRef = FF61C9622B2A7DC6003CD1CB /* OllamaKit */; };
FF6B7B132B3EE7AC00E8FEA3 /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6B7B122B3EE7AC00E8FEA3 /* Throttler.swift */; };
FFB0327D2B312F3D0066A9DB /* RecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0327C2B312F3D0066A9DB /* RecordingView.swift */; };
FFBBF4842B34881B008D611C /* SpeechRecogniser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBBF4832B34881B008D611C /* SpeechRecogniser.swift */; };
FFBBF4882B34F9C8008D611C /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBBF4872B34F9C8008D611C /* View+Extension.swift */; };
Expand Down Expand Up @@ -74,6 +75,7 @@
FF1002722B276EC10011A4DC /* AppColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColorScheme.swift; sourceTree = "<group>"; };
FF1002742B278C170011A4DC /* AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStore.swift; sourceTree = "<group>"; };
FF5FA0D52B35169400BC471D /* Binding+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extension.swift"; sourceTree = "<group>"; };
FF6B7B122B3EE7AC00E8FEA3 /* Throttler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttler.swift; sourceTree = "<group>"; };
FFB0327C2B312F3D0066A9DB /* RecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingView.swift; sourceTree = "<group>"; };
FFBBF4822B348345008D611C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFBBF4832B34881B008D611C /* SpeechRecogniser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechRecogniser.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -263,6 +265,7 @@
FF10023D2B24F4900011A4DC /* OllamaService.swift */,
FF10024F2B25C79F0011A4DC /* SwiftDataService.swift */,
FF0146CC2B3DADCA00A2A9F6 /* HapticsService.swift */,
FF6B7B122B3EE7AC00E8FEA3 /* Throttler.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -391,6 +394,7 @@
FF1002392B24BBF20011A4DC /* Chat.swift in Sources */,
FFB0327D2B312F3D0066A9DB /* RecordingView.swift in Sources */,
FF10024A2B25BE740011A4DC /* LanguageModelSD.swift in Sources */,
FF6B7B132B3EE7AC00E8FEA3 /* Throttler.swift in Sources */,
FF1002402B24F7320011A4DC /* SideBarMenuView.swift in Sources */,
FFBBF4882B34F9C8008D611C /* View+Extension.swift in Sources */,
FFBBF4842B34881B008D611C /* SpeechRecogniser.swift in Sources */,
Expand Down Expand Up @@ -564,7 +568,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = subj.Enchanted;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down Expand Up @@ -609,7 +613,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = subj.Enchanted;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,4 @@
uuid = "F86A12CB-2FB3-442A-8D63-13A618A57A08"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "94A85A32-A41A-49F5-8A96-8582F50E6F3A"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Enchanted/UI/Views/Sidebar/SidebarView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "5"
endingLineNumber = "5"
landmarkName = "unknown"
landmarkType = "0">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "93592730-88C1-4814-A19F-6389E78CABAE"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Enchanted/UI/Views/Settings/SettingsView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "15"
endingLineNumber = "15"
landmarkName = "SettingsView"
landmarkType = "14">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>
33 changes: 33 additions & 0 deletions Enchanted/Services/Throttler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Throttler.swift
// Enchanted
//
// Created by Augustinas Malinauskas on 29/12/2023.
//

import Foundation

class Throttler {
private var workItem: DispatchWorkItem?
private var lastRun: Date = .distantPast
private let queue: DispatchQueue
private let delay: TimeInterval

init(delay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) {
self.delay = delay
self.queue = queue
}

func throttle(_ block: @escaping () -> Void) {
workItem?.cancel()

let item = DispatchWorkItem { [weak self] in
self?.lastRun = Date()
block()
}
self.workItem = item

let delayFactor = Date().timeIntervalSince(lastRun) >= delay ? 0 : delay
queue.asyncAfter(deadline: .now() + delayFactor, execute: item)
}
}
55 changes: 34 additions & 21 deletions Enchanted/Stores/ConversationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ final class ConversationStore {
private var swiftDataService: SwiftDataService
private var generation: AnyCancellable?

/// For some reason (SwiftUI bug / too frequent UI updates) updating UI for each stream message sometimes freezes the UI.
/// Throttling UI updates seem to fix the issue.
private var currentMessageBuffer: String = ""
private let throttler = Throttler(delay: 0.25)

var conversationState: ConversationState = .completed
var conversations: [ConversationSD] = []
var selectedConversation: ConversationSD?
var messages: [MessageSD] = []
@MainActor var messages: [MessageSD] = []

init(swiftDataService: SwiftDataService) {
self.swiftDataService = swiftDataService
Expand All @@ -36,12 +41,12 @@ final class ConversationStore {
try swiftDataService.createConversation(conversation)
}

func reloadConversation(_ conversation: ConversationSD) throws {
@MainActor func reloadConversation(_ conversation: ConversationSD) throws {
selectedConversation = try swiftDataService.getConversation(conversation.id)
messages = try swiftDataService.fetchMessages(conversation.id)
}

func selectConversation(_ conversation: ConversationSD) throws {
@MainActor func selectConversation(_ conversation: ConversationSD) throws {
try reloadConversation(conversation)
}

Expand All @@ -50,8 +55,8 @@ final class ConversationStore {
conversations = try swiftDataService.fetchConversations()
}

// @MainActor
func stopGenerate() {
// @MainActor
@MainActor func stopGenerate() {
generation?.cancel()
handleComplete()
withAnimation {
Expand All @@ -75,11 +80,11 @@ final class ConversationStore {

let userMessage = MessageSD(content: userPrompt, role: "user", image: image?.render()?.compressImageData())
userMessage.conversation = conversation

var messageHistory = conversation.messages
.sorted{$0.createdAt < $1.createdAt}
.map{ChatMessage(role: $0.role, content: $0.content)
}
}

/// attach selected image to the last Message
if let lastMessage = messageHistory.popLast() {
Expand Down Expand Up @@ -112,7 +117,7 @@ final class ConversationStore {
case .finished:
self?.handleComplete()
case .failure(let error):
self?.handleError(error.localizedDescription)
self?.handleError(error.localizedDescription)
}
}, receiveValue: { [weak self] response in
self?.handleReceive(response)
Expand All @@ -123,38 +128,46 @@ final class ConversationStore {
}
}

// @MainActor
@MainActor
private func handleReceive(_ response: OKChatResponse) {
DispatchQueue.main.async { [self] in
if messages.isEmpty { return }
if messages.isEmpty { return }

if let responseContent = response.message?.content {
currentMessageBuffer = currentMessageBuffer + responseContent

let lastIndex = messages.count - 1
let currentContent = messages[lastIndex].content

if let responseContent = response.message?.content {
messages[lastIndex].content = currentContent + responseContent
throttler.throttle { [weak self] in
guard let self = self else { return }
let lastIndex = self.messages.count - 1
self.messages[lastIndex].content.append(currentMessageBuffer)
currentMessageBuffer = ""
}
conversationState = .loading
}
}

// @MainActor
@MainActor
private func handleError(_ errorMessage: String) {
guard let lastMesasge = messages.last else { return }
lastMesasge.error = true
lastMesasge.done = false
try? swiftDataService.updateMessage(lastMesasge)

Task(priority: .background) {
try? swiftDataService.updateMessage(lastMesasge)
}

withAnimation {
conversationState = .error(message: errorMessage)
}
}

// @MainActor
@MainActor
private func handleComplete() {
guard let lastMesasge = messages.last else { return }
lastMesasge.error = false
lastMesasge.done = true
try? swiftDataService.updateMessage(lastMesasge)

Task(priority: .background) {
try self.swiftDataService.updateMessage(lastMesasge)
}

withAnimation {
conversationState = .completed
Expand Down
13 changes: 4 additions & 9 deletions Enchanted/UI/Views/Chat/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,16 @@ struct Chat: View {

func onConversationTap(_ conversation: ConversationSD) {
withAnimation(.bouncy(duration: 0.3)) {
do {
try conversationStore.selectConversation(conversation)
Task {
await languageModelStore.setModel(model: conversation.model)
}
} catch {

Task {
try await conversationStore.selectConversation(conversation)
await languageModelStore.setModel(model: conversation.model)
}
showMenu.toggle()
}
Haptics.shared.play(.medium)
}

// @MainActor
func onStopGenerateTap() {
@MainActor func onStopGenerateTap() {
conversationStore.stopGenerate()
Haptics.shared.play(.medium)
}
Expand Down

0 comments on commit b31e229

Please sign in to comment.