perf(chat): speed up session switch and stabilize message cursor

This commit is contained in:
tisonhuang
2026-03-04 18:05:00 +08:00
parent ac4482bc8b
commit 7b4aa23f35
3 changed files with 104 additions and 51 deletions

View File

@@ -1435,6 +1435,7 @@ class ChatService {
endTime: number = 0, endTime: number = 0,
ascending: boolean = false ascending: boolean = false
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> { ): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
let releaseMessageCursorMutex: (() => void) | null = null
try { try {
const connectResult = await this.ensureConnected() const connectResult = await this.ensureConnected()
if (!connectResult.success) { if (!connectResult.success) {
@@ -1448,6 +1449,12 @@ class ChatService {
await new Promise(resolve => setTimeout(resolve, 1)) await new Promise(resolve => setTimeout(resolve, 1))
} }
this.messageCursorMutex = true this.messageCursorMutex = true
let mutexReleased = false
releaseMessageCursorMutex = () => {
if (mutexReleased) return
this.messageCursorMutex = false
mutexReleased = true
}
let state = this.messageCursors.get(sessionId) let state = this.messageCursors.get(sessionId)
@@ -1486,7 +1493,7 @@ class ChatService {
state = { cursor: cursorResult.cursor, fetched: 0, batchSize, startTime, endTime, ascending } state = { cursor: cursorResult.cursor, fetched: 0, batchSize, startTime, endTime, ascending }
this.messageCursors.set(sessionId, state) this.messageCursors.set(sessionId, state)
this.messageCursorMutex = false releaseMessageCursorMutex?.()
// 如果需要跳过消息(offset > 0),逐批获取但不返回 // 如果需要跳过消息(offset > 0),逐批获取但不返回
// 注意:仅在 offset === 0 时重建游标最安全; // 注意:仅在 offset === 0 时重建游标最安全;
@@ -1519,7 +1526,6 @@ class ChatService {
} }
skipped += count skipped += count
state.fetched += count
// If satisfied offset, break // If satisfied offset, break
if (skipped >= offset) break; if (skipped >= offset) break;
@@ -1532,6 +1538,7 @@ class ChatService {
if (attempts >= maxSkipAttempts) { if (attempts >= maxSkipAttempts) {
console.error(`[ChatService] 跳过消息超过最大尝试次数: attempts=${attempts}`) console.error(`[ChatService] 跳过消息超过最大尝试次数: attempts=${attempts}`)
} }
state.fetched = offset
console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}, buffered=${state.bufferedMessages?.length || 0}`) console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}, buffered=${state.bufferedMessages?.length || 0}`)
} }
} }
@@ -1557,7 +1564,6 @@ class ChatService {
const nextBatch = await wcdbService.fetchMessageBatch(state.cursor) const nextBatch = await wcdbService.fetchMessageBatch(state.cursor)
if (nextBatch.success && nextBatch.rows) { if (nextBatch.success && nextBatch.rows) {
rows = rows.concat(nextBatch.rows) rows = rows.concat(nextBatch.rows)
state.fetched += nextBatch.rows.length
actualHasMore = nextBatch.hasMore === true actualHasMore = nextBatch.hasMore === true
} else if (!nextBatch.success) { } else if (!nextBatch.success) {
console.error('[ChatService] 获取消息批次失败:', nextBatch.error) console.error('[ChatService] 获取消息批次失败:', nextBatch.error)
@@ -1624,14 +1630,15 @@ class ChatService {
} }
state.fetched += rows.length state.fetched += rows.length
this.messageCursorMutex = false releaseMessageCursorMutex?.()
this.messageCacheService.set(sessionId, filtered) this.messageCacheService.set(sessionId, filtered)
return { success: true, messages: filtered, hasMore } return { success: true, messages: filtered, hasMore }
} catch (e) { } catch (e) {
this.messageCursorMutex = false
console.error('ChatService: 获取消息失败:', e) console.error('ChatService: 获取消息失败:', e)
return { success: false, error: String(e) } return { success: false, error: String(e) }
} finally {
releaseMessageCursorMutex?.()
} }
} }
@@ -1726,7 +1733,7 @@ class ChatService {
} }
async getLatestMessages(sessionId: string, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; error?: string }> { async getLatestMessages(sessionId: string, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
try { try {
const connectResult = await this.ensureConnected() const connectResult = await this.ensureConnected()
if (!connectResult.success) { if (!connectResult.success) {
@@ -1757,7 +1764,7 @@ class ChatService {
await Promise.allSettled(fixPromises) await Promise.allSettled(fixPromises)
} }
return { success: true, messages: normalized } return { success: true, messages: normalized, hasMore: batch.hasMore === true }
} finally { } finally {
await wcdbService.closeMessageCursor(cursorResult.cursor) await wcdbService.closeMessageCursor(cursorResult.cursor)
} }

View File

@@ -290,6 +290,13 @@ interface GroupMembersPanelCacheEntry {
includeMessageCounts: boolean includeMessageCounts: boolean
} }
interface LoadMessagesOptions {
preferLatestPath?: boolean
deferGroupSenderWarmup?: boolean
forceInitialLimit?: number
switchRequestSeq?: number
}
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts // 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts // 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
import { avatarLoadQueue } from '../utils/AvatarLoadQueue' import { avatarLoadQueue } from '../utils/AvatarLoadQueue'
@@ -521,6 +528,8 @@ function ChatPage(_props: ChatPageProps) {
const sessionsRef = useRef<ChatSession[]>([]) const sessionsRef = useRef<ChatSession[]>([])
const currentSessionRef = useRef<string | null>(null) const currentSessionRef = useRef<string | null>(null)
const pendingSessionLoadRef = useRef<string | null>(null) const pendingSessionLoadRef = useRef<string | null>(null)
const sessionSwitchRequestSeqRef = useRef(0)
const initialLoadRequestedSessionRef = useRef<string | null>(null)
const prevSessionRef = useRef<string | null>(null) const prevSessionRef = useRef<string | null>(null)
const isLoadingMessagesRef = useRef(false) const isLoadingMessagesRef = useRef(false)
const isLoadingMoreRef = useRef(false) const isLoadingMoreRef = useRef(false)
@@ -1424,6 +1433,8 @@ function ChatPage(_props: ChatPageProps) {
preloadImageKeysRef.current.clear() preloadImageKeysRef.current.clear()
lastPreloadSessionRef.current = null lastPreloadSessionRef.current = null
pendingSessionLoadRef.current = null pendingSessionLoadRef.current = null
initialLoadRequestedSessionRef.current = null
sessionSwitchRequestSeqRef.current += 1
setIsSessionSwitching(false) setIsSessionSwitching(false)
setSessionDetail(null) setSessionDetail(null)
setIsRefreshingDetailStats(false) setIsRefreshingDetailStats(false)
@@ -1887,32 +1898,61 @@ function ChatPage(_props: ChatPageProps) {
setIsRefreshingMessages(false) setIsRefreshingMessages(false)
} }
} }
// 消息批量大小控制(保持稳定,避免游标反复重建)
// 动态游标批量大小控制
const currentBatchSizeRef = useRef(50) const currentBatchSizeRef = useRef(50)
const warmupGroupSenderProfiles = useCallback((usernames: string[], defer = false) => {
if (!Array.isArray(usernames) || usernames.length === 0) return
const runWarmup = () => {
const batchPromise = loadContactInfoBatch(usernames)
usernames.forEach(username => {
if (!senderAvatarLoading.has(username)) {
senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null))
}
})
batchPromise.finally(() => {
usernames.forEach(username => senderAvatarLoading.delete(username))
})
}
if (defer) {
if ('requestIdleCallback' in window) {
window.requestIdleCallback(() => {
runWarmup()
}, { timeout: 1200 })
} else {
globalThis.setTimeout(runWarmup, 120)
}
return
}
runWarmup()
}, [loadContactInfoBatch])
// 加载消息 // 加载消息
const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0, ascending = false) => { const loadMessages = async (
sessionId: string,
offset = 0,
startTime = 0,
endTime = 0,
ascending = false,
options: LoadMessagesOptions = {}
) => {
const listEl = messageListRef.current const listEl = messageListRef.current
const session = sessionMapRef.current.get(sessionId) const session = sessionMapRef.current.get(sessionId)
const unreadCount = session?.unreadCount ?? 0 const unreadCount = session?.unreadCount ?? 0
let messageLimit = 50 let messageLimit = currentBatchSizeRef.current
if (offset === 0) { if (offset === 0) {
// 初始加载:重置批量大小 const preferredLimit = Number.isFinite(options.forceInitialLimit)
currentBatchSizeRef.current = 50 ? Math.max(10, Math.floor(options.forceInitialLimit as number))
// 首屏优化:消息过多时限制数量 : (unreadCount > 99 ? 30 : 40)
messageLimit = unreadCount > 99 ? 30 : 50 currentBatchSizeRef.current = preferredLimit
messageLimit = preferredLimit
} else { } else {
// 滚动加载:动态递增 (50 -> 100 -> 200) // 同一会话内保持固定批量,避免后端游标因 batch 改变而重建
if (currentBatchSizeRef.current < 100) {
currentBatchSizeRef.current = 100
} else {
currentBatchSizeRef.current = 200
}
messageLimit = currentBatchSizeRef.current messageLimit = currentBatchSizeRef.current
} }
@@ -1929,12 +1969,19 @@ function ChatPage(_props: ChatPageProps) {
const firstMsgEl = listEl?.querySelector('.message-wrapper') as HTMLElement | null const firstMsgEl = listEl?.querySelector('.message-wrapper') as HTMLElement | null
try { try {
const result = await window.electronAPI.chat.getMessages(sessionId, offset, messageLimit, startTime, endTime, ascending) as { const useLatestPath = offset === 0 && startTime === 0 && endTime === 0 && !ascending && options.preferLatestPath
const result = (useLatestPath
? await window.electronAPI.chat.getLatestMessages(sessionId, messageLimit)
: await window.electronAPI.chat.getMessages(sessionId, offset, messageLimit, startTime, endTime, ascending)
) as {
success: boolean; success: boolean;
messages?: Message[]; messages?: Message[];
hasMore?: boolean; hasMore?: boolean;
error?: string error?: string
} }
if (options.switchRequestSeq && options.switchRequestSeq !== sessionSwitchRequestSeqRef.current) {
return
}
if (currentSessionRef.current !== sessionId) { if (currentSessionRef.current !== sessionId) {
return return
} }
@@ -1947,8 +1994,7 @@ function ChatPage(_props: ChatPageProps) {
setHasMoreMessages(false) setHasMoreMessages(false)
} }
// 预取发送者信息:在关闭加载遮罩前处理 // 群聊发送者信息补齐改为非阻塞执行,避免影响首屏切换
const unreadCount = session?.unreadCount ?? 0
const isGroup = sessionId.includes('@chatroom') const isGroup = sessionId.includes('@chatroom')
if (isGroup && result.messages.length > 0) { if (isGroup && result.messages.length > 0) {
const unknownSenders = [...new Set(result.messages const unknownSenders = [...new Set(result.messages
@@ -1956,18 +2002,7 @@ function ChatPage(_props: ChatPageProps) {
.map(m => m.senderUsername as string) .map(m => m.senderUsername as string)
)] )]
if (unknownSenders.length > 0) { if (unknownSenders.length > 0) {
warmupGroupSenderProfiles(unknownSenders, options.deferGroupSenderWarmup === true)
// 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求
const batchPromise = loadContactInfoBatch(unknownSenders)
unknownSenders.forEach(username => {
if (!senderAvatarLoading.has(username)) {
senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null))
}
})
// 确保在请求完成后清理 loading 状态
batchPromise.finally(() => {
unknownSenders.forEach(username => senderAvatarLoading.delete(username))
})
} }
} }
@@ -1993,15 +2028,7 @@ function ChatPage(_props: ChatPageProps) {
.map(m => m.senderUsername as string) .map(m => m.senderUsername as string)
)] )]
if (unknownSenders.length > 0) { if (unknownSenders.length > 0) {
const batchPromise = loadContactInfoBatch(unknownSenders) warmupGroupSenderProfiles(unknownSenders, false)
unknownSenders.forEach(username => {
if (!senderAvatarLoading.has(username)) {
senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null))
}
})
batchPromise.finally(() => {
unknownSenders.forEach(username => senderAvatarLoading.delete(username))
})
} }
} }
@@ -2042,11 +2069,14 @@ function ChatPage(_props: ChatPageProps) {
setLoadingMessages(false) setLoadingMessages(false)
setLoadingMore(false) setLoadingMore(false)
if (offset === 0 && pendingSessionLoadRef.current === sessionId) { if (offset === 0 && pendingSessionLoadRef.current === sessionId) {
if (!options.switchRequestSeq || options.switchRequestSeq === sessionSwitchRequestSeqRef.current) {
pendingSessionLoadRef.current = null pendingSessionLoadRef.current = null
initialLoadRequestedSessionRef.current = null
setIsSessionSwitching(false) setIsSessionSwitching(false)
} }
} }
} }
}
// 加载更晚的消息 // 加载更晚的消息
const loadLaterMessages = useCallback(async () => { const loadLaterMessages = useCallback(async () => {
@@ -2088,7 +2118,10 @@ function ChatPage(_props: ChatPageProps) {
return return
} }
if (session.username === currentSessionId) return if (session.username === currentSessionId) return
const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1
sessionSwitchRequestSeqRef.current = switchRequestSeq
pendingSessionLoadRef.current = session.username pendingSessionLoadRef.current = session.username
initialLoadRequestedSessionRef.current = session.username
setIsSessionSwitching(true) setIsSessionSwitching(true)
setCurrentSession(session.username, { preserveMessages: false }) setCurrentSession(session.username, { preserveMessages: false })
void hydrateSessionPreview(session.username) void hydrateSessionPreview(session.username)
@@ -2096,7 +2129,12 @@ function ChatPage(_props: ChatPageProps) {
setJumpStartTime(0) setJumpStartTime(0)
setJumpEndTime(0) setJumpEndTime(0)
setNoMessageTable(false) setNoMessageTable(false)
void loadMessages(session.username, 0, 0, 0) void loadMessages(session.username, 0, 0, 0, false, {
preferLatestPath: true,
deferGroupSenderWarmup: true,
forceInitialLimit: 30,
switchRequestSeq
})
// 切换会话后回到正常聊天窗口:收起详情侧栏,详情需手动再次展开 // 切换会话后回到正常聊天窗口:收起详情侧栏,详情需手动再次展开
setShowDetailPanel(false) setShowDetailPanel(false)
setShowGroupMembersPanel(false) setShowGroupMembersPanel(false)
@@ -2376,8 +2414,15 @@ function ChatPage(_props: ChatPageProps) {
useEffect(() => { useEffect(() => {
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) { if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) {
if (pendingSessionLoadRef.current === currentSessionId) return
if (initialLoadRequestedSessionRef.current === currentSessionId) return
initialLoadRequestedSessionRef.current = currentSessionId
setHasInitialMessages(false) setHasInitialMessages(false)
loadMessages(currentSessionId, 0) void loadMessages(currentSessionId, 0, 0, 0, false, {
preferLatestPath: true,
deferGroupSenderWarmup: true,
forceInitialLimit: 30
})
} }
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable]) }, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable])

View File

@@ -177,6 +177,7 @@ export interface ElectronAPI {
getLatestMessages: (sessionId: string, limit?: number) => Promise<{ getLatestMessages: (sessionId: string, limit?: number) => Promise<{
success: boolean success: boolean
messages?: Message[] messages?: Message[]
hasMore?: boolean
error?: string error?: string
}> }>
getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{ getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{