mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #72163 from NousResearch/bb/desktop-render-waste
perf(desktop): stop the statusbar re-rendering per streaming token
This commit is contained in:
commit
8943c9958b
3 changed files with 64 additions and 8 deletions
|
|
@ -12,6 +12,7 @@ import { useI18n } from '@/i18n'
|
|||
import { Activity, AlertCircle, Clock, Command, FolderOpen, Globe, Hash, Loader2, Terminal } from '@/lib/icons'
|
||||
import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
|
||||
import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
|
||||
import { useStoreSelector } from '@/lib/use-session-slice'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { copyFilePath, revealFile } from '@/store/file-actions'
|
||||
import { revealFileInTree } from '@/store/layout'
|
||||
|
|
@ -117,19 +118,30 @@ export function useStatusbarItems({
|
|||
// tile makes the statusbar describe THAT session.
|
||||
const focusedStoredSessionId = useStore($focusedStoredSessionId)
|
||||
const focusedRuntimeId = useStore($focusedRuntimeId)
|
||||
const focusedState = useStore($focusedSessionState)
|
||||
// `$focusedSessionState` is a projection of `$sessionStates`, which is
|
||||
// republished on EVERY message delta — tens of times a second during a turn.
|
||||
// Only three fields are read off it here, so subscribing to the whole object
|
||||
// re-ran this hook (and re-created all ~9 statusbar items) per token. Select
|
||||
// each field individually so an unchanged readout bails out instead.
|
||||
const focusedBusy = useStoreSelector($focusedSessionState, state => Boolean(state?.busy))
|
||||
const focusedTurnStartedAt = useStoreSelector($focusedSessionState, state => state?.turnStartedAt ?? null)
|
||||
// `usage` is an object, so it can't be compared as a scalar. It IS however
|
||||
// replaced wholesale rather than mutated, and only changes when the backend
|
||||
// reports new usage — far rarer than a delta — so its reference is a valid
|
||||
// bail-out key on its own.
|
||||
const focusedUsage = useStoreSelector($focusedSessionState, state => state?.usage ?? null)
|
||||
const sessions = useStore($sessions)
|
||||
const selectedStoredSessionId = useStore($selectedStoredSessionId)
|
||||
const primaryFocused = !focusedStoredSessionId || focusedStoredSessionId === selectedStoredSessionId
|
||||
|
||||
const activeSessionId = primaryFocused ? primaryActiveSessionId : (focusedRuntimeId ?? null)
|
||||
const busy = primaryFocused ? primaryBusy : Boolean(focusedState?.busy)
|
||||
const busy = primaryFocused ? primaryBusy : focusedBusy
|
||||
|
||||
// EMPTY_USAGE (module constant) keeps the fallback referentially stable —
|
||||
// a fresh `{...}` each render would bust the usage-label memos below.
|
||||
const currentUsage = primaryFocused ? primaryUsage : (focusedState?.usage ?? EMPTY_USAGE)
|
||||
const currentUsage = primaryFocused ? primaryUsage : (focusedUsage ?? EMPTY_USAGE)
|
||||
|
||||
const turnStartedAt = primaryFocused ? primaryTurnStartedAt : (focusedState?.turnStartedAt ?? null)
|
||||
const turnStartedAt = primaryFocused ? primaryTurnStartedAt : focusedTurnStartedAt
|
||||
|
||||
// A tile's session-start comes from its stored row (the cache only knows
|
||||
// runtime state); seconds → ms.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type ComponentProps, type ReactNode, useState } from 'react'
|
||||
import { type ComponentProps, memo, type ReactNode, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
|
|
@ -95,7 +95,19 @@ export function StatusbarControls({ className, leftItems = [], items = [], ...pr
|
|||
)
|
||||
}
|
||||
|
||||
function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate: ReturnType<typeof useNavigate> }) {
|
||||
/** Memoized: `useStatusbarItems` rebuilds the item array whenever ANY of its
|
||||
* inputs change, but each individual item object is usually identical across
|
||||
* those rebuilds. Without this, one changed item (the running timer, say)
|
||||
* re-rendered every other item in the bar — measured at 1,446 wasted renders
|
||||
* of 2,174 during a five-tab streaming run. `navigate` is stable for the
|
||||
* router's lifetime, so item identity is the only real input. */
|
||||
const StatusbarItemView = memo(function StatusbarItemView({
|
||||
item,
|
||||
navigate
|
||||
}: {
|
||||
item: StatusbarItem
|
||||
navigate: ReturnType<typeof useNavigate>
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
// Render escape hatch: the contribution owns its own chrome/state/tooltip.
|
||||
|
|
@ -230,4 +242,4 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
|
|||
</button>
|
||||
</Tip>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useSyncExternalStore } from 'react'
|
||||
import { useCallback, useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
interface SliceStore<T> {
|
||||
get(): Record<string, T[] | undefined>
|
||||
|
|
@ -29,3 +29,35 @@ export function useSessionSlice<T>(store: SliceStore<T>, key: string | null): T[
|
|||
() => (key ? (store.get()[key] ?? (EMPTY as unknown as T[])) : (EMPTY as unknown as T[]))
|
||||
)
|
||||
}
|
||||
|
||||
interface ReadableStore<T> {
|
||||
get(): T
|
||||
listen(listener: () => void): () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a SCALAR derived from a hot store, re-rendering only when that
|
||||
* scalar changes by `Object.is` — not on every write to the store it came from.
|
||||
*
|
||||
* `useStore($someHotStore)` bails out on reference equality alone, so a store
|
||||
* republished per streaming token re-renders every consumer even when the two
|
||||
* or three fields they actually read are identical. `$sessionStates` is the
|
||||
* canonical case: it is republished on every message delta, so a component
|
||||
* reading only `busy` or `turnStartedAt` off it pays for the whole transcript's
|
||||
* churn.
|
||||
*
|
||||
* `select` must return a PRIMITIVE (or a referentially stable value). Returning
|
||||
* a fresh object or array defeats the bail-out and reintroduces the churn this
|
||||
* exists to remove — derive one scalar per call instead.
|
||||
*/
|
||||
export function useStoreSelector<T, S>(store: ReadableStore<T>, select: (value: T) => S): S {
|
||||
// `select` is read through a ref so an inline arrow at the call site doesn't
|
||||
// resubscribe on every render; useSyncExternalStore re-reads the snapshot on
|
||||
// each render anyway, so the latest selector is always applied.
|
||||
const selectRef = useRef(select)
|
||||
selectRef.current = select
|
||||
|
||||
const subscribe = useCallback((onChange: () => void) => store.listen(onChange), [store])
|
||||
|
||||
return useSyncExternalStore(subscribe, () => selectRef.current(store.get()))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue