Skip to content

Commit a6d4b37

Browse files
committed
perf: batch AI streaming tokens — reduce MainActor hops from ~80/sec to ~12/sec
1 parent c2ffce7 commit a6d4b37

1 file changed

Lines changed: 46 additions & 6 deletions

File tree

TablePro/ViewModels/AIChatViewModel.swift

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ final class AIChatViewModel {
380380

381381
// Capture value types on main actor before detaching
382382
let chatMessages = Array(messages.dropLast())
383+
let assistantIndex = messages.count - 1
383384

384385
streamingTask = Task.detached(priority: .userInitiated) { [weak self] in
385386
do {
@@ -389,17 +390,56 @@ final class AIChatViewModel {
389390
systemPrompt: systemPrompt
390391
)
391392

393+
// Batch tokens off the main actor, flush on interval
394+
var pendingContent = ""
395+
var pendingUsage: AITokenUsage?
396+
let flushInterval: ContinuousClock.Duration = .milliseconds(80)
397+
var lastFlushTime: ContinuousClock.Instant = .now
398+
392399
for try await event in stream {
393400
guard !Task.isCancelled else { break }
401+
switch event {
402+
case .text(let token):
403+
pendingContent += token
404+
case .usage(let usage):
405+
pendingUsage = usage
406+
}
407+
408+
if ContinuousClock.now - lastFlushTime >= flushInterval {
409+
let content = pendingContent
410+
let usage = pendingUsage
411+
pendingContent = ""
412+
pendingUsage = nil
413+
await MainActor.run { [weak self] in
414+
guard let self,
415+
assistantIndex < self.messages.count,
416+
self.messages[assistantIndex].id == assistantID
417+
else { return }
418+
if !content.isEmpty {
419+
self.messages[assistantIndex].content += content
420+
}
421+
if let usage {
422+
self.messages[assistantIndex].usage = usage
423+
}
424+
}
425+
lastFlushTime = .now
426+
}
427+
}
428+
429+
// Final flush — deliver remaining buffered tokens
430+
if !Task.isCancelled, !pendingContent.isEmpty || pendingUsage != nil {
431+
let content = pendingContent
432+
let usage = pendingUsage
394433
await MainActor.run { [weak self] in
395434
guard let self,
396-
let idx = self.messages.firstIndex(where: { $0.id == assistantID })
435+
assistantIndex < self.messages.count,
436+
self.messages[assistantIndex].id == assistantID
397437
else { return }
398-
switch event {
399-
case .text(let token):
400-
self.messages[idx].content += token
401-
case .usage(let usage):
402-
self.messages[idx].usage = usage
438+
if !content.isEmpty {
439+
self.messages[assistantIndex].content += content
440+
}
441+
if let usage {
442+
self.messages[assistantIndex].usage = usage
403443
}
404444
}
405445
}

0 commit comments

Comments
 (0)