mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
refactor(cli): remove renderLogger and add performance optimizations
- Remove unused renderLogger.ts utility file and all its usages - Add RAF-style scroll throttling to reduce state updates - Stabilize useExtensionHost hook return values with useCallback/useMemo - Add streaming message debouncing to batch rapid partial updates - Add shallow array equality checks to prevent unnecessary re-renders
This commit is contained in:
parent
adc1da129c
commit
9ffe49a22b
3 changed files with 164 additions and 54 deletions
|
|
@ -6,7 +6,6 @@ import type { WebviewMessage } from "@roo-code/types"
|
|||
import { getGlobalCommandsForAutocomplete } from "../utils/globalCommands.js"
|
||||
import { arePathsEqual } from "../utils/pathUtils.js"
|
||||
import { getContextWindow } from "../utils/getContextWindow.js"
|
||||
|
||||
import type { AppProps } from "./types.js"
|
||||
import * as theme from "./theme.js"
|
||||
|
||||
|
|
@ -140,10 +139,9 @@ function AppInner({
|
|||
} = useUIStateStore()
|
||||
|
||||
// Compute context window from router models and API configuration
|
||||
const contextWindow = useMemo(
|
||||
() => getContextWindow(routerModels, apiConfiguration),
|
||||
[routerModels, apiConfiguration],
|
||||
)
|
||||
const contextWindow = useMemo(() => {
|
||||
return getContextWindow(routerModels, apiConfiguration)
|
||||
}, [routerModels, apiConfiguration])
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const autocompleteRef = useRef<AutocompleteInputHandle<any>>(null)
|
||||
|
|
@ -175,6 +173,10 @@ function AppInner({
|
|||
const [scrollState, setScrollState] = useState({ scrollTop: 0, maxScroll: 0, isAtBottom: true })
|
||||
const { scrollToBottomTrigger, scrollToBottom } = useScrollToBottom()
|
||||
|
||||
// RAF-style throttle refs for scroll updates (prevents multiple state updates per event loop tick)
|
||||
const rafIdRef = useRef<NodeJS.Immediate | null>(null)
|
||||
const pendingScrollRef = useRef<{ scrollTop: number; maxScroll: number; isAtBottom: boolean } | null>(null)
|
||||
|
||||
// Toast notifications for ephemeral messages (e.g., mode changes)
|
||||
const { currentToast, showInfo } = useToast()
|
||||
|
||||
|
|
@ -275,9 +277,31 @@ function AppInner({
|
|||
prevMessageCount.current = messages.length
|
||||
}, [messages.length, scrollState.isAtBottom, scrollToBottom])
|
||||
|
||||
// Handle scroll state changes from ScrollArea
|
||||
// Handle scroll state changes from ScrollArea (RAF-throttled to coalesce rapid updates)
|
||||
const handleScroll = useCallback((scrollTop: number, maxScroll: number, isAtBottom: boolean) => {
|
||||
setScrollState({ scrollTop, maxScroll, isAtBottom })
|
||||
// Store the latest scroll values in ref
|
||||
pendingScrollRef.current = { scrollTop, maxScroll, isAtBottom }
|
||||
|
||||
// Only schedule one update per event loop tick
|
||||
if (rafIdRef.current === null) {
|
||||
rafIdRef.current = setImmediate(() => {
|
||||
rafIdRef.current = null
|
||||
const pending = pendingScrollRef.current
|
||||
if (pending) {
|
||||
setScrollState(pending)
|
||||
pendingScrollRef.current = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Cleanup RAF-style timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (rafIdRef.current !== null) {
|
||||
clearImmediate(rafIdRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// File search handler for the file trigger
|
||||
|
|
@ -350,17 +374,15 @@ function AppInner({
|
|||
if (fileSearchResults === prevFileSearchResultsRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentPickerState = pickerStateRef.current
|
||||
const willRefresh =
|
||||
currentPickerState.isOpen && currentPickerState.activeTrigger?.id === "file" && fileSearchResults.length > 0
|
||||
|
||||
prevFileSearchResultsRef.current = fileSearchResults
|
||||
|
||||
// Read pickerState from ref to avoid dependency
|
||||
const currentPickerState = pickerStateRef.current
|
||||
|
||||
// Only refresh when file picker is open and we have new results
|
||||
if (
|
||||
currentPickerState.isOpen &&
|
||||
currentPickerState.activeTrigger?.id === "file" &&
|
||||
fileSearchResults.length > 0
|
||||
) {
|
||||
if (willRefresh) {
|
||||
autocompleteRef.current?.refreshSearch()
|
||||
followupAutocompleteRef.current?.refreshSearch()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useCallback } from "react"
|
||||
import { useEffect, useRef, useCallback, useMemo } from "react"
|
||||
import { useApp } from "ink"
|
||||
import { randomUUID } from "crypto"
|
||||
import type { ExtensionMessage, WebviewMessage } from "@roo-code/types"
|
||||
|
|
@ -179,27 +179,28 @@ export function useExtensionHost({
|
|||
}
|
||||
}, []) // Run once on mount
|
||||
|
||||
// Expose sendToExtension method
|
||||
const sendToExtension = hostRef.current
|
||||
? (msg: WebviewMessage) => {
|
||||
hostRef.current?.sendToExtension(msg)
|
||||
}
|
||||
: null
|
||||
// Stable sendToExtension - uses ref to always access current host
|
||||
// This function reference never changes, preventing downstream useCallback/useMemo invalidations
|
||||
const sendToExtension = useCallback((msg: WebviewMessage) => {
|
||||
hostRef.current?.sendToExtension(msg)
|
||||
}, [])
|
||||
|
||||
// Expose runTask method
|
||||
const runTask = hostRef.current
|
||||
? (prompt: string) => {
|
||||
if (!hostRef.current) {
|
||||
return Promise.reject(new Error("Extension host not ready"))
|
||||
}
|
||||
return hostRef.current.runTask(prompt)
|
||||
}
|
||||
: null
|
||||
// Stable runTask - uses ref to always access current host
|
||||
const runTask = useCallback((prompt: string): Promise<void> => {
|
||||
if (!hostRef.current) {
|
||||
return Promise.reject(new Error("Extension host not ready"))
|
||||
}
|
||||
return hostRef.current.runTask(prompt)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
isReady: isReadyRef.current,
|
||||
sendToExtension,
|
||||
runTask,
|
||||
cleanup,
|
||||
}
|
||||
// Memoized return object to prevent unnecessary re-renders in consumers
|
||||
return useMemo(
|
||||
() => ({
|
||||
isReady: isReadyRef.current,
|
||||
sendToExtension,
|
||||
runTask,
|
||||
cleanup,
|
||||
}),
|
||||
[sendToExtension, runTask, cleanup],
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,38 @@ import type { TokenUsage, ProviderSettings, TodoItem } from "@roo-code/types"
|
|||
import type { TUIMessage, PendingAsk, TaskHistoryItem } from "./types.js"
|
||||
import type { FileResult, SlashCommandResult, ModeResult } from "./components/autocomplete/index.js"
|
||||
|
||||
/**
|
||||
* Shallow array equality check - compares array length and element references.
|
||||
* Used to prevent unnecessary state updates when array content hasn't changed.
|
||||
*/
|
||||
function shallowArrayEqual<T>(a: T[], b: T[]): boolean {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming message debounce configuration.
|
||||
* Batches rapid partial message updates to reduce re-renders during streaming.
|
||||
* Higher values = fewer renders but text appears more "chunky"
|
||||
* Lower values = smoother text but more renders
|
||||
*/
|
||||
const STREAMING_DEBOUNCE_MS = 150 // 150ms debounce for aggressive batching
|
||||
|
||||
// Pending streaming updates - batched and flushed after debounce interval
|
||||
interface PendingStreamUpdate {
|
||||
id: string
|
||||
content: string
|
||||
partial: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const pendingStreamUpdates: Map<string, PendingStreamUpdate> = new Map()
|
||||
let streamingDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/**
|
||||
* RouterModels type for context window lookup.
|
||||
* Simplified version - we only need contextWindow from ModelInfo.
|
||||
|
|
@ -120,24 +152,74 @@ const initialState: CLIState = {
|
|||
previousTodos: [],
|
||||
}
|
||||
|
||||
export const useCLIStore = create<CLIState & CLIActions>((set) => ({
|
||||
export const useCLIStore = create<CLIState & CLIActions>((set, get) => ({
|
||||
...initialState,
|
||||
|
||||
addMessage: (msg) =>
|
||||
set((state) => {
|
||||
// Check if message already exists (by ID).
|
||||
const existingIndex = state.messages.findIndex((m) => m.id === msg.id)
|
||||
addMessage: (msg) => {
|
||||
const state = get()
|
||||
// Check if message already exists (by ID).
|
||||
const existingIndex = state.messages.findIndex((m) => m.id === msg.id)
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update existing message in place.
|
||||
const updated = [...state.messages]
|
||||
updated[existingIndex] = msg
|
||||
return { messages: updated }
|
||||
// For NEW messages (not updates) - always apply immediately
|
||||
if (existingIndex === -1) {
|
||||
set({ messages: [...state.messages, msg] })
|
||||
return
|
||||
}
|
||||
|
||||
// For UPDATES to existing messages:
|
||||
// If partial (streaming) and message exists, debounce the update
|
||||
if (msg.partial) {
|
||||
// Queue the update
|
||||
pendingStreamUpdates.set(msg.id, {
|
||||
id: msg.id,
|
||||
content: msg.content,
|
||||
partial: true,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
// Schedule flush if not already scheduled
|
||||
if (!streamingDebounceTimer) {
|
||||
streamingDebounceTimer = setTimeout(() => {
|
||||
// Flush all pending updates as a single batch
|
||||
const currentState = get()
|
||||
const updates = Array.from(pendingStreamUpdates.values())
|
||||
pendingStreamUpdates.clear()
|
||||
streamingDebounceTimer = null
|
||||
|
||||
if (updates.length === 0) return
|
||||
|
||||
// Apply all pending updates in one state change
|
||||
const newMessages = [...currentState.messages]
|
||||
let hasChanges = false
|
||||
|
||||
for (const update of updates) {
|
||||
const idx = newMessages.findIndex((m) => m.id === update.id)
|
||||
if (idx !== -1 && newMessages[idx]) {
|
||||
newMessages[idx] = {
|
||||
...newMessages[idx],
|
||||
content: update.content,
|
||||
partial: update.partial,
|
||||
}
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
set({ messages: newMessages })
|
||||
}
|
||||
}, STREAMING_DEBOUNCE_MS)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Add new message.
|
||||
return { messages: [...state.messages, msg] }
|
||||
}),
|
||||
// Non-partial update (final message) - apply immediately and clear any pending
|
||||
// This ensures the final complete message is always shown
|
||||
pendingStreamUpdates.delete(msg.id)
|
||||
|
||||
const updated = [...state.messages]
|
||||
updated[existingIndex] = msg
|
||||
set({ messages: updated })
|
||||
},
|
||||
|
||||
updateMessage: (id, content, partial) =>
|
||||
set((state) => {
|
||||
|
|
@ -195,10 +277,15 @@ export const useCLIStore = create<CLIState & CLIActions>((set) => ({
|
|||
apiConfiguration: state.apiConfiguration,
|
||||
})),
|
||||
setIsResumingTask: (isResuming) => set({ isResumingTask: isResuming }),
|
||||
setFileSearchResults: (results) => set({ fileSearchResults: results }),
|
||||
setAllSlashCommands: (commands) => set({ allSlashCommands: commands }),
|
||||
setAvailableModes: (modes) => set({ availableModes: modes }),
|
||||
setTaskHistory: (history) => set({ taskHistory: history }),
|
||||
// Use shallow equality to prevent unnecessary re-renders when array content is the same
|
||||
setFileSearchResults: (results) =>
|
||||
set((state) => (shallowArrayEqual(state.fileSearchResults, results) ? state : { fileSearchResults: results })),
|
||||
setAllSlashCommands: (commands) =>
|
||||
set((state) => (shallowArrayEqual(state.allSlashCommands, commands) ? state : { allSlashCommands: commands })),
|
||||
setAvailableModes: (modes) =>
|
||||
set((state) => (shallowArrayEqual(state.availableModes, modes) ? state : { availableModes: modes })),
|
||||
setTaskHistory: (history) =>
|
||||
set((state) => (shallowArrayEqual(state.taskHistory, history) ? state : { taskHistory: history })),
|
||||
setCurrentTaskId: (taskId) => set({ currentTaskId: taskId }),
|
||||
setCurrentMode: (mode) => set({ currentMode: mode }),
|
||||
setTokenUsage: (usage) => set({ tokenUsage: usage }),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue