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">
|
||||
<SessionList
|
||||
instanceId={props.instance.id}
|
||||
sessions={allInstanceSessions()}
|
||||
threads={sessionThreads()}
|
||||
activeSessionId={activeSessionIdForInstance()}
|
||||
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 MessageItem from "./message-item"
|
||||
import ToolCall from "./tool-call"
|
||||
@@ -235,16 +235,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
const index = props.messageIndex
|
||||
const lastAssistantIdx = props.lastAssistantIndex()
|
||||
const isQueued = current.role === "user" && (lastAssistantIdx === -1 || index > lastAssistantIdx)
|
||||
const info = messageInfo()
|
||||
const infoTime = (info?.time ?? {}) as { created?: number; updated?: number; completed?: number }
|
||||
const infoTimestamp =
|
||||
typeof infoTime.completed === "number"
|
||||
? 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 : ""
|
||||
|
||||
// Intentionally untracked: messageInfoVersion updates should not trigger
|
||||
// a full message block rebuild; record revision is the invalidation key.
|
||||
const info = untrack(messageInfo)
|
||||
|
||||
const cacheSignature = [
|
||||
current.id,
|
||||
current.revision,
|
||||
@@ -252,8 +247,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
props.showThinking() ? 1 : 0,
|
||||
props.thinkingDefaultExpanded() ? 1 : 0,
|
||||
props.showUsageMetrics() ? 1 : 0,
|
||||
infoTimestamp,
|
||||
infoErrorName,
|
||||
].join("|")
|
||||
|
||||
const cachedBlock = sessionCache.messageBlocks.get(current.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { getSessionStatus } from "../stores/session-status"
|
||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isSessionParentExpanded,
|
||||
loading,
|
||||
renameSession,
|
||||
sessions as sessionStateSessions,
|
||||
setActiveSessionFromList,
|
||||
toggleSessionParentExpanded,
|
||||
} from "../stores/sessions"
|
||||
@@ -25,7 +26,6 @@ const log = getLogger("session")
|
||||
|
||||
interface SessionListProps {
|
||||
instanceId: string
|
||||
sessions: Map<string, Session>
|
||||
threads: SessionThread[]
|
||||
activeSessionId: string | null
|
||||
onSelect: (sessionId: string) => void
|
||||
@@ -58,7 +58,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
|
||||
|
||||
const selectSession = (sessionId: string) => {
|
||||
const session = props.sessions.get(sessionId)
|
||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||
const parentId = session?.parentId ?? session?.id
|
||||
if (parentId) {
|
||||
ensureSessionParentExpanded(props.instanceId, parentId)
|
||||
@@ -132,7 +132,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
}
|
||||
|
||||
const openRenameDialog = (sessionId: string) => {
|
||||
const session = props.sessions.get(sessionId)
|
||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||
if (!session) return
|
||||
const label = session.title && session.title.trim() ? session.title : sessionId
|
||||
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
||||
@@ -167,7 +167,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
expanded?: boolean
|
||||
onToggleExpand?: () => void
|
||||
}> = (rowProps) => {
|
||||
const session = () => props.sessions.get(rowProps.sessionId)
|
||||
const session = createMemo(() => sessionStateSessions().get(props.instanceId)?.get(rowProps.sessionId))
|
||||
if (!session()) {
|
||||
return <></>
|
||||
}
|
||||
@@ -293,7 +293,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const activeId = props.activeSessionId
|
||||
if (!activeId || activeId === "info") return null
|
||||
|
||||
const activeSession = props.sessions.get(activeId)
|
||||
const activeSession = sessionStateSessions().get(props.instanceId)?.get(activeId)
|
||||
if (!activeSession) return null
|
||||
|
||||
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
|
||||
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) => {
|
||||
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)
|
||||
|
||||
@@ -390,9 +390,35 @@ function getSessionFamily(instanceId: string, parentId: string): Session[] {
|
||||
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[] {
|
||||
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 childrenByParent = new Map<string, Session[]>()
|
||||
@@ -416,6 +442,8 @@ function getSessionThreads(instanceId: string): SessionThread[] {
|
||||
const threads: SessionThread[] = []
|
||||
|
||||
for (const parent of parents) {
|
||||
seenParents.add(parent.id)
|
||||
|
||||
const children = childrenByParent.get(parent.id) ?? []
|
||||
if (children.length > 1) {
|
||||
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 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) => {
|
||||
|
||||
Reference in New Issue
Block a user