perf(ui): reduce session list churn and message block invalidation
This commit is contained in:
@@ -875,7 +875,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
||||||
<SessionList
|
<SessionList
|
||||||
instanceId={props.instance.id}
|
instanceId={props.instance.id}
|
||||||
sessions={allInstanceSessions()}
|
|
||||||
threads={sessionThreads()}
|
threads={sessionThreads()}
|
||||||
activeSessionId={activeSessionIdForInstance()}
|
activeSessionId={activeSessionIdForInstance()}
|
||||||
onSelect={handleSessionSelect}
|
onSelect={handleSessionSelect}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
||||||
import { FoldVertical } from "lucide-solid"
|
import { FoldVertical } from "lucide-solid"
|
||||||
import MessageItem from "./message-item"
|
import MessageItem from "./message-item"
|
||||||
import ToolCall from "./tool-call"
|
import ToolCall from "./tool-call"
|
||||||
@@ -235,16 +235,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
const index = props.messageIndex
|
const index = props.messageIndex
|
||||||
const lastAssistantIdx = props.lastAssistantIndex()
|
const lastAssistantIdx = props.lastAssistantIndex()
|
||||||
const isQueued = current.role === "user" && (lastAssistantIdx === -1 || index > lastAssistantIdx)
|
const isQueued = current.role === "user" && (lastAssistantIdx === -1 || index > lastAssistantIdx)
|
||||||
const info = messageInfo()
|
|
||||||
const infoTime = (info?.time ?? {}) as { created?: number; updated?: number; completed?: number }
|
// Intentionally untracked: messageInfoVersion updates should not trigger
|
||||||
const infoTimestamp =
|
// a full message block rebuild; record revision is the invalidation key.
|
||||||
typeof infoTime.completed === "number"
|
const info = untrack(messageInfo)
|
||||||
? infoTime.completed
|
|
||||||
: typeof infoTime.updated === "number"
|
|
||||||
? infoTime.updated
|
|
||||||
: infoTime.created ?? 0
|
|
||||||
const infoError = (info as { error?: { name?: string } } | undefined)?.error
|
|
||||||
const infoErrorName = typeof infoError?.name === "string" ? infoError.name : ""
|
|
||||||
const cacheSignature = [
|
const cacheSignature = [
|
||||||
current.id,
|
current.id,
|
||||||
current.revision,
|
current.revision,
|
||||||
@@ -252,8 +247,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
props.showThinking() ? 1 : 0,
|
props.showThinking() ? 1 : 0,
|
||||||
props.thinkingDefaultExpanded() ? 1 : 0,
|
props.thinkingDefaultExpanded() ? 1 : 0,
|
||||||
props.showUsageMetrics() ? 1 : 0,
|
props.showUsageMetrics() ? 1 : 0,
|
||||||
infoTimestamp,
|
|
||||||
infoErrorName,
|
|
||||||
].join("|")
|
].join("|")
|
||||||
|
|
||||||
const cachedBlock = sessionCache.messageBlocks.get(current.id)
|
const cachedBlock = sessionCache.messageBlocks.get(current.id)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
||||||
import type { Session, SessionStatus } from "../types/session"
|
import type { SessionStatus } from "../types/session"
|
||||||
import type { SessionThread } from "../stores/session-state"
|
import type { SessionThread } from "../stores/session-state"
|
||||||
import { getSessionStatus } from "../stores/session-status"
|
import { getSessionStatus } from "../stores/session-status"
|
||||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
isSessionParentExpanded,
|
isSessionParentExpanded,
|
||||||
loading,
|
loading,
|
||||||
renameSession,
|
renameSession,
|
||||||
|
sessions as sessionStateSessions,
|
||||||
setActiveSessionFromList,
|
setActiveSessionFromList,
|
||||||
toggleSessionParentExpanded,
|
toggleSessionParentExpanded,
|
||||||
} from "../stores/sessions"
|
} from "../stores/sessions"
|
||||||
@@ -25,7 +26,6 @@ const log = getLogger("session")
|
|||||||
|
|
||||||
interface SessionListProps {
|
interface SessionListProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessions: Map<string, Session>
|
|
||||||
threads: SessionThread[]
|
threads: SessionThread[]
|
||||||
activeSessionId: string | null
|
activeSessionId: string | null
|
||||||
onSelect: (sessionId: string) => void
|
onSelect: (sessionId: string) => void
|
||||||
@@ -58,7 +58,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
|
|
||||||
|
|
||||||
const selectSession = (sessionId: string) => {
|
const selectSession = (sessionId: string) => {
|
||||||
const session = props.sessions.get(sessionId)
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
const parentId = session?.parentId ?? session?.id
|
const parentId = session?.parentId ?? session?.id
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
ensureSessionParentExpanded(props.instanceId, parentId)
|
ensureSessionParentExpanded(props.instanceId, parentId)
|
||||||
@@ -132,7 +132,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openRenameDialog = (sessionId: string) => {
|
const openRenameDialog = (sessionId: string) => {
|
||||||
const session = props.sessions.get(sessionId)
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
const label = session.title && session.title.trim() ? session.title : sessionId
|
const label = session.title && session.title.trim() ? session.title : sessionId
|
||||||
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
||||||
@@ -167,7 +167,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
onToggleExpand?: () => void
|
onToggleExpand?: () => void
|
||||||
}> = (rowProps) => {
|
}> = (rowProps) => {
|
||||||
const session = () => props.sessions.get(rowProps.sessionId)
|
const session = createMemo(() => sessionStateSessions().get(props.instanceId)?.get(rowProps.sessionId))
|
||||||
if (!session()) {
|
if (!session()) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const activeId = props.activeSessionId
|
const activeId = props.activeSessionId
|
||||||
if (!activeId || activeId === "info") return null
|
if (!activeId || activeId === "info") return null
|
||||||
|
|
||||||
const activeSession = props.sessions.get(activeId)
|
const activeSession = sessionStateSessions().get(props.instanceId)?.get(activeId)
|
||||||
if (!activeSession) return null
|
if (!activeSession) return null
|
||||||
|
|
||||||
return activeSession.parentId ?? activeSession.id
|
return activeSession.parentId ?? activeSession.id
|
||||||
|
|||||||
@@ -240,8 +240,20 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
const messageId = typeof info.id === "string" ? info.id : undefined
|
const messageId = typeof info.id === "string" ? info.id : undefined
|
||||||
if (!sessionId || !messageId) return
|
if (!sessionId || !messageId) return
|
||||||
|
|
||||||
|
const timeInfo = (info.time ?? {}) as { created?: number; updated?: number; completed?: number }
|
||||||
|
const nextUpdated =
|
||||||
|
typeof timeInfo.completed === "number" && timeInfo.completed > 0
|
||||||
|
? timeInfo.completed
|
||||||
|
: typeof timeInfo.updated === "number" && timeInfo.updated > 0
|
||||||
|
? timeInfo.updated
|
||||||
|
: typeof timeInfo.created === "number" && timeInfo.created > 0
|
||||||
|
? timeInfo.created
|
||||||
|
: Date.now()
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
session.time = { ...(session.time ?? {}), updated: Date.now() }
|
const currentUpdated = session.time?.updated ?? 0
|
||||||
|
if (nextUpdated <= currentUpdated) return false
|
||||||
|
session.time = { ...(session.time ?? {}), updated: nextUpdated }
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
const store = messageStoreBus.getOrCreate(instanceId)
|
||||||
|
|||||||
@@ -390,9 +390,35 @@ function getSessionFamily(instanceId: string, parentId: string): Session[] {
|
|||||||
return [parent, ...children]
|
return [parent, ...children]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionThreadCacheEntry = {
|
||||||
|
signature: string
|
||||||
|
thread: SessionThread
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionThreadCache = {
|
||||||
|
byParentId: Map<string, SessionThreadCacheEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionThreadCache = new Map<string, SessionThreadCache>()
|
||||||
|
|
||||||
|
function getOrCreateSessionThreadCache(instanceId: string): SessionThreadCache {
|
||||||
|
let cache = sessionThreadCache.get(instanceId)
|
||||||
|
if (!cache) {
|
||||||
|
cache = { byParentId: new Map() }
|
||||||
|
sessionThreadCache.set(instanceId, cache)
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
function getSessionThreads(instanceId: string): SessionThread[] {
|
function getSessionThreads(instanceId: string): SessionThread[] {
|
||||||
const instanceSessions = sessions().get(instanceId)
|
const instanceSessions = sessions().get(instanceId)
|
||||||
if (!instanceSessions || instanceSessions.size === 0) return []
|
if (!instanceSessions || instanceSessions.size === 0) {
|
||||||
|
sessionThreadCache.delete(instanceId)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache = getOrCreateSessionThreadCache(instanceId)
|
||||||
|
const seenParents = new Set<string>()
|
||||||
|
|
||||||
const parents: Session[] = []
|
const parents: Session[] = []
|
||||||
const childrenByParent = new Map<string, Session[]>()
|
const childrenByParent = new Map<string, Session[]>()
|
||||||
@@ -416,6 +442,8 @@ function getSessionThreads(instanceId: string): SessionThread[] {
|
|||||||
const threads: SessionThread[] = []
|
const threads: SessionThread[] = []
|
||||||
|
|
||||||
for (const parent of parents) {
|
for (const parent of parents) {
|
||||||
|
seenParents.add(parent.id)
|
||||||
|
|
||||||
const children = childrenByParent.get(parent.id) ?? []
|
const children = childrenByParent.get(parent.id) ?? []
|
||||||
if (children.length > 1) {
|
if (children.length > 1) {
|
||||||
children.sort((a, b) => (b.time.updated ?? 0) - (a.time.updated ?? 0))
|
children.sort((a, b) => (b.time.updated ?? 0) - (a.time.updated ?? 0))
|
||||||
@@ -425,7 +453,23 @@ function getSessionThreads(instanceId: string): SessionThread[] {
|
|||||||
const latestChild = children[0]?.time.updated ?? 0
|
const latestChild = children[0]?.time.updated ?? 0
|
||||||
const latestUpdated = Math.max(parentUpdated, latestChild)
|
const latestUpdated = Math.max(parentUpdated, latestChild)
|
||||||
|
|
||||||
threads.push({ parent, children, latestUpdated })
|
const childIds = children.map((child) => child.id).join(",")
|
||||||
|
const signature = `${parentUpdated}:${latestChild}:${childIds}`
|
||||||
|
|
||||||
|
const cached = cache.byParentId.get(parent.id)
|
||||||
|
if (cached && cached.signature === signature) {
|
||||||
|
threads.push(cached.thread)
|
||||||
|
} else {
|
||||||
|
const thread: SessionThread = { parent, children, latestUpdated }
|
||||||
|
cache.byParentId.set(parent.id, { signature, thread })
|
||||||
|
threads.push(thread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parentId of Array.from(cache.byParentId.keys())) {
|
||||||
|
if (!seenParents.has(parentId)) {
|
||||||
|
cache.byParentId.delete(parentId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
threads.sort((a, b) => {
|
threads.sort((a, b) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user