perf(export): prioritize visible content stats loading

This commit is contained in:
tisonhuang
2026-03-04 16:33:06 +08:00
parent 64542f2902
commit d476fbbdae

View File

@@ -471,6 +471,9 @@ const EXPORT_CARD_DIAG_POLL_INTERVAL_MS = 1200
const EXPORT_REENTER_SESSION_SOFT_REFRESH_MS = 5 * 60 * 1000 const EXPORT_REENTER_SESSION_SOFT_REFRESH_MS = 5 * 60 * 1000
const EXPORT_REENTER_CONTACTS_SOFT_REFRESH_MS = 5 * 60 * 1000 const EXPORT_REENTER_CONTACTS_SOFT_REFRESH_MS = 5 * 60 * 1000
const EXPORT_REENTER_SNS_SOFT_REFRESH_MS = 3 * 60 * 1000 const EXPORT_REENTER_SNS_SOFT_REFRESH_MS = 3 * 60 * 1000
const EXPORT_CONTENT_STATS_FIRST_SCREEN_LIMIT = 120
const EXPORT_CONTENT_STATS_CHUNK_SIZE = 80
const EXPORT_CONTENT_STATS_CHUNK_CONCURRENCY = 2
type SessionDataSource = 'cache' | 'network' | null type SessionDataSource = 'cache' | 'network' | null
type ContactsDataSource = 'cache' | 'network' | null type ContactsDataSource = 'cache' | 'network' | null
@@ -539,6 +542,11 @@ interface SessionContentMetric {
emojiMessages?: number emojiMessages?: number
} }
interface SessionContentStatsProgress {
completed: number
total: number
}
interface SessionExportCacheMeta { interface SessionExportCacheMeta {
updatedAt: number updatedAt: number
stale: boolean stale: boolean
@@ -869,6 +877,7 @@ function ExportPage() {
const [isLoadingSessionCounts, setIsLoadingSessionCounts] = useState(false) const [isLoadingSessionCounts, setIsLoadingSessionCounts] = useState(false)
const [sessionContentMetrics, setSessionContentMetrics] = useState<Record<string, SessionContentMetric>>({}) const [sessionContentMetrics, setSessionContentMetrics] = useState<Record<string, SessionContentMetric>>({})
const [isLoadingSessionContentStats, setIsLoadingSessionContentStats] = useState(false) const [isLoadingSessionContentStats, setIsLoadingSessionContentStats] = useState(false)
const [sessionContentStatsProgress, setSessionContentStatsProgress] = useState<SessionContentStatsProgress>({ completed: 0, total: 0 })
const [contactsListScrollTop, setContactsListScrollTop] = useState(0) const [contactsListScrollTop, setContactsListScrollTop] = useState(0)
const [contactsListViewportHeight, setContactsListViewportHeight] = useState(480) const [contactsListViewportHeight, setContactsListViewportHeight] = useState(480)
const [contactsLoadTimeoutMs, setContactsLoadTimeoutMs] = useState(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS) const [contactsLoadTimeoutMs, setContactsLoadTimeoutMs] = useState(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS)
@@ -960,6 +969,8 @@ function ExportPage() {
const contactsUpdatedAtRef = useRef<number | null>(null) const contactsUpdatedAtRef = useRef<number | null>(null)
const sessionsHydratedAtRef = useRef(0) const sessionsHydratedAtRef = useRef(0)
const snsStatsHydratedAtRef = useRef(0) const snsStatsHydratedAtRef = useRef(0)
const filteredContactUsernamesRef = useRef<string[]>([])
const visibleContactUsernamesRef = useRef<string[]>([])
const inProgressSessionIdsRef = useRef<string[]>([]) const inProgressSessionIdsRef = useRef<string[]>([])
const activeTaskCountRef = useRef(0) const activeTaskCountRef = useRef(0)
const hasBaseConfigReadyRef = useRef(false) const hasBaseConfigReadyRef = useRef(false)
@@ -1526,9 +1537,16 @@ function ExportPage() {
const exportableSessions = sourceSessions.filter(session => session.hasSession) const exportableSessions = sourceSessions.filter(session => session.hasSession)
if (exportableSessions.length === 0) { if (exportableSessions.length === 0) {
setIsLoadingSessionContentStats(false) setIsLoadingSessionContentStats(false)
setSessionContentStatsProgress({ completed: 0, total: 0 })
return return
} }
const exportableSessionIdSet = new Set(exportableSessions.map(session => session.username))
const visiblePrioritySessionIds = visibleContactUsernamesRef.current
.filter((sessionId) => exportableSessionIdSet.has(sessionId))
const firstScreenPrioritySessionIds = filteredContactUsernamesRef.current
.filter((sessionId) => exportableSessionIdSet.has(sessionId))
.slice(0, EXPORT_CONTENT_STATS_FIRST_SCREEN_LIMIT)
const prioritizedSessionIds = exportableSessions const prioritizedSessionIds = exportableSessions
.filter(session => session.kind === priorityTab) .filter(session => session.kind === priorityTab)
.map(session => session.username) .map(session => session.username)
@@ -1536,32 +1554,84 @@ function ExportPage() {
const remainingSessionIds = exportableSessions const remainingSessionIds = exportableSessions
.filter(session => !prioritizedSet.has(session.username)) .filter(session => !prioritizedSet.has(session.username))
.map(session => session.username) .map(session => session.username)
const orderedSessionIds = [...prioritizedSessionIds, ...remainingSessionIds] const orderedSessionIds = Array.from(new Set([
...visiblePrioritySessionIds,
...firstScreenPrioritySessionIds,
...prioritizedSessionIds,
...remainingSessionIds
]))
if (orderedSessionIds.length === 0) { if (orderedSessionIds.length === 0) {
setIsLoadingSessionContentStats(false) setIsLoadingSessionContentStats(false)
setSessionContentStatsProgress({ completed: 0, total: 0 })
return return
} }
setIsLoadingSessionContentStats(true) const total = orderedSessionIds.length
try { const processedSessionIds = new Set<string>()
const chunkSize = 80 const markChunkProcessed = (chunk: string[]) => {
for (let i = 0; i < orderedSessionIds.length; i += chunkSize) { for (const sessionId of chunk) {
const chunk = orderedSessionIds.slice(i, i + chunkSize) processedSessionIds.add(sessionId)
if (chunk.length === 0) continue }
const result = await window.electronAPI.chat.getExportSessionStats( if (!isStale()) {
setSessionContentStatsProgress({ completed: processedSessionIds.size, total })
}
}
const runChunk = async (chunk: string[]) => {
if (chunk.length === 0) return
const result = await withTimeout(
window.electronAPI.chat.getExportSessionStats(
chunk, chunk,
{ includeRelations: false, allowStaleCache: true } { includeRelations: false, allowStaleCache: true }
) ),
if (isStale()) return 25000
if (result.success && result.data) { )
mergeSessionContentMetrics(result.data as Record<string, SessionExportMetric | undefined>) if (isStale()) return
} if (result?.success && result.data) {
mergeSessionContentMetrics(result.data as Record<string, SessionExportMetric | undefined>)
} }
markChunkProcessed(chunk)
}
setIsLoadingSessionContentStats(true)
setSessionContentStatsProgress({ completed: 0, total })
try {
const immediateSessionIds = Array.from(new Set([
...visiblePrioritySessionIds,
...firstScreenPrioritySessionIds
])).slice(0, EXPORT_CONTENT_STATS_FIRST_SCREEN_LIMIT)
for (let i = 0; i < immediateSessionIds.length; i += EXPORT_CONTENT_STATS_CHUNK_SIZE) {
const chunk = immediateSessionIds.slice(i, i + EXPORT_CONTENT_STATS_CHUNK_SIZE)
await runChunk(chunk)
if (isStale()) return
}
const remainingIds = orderedSessionIds.filter((sessionId) => !processedSessionIds.has(sessionId))
const remainingChunks: string[][] = []
for (let i = 0; i < remainingIds.length; i += EXPORT_CONTENT_STATS_CHUNK_SIZE) {
const chunk = remainingIds.slice(i, i + EXPORT_CONTENT_STATS_CHUNK_SIZE)
if (chunk.length === 0) continue
remainingChunks.push(chunk)
}
let nextChunkIndex = 0
const workerCount = Math.min(EXPORT_CONTENT_STATS_CHUNK_CONCURRENCY, remainingChunks.length)
await Promise.all(Array.from({ length: workerCount }, async () => {
while (true) {
if (isStale()) return
const index = nextChunkIndex
nextChunkIndex += 1
if (index >= remainingChunks.length) return
await runChunk(remainingChunks[index])
}
}))
} catch (error) { } catch (error) {
console.error('导出页加载会话内容统计失败:', error) console.error('导出页加载会话内容统计失败:', error)
} finally { } finally {
if (!isStale()) { if (!isStale()) {
setSessionContentStatsProgress({ completed: processedSessionIds.size, total })
setIsLoadingSessionContentStats(false) setIsLoadingSessionContentStats(false)
} }
} }
@@ -1663,6 +1733,7 @@ function ExportPage() {
setSessionContentMetrics({}) setSessionContentMetrics({})
setIsLoadingSessionCounts(false) setIsLoadingSessionCounts(false)
setIsLoadingSessionContentStats(false) setIsLoadingSessionContentStats(false)
setSessionContentStatsProgress({ completed: 0, total: 0 })
const isStale = () => sessionLoadTokenRef.current !== loadToken const isStale = () => sessionLoadTokenRef.current !== loadToken
@@ -1943,6 +2014,7 @@ function ExportPage() {
setIsSessionEnriching(false) setIsSessionEnriching(false)
setIsLoadingSessionCounts(false) setIsLoadingSessionCounts(false)
setIsLoadingSessionContentStats(false) setIsLoadingSessionContentStats(false)
setSessionContentStatsProgress({ completed: 0, total: 0 })
}, [isExportRoute]) }, [isExportRoute])
useEffect(() => { useEffect(() => {
@@ -3425,6 +3497,14 @@ function ExportPage() {
return filteredContacts.slice(contactStartIndex, contactEndIndex) return filteredContacts.slice(contactStartIndex, contactEndIndex)
}, [filteredContacts, contactStartIndex, contactEndIndex]) }, [filteredContacts, contactStartIndex, contactEndIndex])
useEffect(() => {
filteredContactUsernamesRef.current = filteredContacts.map(contact => contact.username)
}, [filteredContacts])
useEffect(() => {
visibleContactUsernamesRef.current = visibleContacts.map(contact => contact.username)
}, [visibleContacts])
const onContactsListScroll = useCallback((event: UIEvent<HTMLDivElement>) => { const onContactsListScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
setContactsListScrollTop(event.currentTarget.scrollTop) setContactsListScrollTop(event.currentTarget.scrollTop)
}, []) }, [])
@@ -4134,7 +4214,7 @@ function ExportPage() {
{isLoadingSessionContentStats && ( {isLoadingSessionContentStats && (
<span className="meta-item syncing"> <span className="meta-item syncing">
<Loader2 size={12} className="spin" /> <Loader2 size={12} className="spin" />
/// ///{sessionContentStatsProgress.completed}/{sessionContentStatsProgress.total}
</span> </span>
)} )}
</div> </div>