Skip to content

Commit 34fe0e1

Browse files
authored
Merge pull request #6200 from Jasper-256/fix-issue-6199-jan-ui-bottlenecks-token-rendering-speed
Fix Issue #6199
2 parents 56fa4f9 + 4ba56f1 commit 34fe0e1

File tree

2 files changed

+72
-32
lines changed

2 files changed

+72
-32
lines changed

web-app/src/hooks/useAppState.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type AppState = {
2121
updateLoadingModel: (loading: boolean) => void
2222
updateTools: (tools: MCPTool[]) => void
2323
setAbortController: (threadId: string, controller: AbortController) => void
24-
updateTokenSpeed: (message: ThreadMessage) => void
24+
updateTokenSpeed: (message: ThreadMessage, increment?: number) => void
2525
resetTokenSpeed: () => void
2626
setOutOfContextDialog: (show: boolean) => void
2727
}
@@ -74,7 +74,7 @@ export const useAppState = create<AppState>()((set) => ({
7474
},
7575
}))
7676
},
77-
updateTokenSpeed: (message) =>
77+
updateTokenSpeed: (message, increment = 1) =>
7878
set((state) => {
7979
const currentTimestamp = new Date().getTime() // Get current time in milliseconds
8080
if (!state.tokenSpeed) {
@@ -83,15 +83,15 @@ export const useAppState = create<AppState>()((set) => ({
8383
tokenSpeed: {
8484
lastTimestamp: currentTimestamp,
8585
tokenSpeed: 0,
86-
tokenCount: 1,
86+
tokenCount: increment,
8787
message: message.id,
8888
},
8989
}
9090
}
9191

9292
const timeDiffInSeconds =
9393
(currentTimestamp - state.tokenSpeed.lastTimestamp) / 1000 // Time difference in seconds
94-
const totalTokenCount = state.tokenSpeed.tokenCount + 1
94+
const totalTokenCount = state.tokenSpeed.tokenCount + increment
9595
const averageTokenSpeed =
9696
totalTokenCount / (timeDiffInSeconds > 0 ? timeDiffInSeconds : 1) // Calculate average token speed
9797
return {

web-app/src/hooks/useChat.ts

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,66 @@ export const useChat = () => {
311311
toolCalls.push(...completion.choices[0].message.tool_calls)
312312
}
313313
} else {
314+
// High-throughput scheduler: batch UI updates on rAF (requestAnimationFrame)
315+
let rafScheduled = false
316+
let rafHandle: number | undefined
317+
let pendingDeltaCount = 0
318+
const scheduleFlush = () => {
319+
if (rafScheduled) return
320+
rafScheduled = true
321+
const doSchedule = (cb: () => void) => {
322+
if (typeof requestAnimationFrame !== 'undefined') {
323+
rafHandle = requestAnimationFrame(() => cb())
324+
} else {
325+
// Fallback for non-browser test environments
326+
const t = setTimeout(() => cb(), 0) as unknown as number
327+
rafHandle = t
328+
}
329+
}
330+
doSchedule(() => {
331+
const currentContent = newAssistantThreadContent(
332+
activeThread.id,
333+
accumulatedText,
334+
{
335+
tool_calls: toolCalls.map((e) => ({
336+
...e,
337+
state: 'pending',
338+
})),
339+
}
340+
)
341+
updateStreamingContent(currentContent)
342+
if (pendingDeltaCount > 0) {
343+
updateTokenSpeed(currentContent, pendingDeltaCount)
344+
}
345+
pendingDeltaCount = 0
346+
rafScheduled = false
347+
})
348+
}
349+
const flushIfPending = () => {
350+
if (!rafScheduled) return
351+
if (typeof cancelAnimationFrame !== 'undefined' && rafHandle !== undefined) {
352+
cancelAnimationFrame(rafHandle)
353+
} else if (rafHandle !== undefined) {
354+
clearTimeout(rafHandle)
355+
}
356+
// Do an immediate flush
357+
const currentContent = newAssistantThreadContent(
358+
activeThread.id,
359+
accumulatedText,
360+
{
361+
tool_calls: toolCalls.map((e) => ({
362+
...e,
363+
state: 'pending',
364+
})),
365+
}
366+
)
367+
updateStreamingContent(currentContent)
368+
if (pendingDeltaCount > 0) {
369+
updateTokenSpeed(currentContent, pendingDeltaCount)
370+
}
371+
pendingDeltaCount = 0
372+
rafScheduled = false
373+
}
314374
for await (const part of completion) {
315375
// Error message
316376
if (!part.choices) {
@@ -323,39 +383,19 @@ export const useChat = () => {
323383
const delta = part.choices[0]?.delta?.content || ''
324384

325385
if (part.choices[0]?.delta?.tool_calls) {
326-
const calls = extractToolCall(part, currentCall, toolCalls)
327-
const currentContent = newAssistantThreadContent(
328-
activeThread.id,
329-
accumulatedText,
330-
{
331-
tool_calls: calls.map((e) => ({
332-
...e,
333-
state: 'pending',
334-
})),
335-
}
336-
)
337-
updateStreamingContent(currentContent)
338-
await new Promise((resolve) => setTimeout(resolve, 0))
386+
extractToolCall(part, currentCall, toolCalls)
387+
// Schedule a flush to reflect tool update
388+
scheduleFlush()
339389
}
340390
if (delta) {
341391
accumulatedText += delta
342-
// Create a new object each time to avoid reference issues
343-
// Use a timeout to prevent React from batching updates too quickly
344-
const currentContent = newAssistantThreadContent(
345-
activeThread.id,
346-
accumulatedText,
347-
{
348-
tool_calls: toolCalls.map((e) => ({
349-
...e,
350-
state: 'pending',
351-
})),
352-
}
353-
)
354-
updateStreamingContent(currentContent)
355-
updateTokenSpeed(currentContent)
356-
await new Promise((resolve) => setTimeout(resolve, 0))
392+
pendingDeltaCount += 1
393+
// Batch UI update on next animation frame
394+
scheduleFlush()
357395
}
358396
}
397+
// Ensure any pending buffered content is rendered at the end
398+
flushIfPending()
359399
}
360400
} catch (error) {
361401
const errorMessage =

0 commit comments

Comments
 (0)