From 3a6a8aebe94d7b7b4a43b528ff8d686de52105d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20Paulus=20=F0=9F=A5=AA?= Date: Wed, 20 May 2026 09:21:58 -0700 Subject: [PATCH] first set of perf improvements --- docs/history-performance-improvements.md | 241 ++++++++++++++++++ src/core/controller/task/getTotalTasksSize.ts | 3 + src/sdk/SdkController.ts | 54 +++- src/sdk/sdk-task-control-coordinator.ts | 19 ++ src/sdk/sdk-task-history.ts | 67 ++++- src/sdk/vscode-session-host.ts | 2 + .../src/components/history/HistoryView.tsx | 6 + 7 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 docs/history-performance-improvements.md diff --git a/docs/history-performance-improvements.md b/docs/history-performance-improvements.md new file mode 100644 index 0000000000..bc3086ae42 --- /dev/null +++ b/docs/history-performance-improvements.md @@ -0,0 +1,241 @@ +# History Flow Performance Improvements + +This is a working checklist for improving slowness when: + +1. Opening History. +2. Opening a task from History. + +The current suspected high-level cause is a combination of cold SDK/core host startup, full-history scans, legacy task migration, large message loading, and expensive UI/state updates. We should tackle these incrementally and measure after each change. + +## 1. Add timing instrumentation first + +Status: implemented initial `[HistoryPerf]` logging in the webview and backend. + +Before changing behavior, add focused timing logs around the hot path so each improvement can be verified. + +Suggested spans: + +- `HistoryView` mount to `getTaskHistory` response. +- `HistoryView` mount to `getTotalTasksSize` response. +- `SdkController.getTaskHistory` total time. +- `SdkTaskHistory.listHistory` total time. +- `VscodeSessionHost.create` time. +- `ClineCore.create` / hub connection wait time if separable. +- `host.listHistory` time. +- legacy `readTaskHistory` + merge/sort time. +- `showTaskWithId` total time. +- task lookup time. +- legacy migration time. +- `readMessages` time. +- `sdkMessagesToClineMessages` time. +- webview message push loop time. +- `postStateToWebview` time. + +Goal: confirm which bottleneck dominates for the affected user. + +Initial instrumentation locations: + +- `webview-ui/src/components/history/HistoryView.tsx` +- `src/core/controller/task/getTotalTasksSize.ts` +- `src/sdk/SdkController.ts` +- `src/sdk/sdk-task-control-coordinator.ts` +- `src/sdk/sdk-task-history.ts` +- `src/sdk/vscode-session-host.ts` + +## 2. Avoid unbounded history listing in `showTaskWithId` + +Status: implemented targeted `findHistoryItem(taskId)` lookup for task opening. It now tries SDK `host.get(taskId)` first and falls back to legacy `taskHistory.json` lookup by ID, avoiding the previous default full `listHistory()` call in `SdkController.showTaskWithId`. + +Current issue: + +- `SdkController.showTaskWithId(taskId)` calls `this.taskHistory.listHistory()` with default options. +- `SdkTaskHistory.listHistory()` defaults to a very large limit and may hydrate records. +- Opening one task should not require listing thousands of sessions. + +Potential fixes: + +- Add a targeted `findHistoryItem(taskId)` path that does not scan/hydrate all history. +- At minimum, call `listHistory({ hydrate: false })` where lookup is unavoidable. +- Prefer SDK host `get(taskId)` plus legacy fallback lookup by ID. + +Expected impact: faster task opening, especially with 1000+ old sessions. + +## 3. Reuse a history/session host instead of creating temporary hosts repeatedly + +Current issue: + +- `SdkTaskHistory.withHistoryHost()` creates a temporary `VscodeSessionHost` when there is no active session. +- A single History open or task click can trigger multiple temporary host creations. + +Potential fixes: + +- Keep a shared lazy history host for read-only history operations. +- Dispose it on extension deactivation or after an idle timeout. +- Avoid creating and disposing a host for every `listHistory`, `getClineMessages`, or migration check. + +Expected impact: reduces cold `ClineCore.create()`/hub setup costs and repeated startup overhead. + +## 4. Decouple History from hub startup blocking + +Current issue: + +- `VscodeSessionHost.create()` awaits `getActivationHubConnection()` if hub startup is in progress. +- Starting the hub earlier can hide cold startup, but can also make the first History call wait for hub readiness and causes lifecycle risk. + +Potential fixes: + +- Do not block history-only operations on in-progress hub startup if local fallback is acceptable. +- Bound hub wait time more aggressively for history reads. +- Make hub startup lazy/non-blocking unless a real runtime session needs it. + +Expected impact: avoids History waiting up to the hub startup timeout. + +## 5. Optimize `getTaskHistory` pagination and filtering + +Current issue: + +- `SdkTaskHistory.listHistory({ limit, offset })` converts offset pagination into `hostLimit = offset + limit`. +- Later pages become increasingly expensive. +- Filtering/searching happens after fetching one page, so filtered results can be incomplete and may encourage extra loads. + +Potential fixes: + +- Add cursor-based pagination if SDK/core supports it. +- Add filtering/search/sort to the lower-level history API if possible. +- If not possible, maintain an indexed/cached history list and paginate from the cache. +- Ensure `hasMore` reflects filtered results, not just raw page size. + +Expected impact: better infinite-scroll performance and more correct search/filter behavior. + +## 6. Cache or index merged history metadata + +Current issue: + +- Every history request reads SDK history, reads legacy `taskHistory.json`, merges, sorts, and slices. +- With many old tasks this repeated work adds up. + +Potential fixes: + +- Maintain an in-memory cache of merged history metadata. +- Invalidate on task create/update/delete/favorite/migration. +- Consider persisting a lightweight index if startup scans are still expensive. + +Expected impact: faster repeated History opens, filtering, and state updates. + +## 7. Make `getTotalTasksSize` lazy/cached/backgrounded + +Current issue: + +- Opening History triggers recursive size calculation for `globalStorageFsPath/tasks` and `checkpoints`. +- This can be slow with many tasks/checkpoints. + +Potential fixes: + +- Cache the total size and recompute in the background. +- Show “calculating…” instead of blocking or competing with history load. +- Update size only after delete/export operations or on a debounce. +- Consider removing automatic full directory size scan from initial History mount. + +Expected impact: faster perceived History open and less filesystem contention. + +## 8. Improve legacy task migration on open + +Current issue: + +- Opening an old legacy task can trigger migration from `api_conversation_history.json` into SDK session persistence. +- This may read/translate/sanitize large histories and write new artifacts before the task is shown. + +Potential fixes: + +- Show legacy task immediately from existing UI messages if available, then migrate in background. +- Only migrate when the user resumes/continues the task, not when merely viewing. +- Cache migration status to avoid repeated checks. +- Optimize `migrateLegacyTaskIfNeeded` to avoid repeated full `readTaskHistory()` lookups. + +Expected impact: much faster viewing of old tasks. + +## 9. Batch or replace per-message webview pushes when opening a task + +Current issue: + +- `showTaskWithId` loops through loaded messages and awaits `pushMessageToWebview(msg)` for each message. +- Large tasks can involve many serialized webview messages. + +Potential fixes: + +- Send one batched message list for history load. +- Or rely on `postStateToWebview()` carrying `clineMessages` instead of pushing every message individually. +- If streaming individual messages is needed for UX, chunk them without awaiting each one serially. + +Expected impact: faster task display for long conversations. + +## 10. Navigate to chat earlier when a history item is clicked + +Current issue: + +- `src/core/controller/task/showTaskWithId.ts` sends the chat navigation event only after `controller.showTaskWithId()` completes. +- Backend slowness is experienced as staying stuck in History. + +Potential fixes: + +- Navigate immediately and show a loading state while messages load. +- Or have the webview navigate optimistically on click before awaiting RPC completion. +- Preserve error handling if loading fails. + +Expected impact: improved perceived responsiveness. + +## 11. Debounce History search and avoid duplicate reloads + +Current issue: + +- Search input updates `searchQuery` immediately. +- Search also changes `sortOption` to `mostRelevant`, which can trigger additional reloads. + +Potential fixes: + +- Debounce search queries. +- Coalesce search/sort state updates into one backend request. +- Cancel/ignore stale requests already exists via `historyRequestIdRef`, but backend work may still run. + +Expected impact: fewer expensive history requests while typing. + +## 12. Avoid expensive state history rebuilds after task open + +Current issue: + +- `postStateToWebview()` calls `getStateToPostToWebview()`. +- The SDK implementation then calls `this.taskHistory.listHistory({ limit: 100, hydrate: false })` again. +- This happens after loading a task from history. + +Potential fixes: + +- Reuse the already-found history item for `currentTaskItem`. +- Avoid rebuilding task history during task-open state post. +- Use cached history metadata from item 6. + +Expected impact: reduces extra work after opening a task. + +## 13. Add user-visible loading states and partial rendering + +Current issue: + +- Slow operations can look like the UI is frozen. + +Potential fixes: + +- Show a History loading skeleton immediately. +- Show total size later when available. +- Show chat task shell immediately, then progressively fill messages. + +Expected impact: better perceived performance even before all backend optimizations are complete. + +## Suggested implementation order + +1. Add instrumentation. +2. Fix unbounded `showTaskWithId` lookup. +3. Stop initial History open from doing full folder size scans synchronously. +4. Reuse/cache history host or history metadata. +5. Optimize legacy migration/viewing. +6. Batch task message delivery. +7. Improve pagination/search/filtering. +8. Revisit hub lifecycle only after the above measurements. diff --git a/src/core/controller/task/getTotalTasksSize.ts b/src/core/controller/task/getTotalTasksSize.ts index 0c1694eeed..718848efc5 100644 --- a/src/core/controller/task/getTotalTasksSize.ts +++ b/src/core/controller/task/getTotalTasksSize.ts @@ -1,4 +1,5 @@ import { EmptyRequest, Int64 } from "@shared/proto/cline/common" +import { Logger } from "@/shared/services/Logger" import { getTotalTasksSize as calculateTotalTasksSize } from "../../../utils/storage" import { Controller } from ".." @@ -9,6 +10,8 @@ import { Controller } from ".." * @returns The total size as an Int64 value */ export async function getTotalTasksSize(_controller: Controller, _request: EmptyRequest): Promise { + const startedAt = Date.now() const totalSize = await calculateTotalTasksSize() + Logger.log(`[HistoryPerf] getTotalTasksSize totalSize=${totalSize ?? 0} took ${Date.now() - startedAt}ms`) return { value: totalSize || 0 } } diff --git a/src/sdk/SdkController.ts b/src/sdk/SdkController.ts index 40c234f9be..1d8895ed0e 100644 --- a/src/sdk/SdkController.ts +++ b/src/sdk/SdkController.ts @@ -97,19 +97,18 @@ function dateStringToTimestamp(value: string | null | undefined): number { return Number.isFinite(timestamp) ? timestamp : 0 } -function sdkHistoryRecordToTaskResponse(item: SessionHistoryRecord): TaskResponse { - const metadata = item.metadata +function historyItemToTaskResponse(item: HistoryItem): TaskResponse { return TaskResponse.create({ - id: item.sessionId, - task: metadataString(metadata, "title") ?? item.prompt ?? "", - ts: dateStringToTimestamp(item.updatedAt ?? item.endedAt ?? item.startedAt), - isFavorited: metadataBoolean(metadata, "isFavorited") ?? metadataBoolean(metadata, "is_favorited") ?? false, - size: metadataNumber(metadata, "size") ?? 0, - totalCost: metadataNumber(metadata, "totalCost") ?? 0, - tokensIn: metadataNumber(metadata, "tokensIn") ?? 0, - tokensOut: metadataNumber(metadata, "tokensOut") ?? 0, - cacheWrites: metadataNumber(metadata, "cacheWrites") ?? 0, - cacheReads: metadataNumber(metadata, "cacheReads") ?? 0, + id: item.id, + task: item.task, + ts: item.ts, + isFavorited: item.isFavorited ?? false, + size: item.size ?? 0, + totalCost: item.totalCost ?? 0, + tokensIn: item.tokensIn ?? 0, + tokensOut: item.tokensOut ?? 0, + cacheWrites: item.cacheWrites ?? 0, + cacheReads: item.cacheReads ?? 0, }) } @@ -788,13 +787,23 @@ export class Controller { * 3. Only then push state to the webview */ async showTaskWithId(taskId: string): Promise { - const historyItem = (await this.taskHistory.listHistory()).find((item) => item.sessionId === taskId) + const startedAt = Date.now() + const lookupStartedAt = Date.now() + const historyItem = await this.taskHistory.findHistoryItem(taskId) + const lookupElapsed = Date.now() - lookupStartedAt if (!historyItem) { + Logger.log( + `[HistoryPerf] SdkController.showTaskWithId taskId=${taskId} found=false targetedLookup=${lookupElapsed}ms total=${Date.now() - startedAt}ms`, + ) throw new Error(`Task not found in history: ${taskId}`) } + const controlStartedAt = Date.now() await this.taskControl.showTaskWithId(taskId, { skipHistoryLookup: true }) - return sdkHistoryRecordToTaskResponse(historyItem) + Logger.log( + `[HistoryPerf] SdkController.showTaskWithId taskId=${taskId} targetedLookup=${lookupElapsed}ms control=${Date.now() - controlStartedAt}ms total=${Date.now() - startedAt}ms`, + ) + return historyItemToTaskResponse(historyItem) } // ---- Mode switching (Step 8) ---- @@ -921,11 +930,17 @@ export class Controller { } async getTaskHistory(request: GetTaskHistoryRequest): Promise { + const startedAt = Date.now() const { favoritesOnly, currentWorkspaceOnly, searchQuery, sortBy } = request const limit = request.limit > 0 ? Math.min(request.limit, 100) : 50 const offset = request.offset > 0 ? request.offset : 0 + const workspaceStartedAt = Date.now() const workspacePath = currentWorkspaceOnly ? await this.getWorkspaceRoot() : undefined + const workspaceElapsed = Date.now() - workspaceStartedAt + const listStartedAt = Date.now() const sessionHistory = await this.taskHistory.listHistory({ hydrate: false, limit: limit + 1, offset }) + const listElapsed = Date.now() - listStartedAt + const transformStartedAt = Date.now() let filteredTasks = sessionHistory.filter((item) => { const ts = dateStringToTimestamp(item.updatedAt ?? item.endedAt ?? item.startedAt) @@ -988,6 +1003,7 @@ export class Controller { }) const hasMore = sessionHistory.length > limit + const mapStartedAt = Date.now() const tasks = filteredTasks.slice(0, limit).map((item) => { const metadata = item.metadata return { @@ -1005,6 +1021,9 @@ export class Controller { } }) + Logger.log( + `[HistoryPerf] SdkController.getTaskHistory offset=${offset} limit=${limit} raw=${sessionHistory.length} filtered=${filteredTasks.length} tasks=${tasks.length} hasMore=${hasMore} workspace=${workspaceElapsed}ms list=${listElapsed}ms filterSortMap=${Date.now() - transformStartedAt}ms map=${Date.now() - mapStartedAt}ms total=${Date.now() - startedAt}ms`, + ) return TaskHistoryArray.create({ tasks, hasMore }) } @@ -1116,10 +1135,17 @@ export class Controller { // ---- State management ---- async postStateToWebview(): Promise { + const startedAt = Date.now() // Import dynamically to avoid circular deps const { sendStateUpdate } = await import("@core/controller/state/subscribeToState") + const stateStartedAt = Date.now() const state = await this.getStateToPostToWebview() + const stateElapsed = Date.now() - stateStartedAt + const sendStartedAt = Date.now() await sendStateUpdate(state) + Logger.log( + `[HistoryPerf] SdkController.postStateToWebview state=${stateElapsed}ms send=${Date.now() - sendStartedAt}ms total=${Date.now() - startedAt}ms`, + ) } async getStateToPostToWebview(): Promise { diff --git a/src/sdk/sdk-task-control-coordinator.ts b/src/sdk/sdk-task-control-coordinator.ts index 57fe282603..23d3d6c35e 100644 --- a/src/sdk/sdk-task-control-coordinator.ts +++ b/src/sdk/sdk-task-control-coordinator.ts @@ -112,16 +112,23 @@ export class SdkTaskControlCoordinator { } async showTaskWithId(taskId: string, options: { skipHistoryLookup?: boolean } = {}): Promise { + const startedAt = Date.now() try { if (!options.skipHistoryLookup) { + const lookupStartedAt = Date.now() const historyItem = await this.options.taskHistory.findHistoryItem(taskId) + Logger.log( + `[HistoryPerf] SdkTaskControlCoordinator.showTaskWithId taskId=${taskId} historyLookup=${Date.now() - lookupStartedAt}ms`, + ) if (!historyItem) { Logger.error(`[SdkController] Task not found in history: ${taskId}`) return } } + const teardownStartedAt = Date.now() this.silentlyTearDownActiveSession() + const teardownElapsed = Date.now() - teardownStartedAt const currentTask = this.options.getTask() if (currentTask) { @@ -132,9 +139,13 @@ export class SdkTaskControlCoordinator { // Load messages before installing the new task proxy so any concurrent // postStateToWebview() caller never sees the new id with empty messages. + const loadMessagesStartedAt = Date.now() const rawMessages = await this.options.taskHistory.getClineMessages(taskId) + const loadMessagesElapsed = Date.now() - loadMessagesStartedAt + const finalizeStartedAt = Date.now() const messages = this.options.messages.finalizeMessagesForSave(rawMessages) const cleanedMessages = messages.length > 0 ? this.appendFreshResumeMessage(messages) : [] + const finalizeElapsed = Date.now() - finalizeStartedAt const task = createTaskProxy( taskId, @@ -146,17 +157,25 @@ export class SdkTaskControlCoordinator { } this.options.setTask(task) + let pushElapsed = 0 if (cleanedMessages.length > 0) { Logger.log(`[SdkController] Loaded ${cleanedMessages.length} messages for task: ${taskId}`) const { pushMessageToWebview } = await import("./webview-grpc-bridge") + const pushStartedAt = Date.now() for (const msg of cleanedMessages) { await pushMessageToWebview(msg) } + pushElapsed = Date.now() - pushStartedAt } else { Logger.log(`[SdkController] No messages found for task: ${taskId}`) } + const postStateStartedAt = Date.now() await this.options.postStateToWebview() + const postStateElapsed = Date.now() - postStateStartedAt + Logger.log( + `[HistoryPerf] SdkTaskControlCoordinator.showTaskWithId taskId=${taskId} rawMessages=${rawMessages.length} cleanedMessages=${cleanedMessages.length} teardown=${teardownElapsed}ms loadMessages=${loadMessagesElapsed}ms finalize=${finalizeElapsed}ms push=${pushElapsed}ms postState=${postStateElapsed}ms total=${Date.now() - startedAt}ms`, + ) Logger.log(`[SdkController] Showing task: ${taskId}`) } catch (error) { Logger.error("[SdkController] Failed to show task:", error) diff --git a/src/sdk/sdk-task-history.ts b/src/sdk/sdk-task-history.ts index b0e44b5ee5..9a5c87e016 100644 --- a/src/sdk/sdk-task-history.ts +++ b/src/sdk/sdk-task-history.ts @@ -208,8 +208,10 @@ export class SdkTaskHistory { return fn(activeHistoryHost) } + const startedAt = Date.now() const { VscodeSessionHost } = await import("./vscode-session-host") const historyHost = await VscodeSessionHost.create({ mcpHub: this.options.mcpHub }) + Logger.log(`[HistoryPerf] SdkTaskHistory.withHistoryHost created temp host in ${Date.now() - startedAt}ms`) try { return await fn(historyHost) } finally { @@ -220,59 +222,94 @@ export class SdkTaskHistory { } async listHistory(options: SdkTaskHistoryListOptions = {}): Promise { + const startedAt = Date.now() const offset = Math.max(0, Math.floor(options.offset ?? 0)) const limit = Math.max(0, Math.floor(options.limit ?? 10_000)) const hostLimit = offset + limit const hostOptions: ClineCoreListHistoryOptions = { ...options } delete (hostOptions as { offset?: number }).offset + const hostStartedAt = Date.now() const sdkHistory = await this.withHistoryHost((host) => host.listHistory({ ...hostOptions, limit: hostLimit || 10_000, includeManifestFallback: true }), ) + const hostElapsed = Date.now() - hostStartedAt + const mergeStartedAt = Date.now() const visibleSdkHistory = sdkHistory.filter((item) => item.isSubagent !== true) const sdkIds = new Set(visibleSdkHistory.map((item) => item.sessionId)) const legacyHistory = readTaskHistory() .filter((item) => item.id && item.task && !sdkIds.has(item.id)) .map(historyItemToSessionHistoryRecord) - return [...visibleSdkHistory, ...legacyHistory] + const result = [...visibleSdkHistory, ...legacyHistory] .sort( (a, b) => dateStringToTimestamp(b.updatedAt ?? b.endedAt ?? b.startedAt) - dateStringToTimestamp(a.updatedAt ?? a.endedAt ?? a.startedAt), ) .slice(offset, offset + limit) + Logger.log( + `[HistoryPerf] SdkTaskHistory.listHistory offset=${offset} limit=${limit} hydrate=${options.hydrate !== false} sdk=${sdkHistory.length} visibleSdk=${visibleSdkHistory.length} legacy=${legacyHistory.length} result=${result.length} host=${hostElapsed}ms mergeSort=${Date.now() - mergeStartedAt}ms total=${Date.now() - startedAt}ms`, + ) + return result } async getClineMessages(taskId: string): Promise { - await this.migrateLegacyTaskIfNeeded(taskId) + const startedAt = Date.now() + const migrateStartedAt = Date.now() + const migrated = await this.migrateLegacyTaskIfNeeded(taskId) + const migrateElapsed = Date.now() - migrateStartedAt + const readStartedAt = Date.now() const sdkMessages = await this.withHistoryHost((host) => host.readMessages(taskId) as Promise) - return sdkMessagesToClineMessages(sdkMessages) + const readElapsed = Date.now() - readStartedAt + const translateStartedAt = Date.now() + const clineMessages = sdkMessagesToClineMessages(sdkMessages) + Logger.log( + `[HistoryPerf] SdkTaskHistory.getClineMessages taskId=${taskId} migrated=${migrated} sdkMessages=${sdkMessages.length} clineMessages=${clineMessages.length} migrate=${migrateElapsed}ms read=${readElapsed}ms translate=${Date.now() - translateStartedAt}ms total=${Date.now() - startedAt}ms`, + ) + return clineMessages } private async migrateLegacyTaskIfNeeded(taskId: string): Promise { + const startedAt = Date.now() return this.withHistoryHost(async (host) => { try { const existing = await host.get(taskId) if (existing) { + Logger.log( + `[HistoryPerf] SdkTaskHistory.migrateLegacyTaskIfNeeded taskId=${taskId} existingSdk=true total=${Date.now() - startedAt}ms`, + ) return false } } catch (error) { Logger.warn(`[SdkTaskHistory] Failed to check SDK session before legacy migration: ${taskId}`, error) } + const legacyLookupStartedAt = Date.now() const historyItem = readTaskHistory().find((item) => item.id === taskId) if (!historyItem) { + Logger.log( + `[HistoryPerf] SdkTaskHistory.migrateLegacyTaskIfNeeded taskId=${taskId} legacyFound=false legacyLookup=${Date.now() - legacyLookupStartedAt}ms total=${Date.now() - startedAt}ms`, + ) return false } + const legacyReadStartedAt = Date.now() const legacyApiHistory = readApiConversationHistory(taskId) if (legacyApiHistory.length === 0) { + Logger.log( + `[HistoryPerf] SdkTaskHistory.migrateLegacyTaskIfNeeded taskId=${taskId} legacyMessages=0 legacyRead=${Date.now() - legacyReadStartedAt}ms total=${Date.now() - startedAt}ms`, + ) return false } + const translateStartedAt = Date.now() const initialMessages = legacyApiHistoryToSdkMessages(legacyApiHistory, historyItem) + const translateElapsed = Date.now() - translateStartedAt if (initialMessages.length === 0) { + Logger.log( + `[HistoryPerf] SdkTaskHistory.migrateLegacyTaskIfNeeded taskId=${taskId} translatedMessages=0 legacyMessages=${legacyApiHistory.length} translate=${translateElapsed}ms total=${Date.now() - startedAt}ms`, + ) return false } @@ -280,6 +317,7 @@ export class SdkTaskHistory { const config = await buildSessionConfig({ cwd, workspaceRoot: cwd, mode: "act" }) config.sessionId = taskId + const startStartedAt = Date.now() await host.start({ config, prompt: undefined, @@ -300,6 +338,9 @@ export class SdkTaskHistory { }) Logger.log(`[SdkTaskHistory] Migrated legacy task to SDK session: ${taskId}`) + Logger.log( + `[HistoryPerf] SdkTaskHistory.migrateLegacyTaskIfNeeded taskId=${taskId} legacyMessages=${legacyApiHistory.length} initialMessages=${initialMessages.length} translate=${translateElapsed}ms start=${Date.now() - startStartedAt}ms total=${Date.now() - startedAt}ms`, + ) return true }) } @@ -332,9 +373,23 @@ export class SdkTaskHistory { } async findHistoryItem(taskId: string): Promise { - const history = await this.listHistory() - const item = history.find((candidate) => candidate.sessionId === taskId) - return item ? sessionHistoryRecordToHistoryItem(item) : undefined + const startedAt = Date.now() + const sdkLookupStartedAt = Date.now() + const sdkRecord = await this.withHistoryHost((host) => host.get(taskId)) + const sdkLookupElapsed = Date.now() - sdkLookupStartedAt + if (sdkRecord && sdkRecord.isSubagent !== true) { + Logger.log( + `[HistoryPerf] SdkTaskHistory.findHistoryItem taskId=${taskId} source=sdk sdkLookup=${sdkLookupElapsed}ms total=${Date.now() - startedAt}ms`, + ) + return sessionHistoryRecordToHistoryItem(sdkRecord as SessionHistoryRecord) + } + + const legacyLookupStartedAt = Date.now() + const legacyItem = readTaskHistory().find((item) => item.id === taskId) + Logger.log( + `[HistoryPerf] SdkTaskHistory.findHistoryItem taskId=${taskId} source=${legacyItem ? "legacy" : "missing"} sdkLookup=${sdkLookupElapsed}ms legacyLookup=${Date.now() - legacyLookupStartedAt}ms total=${Date.now() - startedAt}ms`, + ) + return legacyItem } async deleteTaskFromState(id: string): Promise { diff --git a/src/sdk/vscode-session-host.ts b/src/sdk/vscode-session-host.ts index 8185ca7ba3..327636c416 100644 --- a/src/sdk/vscode-session-host.ts +++ b/src/sdk/vscode-session-host.ts @@ -70,6 +70,7 @@ export class VscodeSessionHost implements SdkSessionHost { } static async create(options: VscodeSessionHostOptions): Promise { + const startedAt = Date.now() // Build tool executor capabilities from options — only include keys that are provided. // When a terminal manager is available, suppress the SDK's built-in run_commands // tool by setting bash to undefined. Our custom run_commands (provided via @@ -117,6 +118,7 @@ export class VscodeSessionHost implements SdkSessionHost { }), }) + Logger.log(`[HistoryPerf] VscodeSessionHost.create took ${Date.now() - startedAt}ms`) Logger.log("[VscodeSessionHost] Initialized with ClineCore + VSCode extra tools") if (options.getTerminalManager) { Logger.log("[VscodeSessionHost] SDK run_commands suppressed; using custom foreground/background terminal tool") diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index d9085e05d7..80b1750395 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -70,6 +70,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { isLoadingHistoryRef.current = true setIsLoadingHistory(true) try { + const startedAt = performance.now() const response = await TaskServiceClient.getTaskHistory( GetTaskHistoryRequest.create({ favoritesOnly: showFavoritesOnly, @@ -80,6 +81,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { offset, }), ) + console.log( + `[HistoryPerf] getTaskHistory offset=${offset} tasks=${response.tasks?.length ?? 0} hasMore=${response.hasMore} took ${Math.round(performance.now() - startedAt)}ms`, + ) if (requestId !== historyRequestIdRef.current) { return } @@ -174,7 +178,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { const fetchTotalTasksSize = useCallback(async () => { try { + const startedAt = performance.now() const response = await TaskServiceClient.getTotalTasksSize(EmptyRequest.create({})) + console.log(`[HistoryPerf] getTotalTasksSize took ${Math.round(performance.now() - startedAt)}ms`) if (response && typeof response.value === "number") { setTotalTasksSize?.(response.value || 0) }