From 7f5499a7511fc4bebf0ef07916b681c0459970a2 Mon Sep 17 00:00:00 2001 From: moe-berri Date: Tue, 6 Oct 2026 14:19:52 -0700 Subject: [PATCH] perf(lens): reduce rendering on live investigations (#44935) * perf(lens): avoid redundant investigation rendering * fix(lens): keep history polling after run fetch failures --- .../investigations/InvestigationProgress.tsx | 11 ++- .../InvestigationsView.integration.test.tsx | 88 +++++++++++++++++++ .../lens/investigations/detail/HistoryTab.tsx | 18 +++- .../lens/investigations/detail/RunPicker.tsx | 9 +- .../lens/investigations/detail/RunReport.tsx | 24 ++--- .../investigations/detail/useRunHistory.ts | 9 +- .../investigations/live/ConclusionsPanel.tsx | 6 +- .../lens/investigations/live/LiveDrawer.tsx | 10 ++- .../live/LiveRun.integration.test.tsx | 26 +++++- .../lens/investigations/live/TraceList.tsx | 12 ++- 10 files changed, 176 insertions(+), 37 deletions(-) diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx index 08e5abdb01d..5fdda9e630e 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx @@ -1,5 +1,6 @@ "use client"; +import { useNow } from "@/hooks/useNow"; import { analysisElapsed, analysisFraction, @@ -13,9 +14,11 @@ import { type Job } from "../model/types"; import { QueueReasonText } from "./QueueReasonText"; import { useQueueReason, type QueueContext } from "./useQueueReason"; -export function InvestigationProgress({ job, now, queue }: { job: Job; now: number; queue?: QueueContext }) { +export function InvestigationProgress({ job, queue }: { job: Job; queue?: QueueContext }) { + const now = useNow(1000); const reason = useQueueReason(job, queue); const progress = analysisProgress(job); + const stages = analysisStages(job); const percent = Math.round(analysisFraction(progress) * 100); const { secondsLeft } = analysisPace(useProgressSamples(progress), now); const queued = progress.step < 0; @@ -28,10 +31,12 @@ export function InvestigationProgress({ job, now, queue }: { job: Job; now: numb aria-valuemin={0} aria-valuemax={100} aria-valuenow={percent} - aria-valuetext={`${progress.title}: ${progress.detail}`} + aria-valuetext={`${percent}% overall. ${progress.title}: ${progress.detail}`} + title={`Overall progress: ${stages.map((stage) => `${stage.label} ${stage.weight * 100}%`).join(", ")}`} className="text-2xl font-semibold" > {queued ? "–" : `${percent}%`} + {!queued && overall} {analysisElapsed(job.created_at, now)} elapsed @@ -39,7 +44,7 @@ export function InvestigationProgress({ job, now, queue }: { job: Job; now: numb
    - {analysisStages(job).map((stage) => ( + {stages.map((stage) => (
  1. { expect(within(screen.getByRole("tabpanel", { name: "History" })).getByText(/Took 2m 13s/)).toBeVisible(); }); +it("keeps history status and cost updating when an older run fails to load", async () => { + testQueryClient.clear(); + vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); + const running = { ...lens.jobs[0], status: "running" as const, cost: 0.25 }; + const older = { ...lens.jobs[0], id: "older", created_at: "2026-09-29T10:00:00Z" }; + const runs = vi.fn().mockResolvedValue([running, older]); + proxy.get.mockImplementation(async (path) => { + if (path.endsWith("/reviews")) return { reviews: [], reviewed: 0 }; + if (path === "/lens") return { lenses: [{ ...lens, jobs: [running] }], workers: [], tracing_enabled: true }; + if (path === "/lens/lens/runs") return runs(); + if (path === "/lens/lens/runs/older") throw new Error("Run unavailable"); + return { data: [] }; + }); + const user = userEvent.setup(); + const { unmount } = renderWithProviders(); + try { + await user.click(await screen.findByRole("tab", { name: "History" })); + const history = within(screen.getByRole("tabpanel", { name: "History" })); + expect(await history.findByText("Running")).toBeVisible(); + expect(history.getByText("$0.25")).toBeVisible(); + await user.selectOptions(screen.getByRole("combobox", { name: "Investigation run" }), "older"); + expect(await screen.findByText(/Could not load this run/)).toBeVisible(); + + runs.mockResolvedValue([{ ...running, status: "completed", cost: 1.75 }, older]); + await act(async () => vi.advanceTimersByTimeAsync(10_000)); + expect(await history.findByText("$1.75")).toBeVisible(); + expect(history.queryByText("Running")).not.toBeInTheDocument(); + expect(history.getAllByText("Completed")).toHaveLength(2); + } finally { + unmount(); + vi.useRealTimers(); + } +}); + it("reads request content from the beginning after its abbreviated preview", async () => { testQueryClient.clear(); const requestId = btoa(JSON.stringify(["requests", "", "request-1"])); @@ -932,6 +967,59 @@ it("pauses monitoring from the detail menu by saving the investigation with moni expect(sentBody(proxy.put, "/lens/lens")).toEqual([{ ...watching.settings, enabled: false }]); }); +it("updates elapsed time without reformatting the activity log and still shows new model calls", async () => { + vi.useFakeTimers(); + const formatTime = vi.spyOn(Date.prototype, "toLocaleTimeString"); + const created_at = new Date(Date.now() - 60_000).toISOString(); + const running = { + ...lens.jobs[0], + status: "running" as const, + stage: "Reading executions", + created_at, + coverage: { ...lens.jobs[0].coverage, selected: 62, screened: 13 }, + steps: Array.from({ length: 200 }, (_, index) => ({ + at: created_at, + kind: "model" as const, + label: `Reviewed run ${index}`, + model: "analysis", + purpose: "extract" as const, + prompt_tokens: 100, + completion_tokens: 20, + cost: 0.01, + })), + }; + const view = (job: typeof running) => ( + + ); + const { rerender, unmount } = renderWithProviders(view(running)); + try { + fireEvent.click(screen.getByRole("button", { name: "Activity log" })); + expect(screen.getByRole("progressbar", { name: "Investigation progress" })).toHaveAttribute("aria-valuenow", "13"); + expect(screen.getByRole("progressbar", { name: "Investigation progress" })).toHaveAttribute( + "aria-valuetext", + "13% overall. Reviewing activity: 13 of 62 selected runs reviewed", + ); + expect(within(screen.getByRole("list", { name: "Investigation steps" })).getAllByRole("listitem")).toHaveLength( + 200, + ); + formatTime.mockClear(); + await act(async () => vi.advanceTimersByTime(3000)); + expect(screen.getByText("1m 3s")).toBeVisible(); + expect(formatTime.mock.calls.length).toBe(0); + + rerender( + view({ ...running, cost: 2.01, steps: [...running.steps, { ...running.steps[0], label: "New model call" }] }), + ); + expect(screen.getByText("201 model calls")).toBeVisible(); + expect(screen.getByText("$2.01")).toBeVisible(); + expect(screen.getByText(/New model call/)).toBeVisible(); + } finally { + unmount(); + formatTime.mockRestore(); + vi.useRealTimers(); + } +}); + it("stops the running job from the run report's primary action", async () => { testQueryClient.clear(); const running = { ...lens.jobs[0], id: "live", status: "running" as const, stage: "Reading executions" }; diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/HistoryTab.tsx b/ui/litellm-dashboard/src/components/lens/investigations/detail/HistoryTab.tsx index 7bd87f8ab26..80d6aed0a48 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/detail/HistoryTab.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/HistoryTab.tsx @@ -23,11 +23,23 @@ export interface HistoryTabProps { export function HistoryTab({ lens }: HistoryTabProps) { const [offset, setOffset] = useState(0); - const history = useRunHistory(lens, offset); + return ( + + + + ); +} + +function HistoryContent({ + lens, + offset, + setOffset, +}: HistoryTabProps & { offset: number; setOffset: (offset: number) => void }) { + const history = useRunHistory(lens, offset, offset > 0); const { openRun } = useRunRoute(); const rows = history.data ?? lens.jobs; return ( - + <> {history.error && (

    Could not load run history.{" "} @@ -85,6 +97,6 @@ export function HistoryTab({ lens }: HistoryTabProps) { )} - + ); } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/RunPicker.tsx b/ui/litellm-dashboard/src/components/lens/investigations/detail/RunPicker.tsx index eb4343ff1b5..3cb6470c552 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/detail/RunPicker.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/RunPicker.tsx @@ -20,9 +20,10 @@ export interface RunPickerProps { export function RunPicker({ lens, job }: RunPickerProps) { const { batchId, selectRun } = useRunRoute(); const history = useRunHistory(lens, 0); + if (!job) return null; const options = history.data ?? lens.jobs; - const reused = job?.coverage?.reused ?? 0; - const newlyReviewed = Math.max(0, (job?.coverage?.screened ?? 0) - reused); + const reused = job.coverage?.reused ?? 0; + const newlyReviewed = Math.max(0, (job.coverage?.screened ?? 0) - reused); const aggregate = batchId === "latest" || batchId === "all"; const outsideHistory = !aggregate && !options.some((j) => j.id === batchId); return ( @@ -34,7 +35,7 @@ export function RunPicker({ lens, job }: RunPickerProps) { onChange={(e) => selectRun(e.target.value)} > - {job && outsideHistory && ( + {outsideHistory && ( @@ -46,7 +47,7 @@ export function RunPicker({ lens, job }: RunPickerProps) { ))} - {job && batchId !== "all" && ( + {batchId !== "all" && (

    {label}
    @@ -187,18 +187,19 @@ function issueStat(job: Job, findings: readonly Finding[] | null | undefined) { return { value: issues.length.toLocaleString(), note: high ? `${high} high priority` : undefined }; } -function RunStats({ job, findings, now }: { job: Job; findings: readonly Finding[] | null | undefined; now: number }) { +function RunDuration({ job }: { job: Job }) { + const now = useNow(isActive(job) ? 1000 : 60000); + const end = job.finished_at ? Date.parse(job.finished_at) : now; + return isActive(job) || job.finished_at ? analysisElapsed(job.created_at, end) : "–"; +} + +function RunStats({ job, findings }: { job: Job; findings: readonly Finding[] | null | undefined }) { const calls = (job.steps ?? []).filter((step) => step.kind === "model").length; const { screened = 0, selected = 0 } = job.coverage ?? {}; - const end = job.finished_at ? Date.parse(job.finished_at) : now; return (
    - + } note={`Started ${shortTime(job.created_at)}`} /> {view.headline(known)}
    - {job && picker} + {picker} {action && actions && ( )}
    - {job && view.body === "progress" && } + {job && view.body === "progress" && } {job?.error && view.body === "partial" && } {job?.error && view.body === "error" && } - {job && } + {job && } {job && } ); diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/useRunHistory.ts b/ui/litellm-dashboard/src/components/lens/investigations/detail/useRunHistory.ts index 94d1519ddc2..8c9de13f828 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/detail/useRunHistory.ts +++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/useRunHistory.ts @@ -8,13 +8,14 @@ import { activeJob } from "../../model/status"; import type { Lens } from "../../model/types"; /** Run history polls off its own rows, so a job the list poll discovers has to push it to refetch. */ -export function useRunHistory(lens: Lens, historyOffset: number) { +export function useRunHistory(lens: Lens, historyOffset: number, poll = true) { const api = useLensApi(); const client = useQueryClient(); const activeId = activeJob(lens.jobs)?.id; useEffect(() => { - if (!activeId) return; + if (!poll || !activeId) return; void client.invalidateQueries({ queryKey: lensKeys.histories() }, { cancelRefetch: false }); - }, [activeId, client]); - return useQuery(lensQueries.history(api, { lensId: lens.id, historyOffset })); + }, [activeId, client, poll]); + const options = lensQueries.history(api, { lensId: lens.id, historyOffset }); + return useQuery({ ...options, refetchInterval: poll ? options.refetchInterval : false }); } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx index 53568b08949..e3755d4a119 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { memo, useEffect, useRef, useState } from "react"; import { cn } from "@/lib/cva.config"; @@ -22,7 +22,7 @@ function useFlashing(groups: readonly Conclusion[]): ReadonlySet { return flashing; } -export function ConclusionsPanel({ +export const ConclusionsPanel = memo(function ConclusionsPanel({ groups, total, scope, @@ -102,4 +102,4 @@ export function ConclusionsPanel({ {scope &&

    {scope}

    } ); -} +}); diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx index 0b2d75328e2..c028d996774 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, type ReactNode } from "react"; +import { useMemo, useState, type ReactNode } from "react"; import { StatusBadge, type StatusTone } from "@/components/shared/table_cells"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; @@ -48,7 +48,11 @@ function Stage({ }) { const legacyReading = running && !activities.length && stageName === "Reading executions"; const { stage, now, charMs } = useStage(reviews, reading, legacyReading, slots); - const listed = legacyReading ? releasedReviews(reviews, stage) : reviews; + const listed = useMemo( + () => (legacyReading ? releasedReviews(reviews, stage) : reviews), + [legacyReading, reviews, stage], + ); + const groups = useMemo(() => conclusions(listed, checks), [listed, checks]); function currentWork() { if (!running) return null; if (activities.length) return ; @@ -61,7 +65,7 @@ function Stage({

    ); } - return <>{children(listed, conclusions(listed, checks), currentWork())}; + return <>{children(listed, groups, currentWork())}; } export function LiveDrawer({ diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.integration.test.tsx index 013aee4e896..0412cb3b7b7 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.integration.test.tsx @@ -1,5 +1,5 @@ -import { fireEvent, screen, within } from "@testing-library/react"; -import { beforeEach, expect, it } from "vitest"; +import { act, fireEvent, screen, within } from "@testing-library/react"; +import { beforeEach, expect, it, vi } from "vitest"; import { renderWithLens, stubGateway } from "@/../tests/lens-test-utils"; import { testQueryClient } from "@/../tests/test-utils"; @@ -62,6 +62,28 @@ function activity(overrides: Partial = {}): Activity { }; } +it("keeps completed trace rows idle while the reading clock advances", async () => { + vi.useFakeTimers(); + const parseTime = vi.spyOn(Date, "parse"); + const reviewed = review(); + const active = job({ stage: "Reading executions", activities: [], reading: [] }); + const { unmount } = renderWithLens(); + try { + fireEvent.click(screen.getByRole("button", { name: "View run" })); + const traces = within(screen.getByRole("list", { name: "Reviewed traces" })); + expect(traces.getByRole("button", { name: /research-agent/ })).toBeVisible(); + parseTime.mockClear(); + await act(async () => vi.advanceTimersByTime(500)); + expect(parseTime.mock.calls.filter(([value]) => value === reviewed.at)).toHaveLength(0); + fireEvent.click(traces.getByRole("button", { name: /research-agent/ })); + expect(traces.getByText("Tool calls: Read × 2 · Python × 1")).toBeVisible(); + } finally { + unmount(); + parseTime.mockRestore(); + vi.useRealTimers(); + } +}); + it("shows real candidate and grouping activity, durable tool counts, and preliminary review scope", async () => { const reviewed = review(); const grouping: Partial = { diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx index b9883ed38de..997485017b7 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { memo, useState } from "react"; import { agoLabel } from "../../model/format"; import { useNow } from "@/hooks/useNow"; @@ -95,7 +95,13 @@ function DoneRow({ ); } -export function TraceList({ reviews, group }: { reviews: readonly Review[]; group: string | null }) { +export const TraceList = memo(function TraceList({ + reviews, + group, +}: { + reviews: readonly Review[]; + group: string | null; +}) { const now = useNow(5000); const [expanded, setExpanded] = useState(null); const rows = newestFirst(reviews, LIMIT).filter((review) => inGroup(review, group)); @@ -117,4 +123,4 @@ export function TraceList({ reviews, group }: { reviews: readonly Review[]; grou ))}
); -} +});