perf(lens): reduce rendering on live investigations (#44935)

* perf(lens): avoid redundant investigation rendering

* fix(lens): keep history polling after run fetch failures
This commit is contained in:
moe-berri 2026-10-06 14:19:52 -07:00 • committed by GitHub
parent acd2a7ccc5
commit 7f5499a751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 176 additions and 37 deletions

View file

@ -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 && <span className="ml-1.5 text-xs font-normal text-muted-foreground">overall</span>}
</span>
<span className="text-xs text-muted-foreground">
{analysisElapsed(job.created_at, now)} elapsed
@ -39,7 +44,7 @@ export function InvestigationProgress({ job, now, queue }: { job: Job; now: numb
</span>
</div>
<ol aria-label="Analysis stages" className="flex gap-1">
{analysisStages(job).map((stage) => (
{stages.map((stage) => (
<li
key={stage.label}
data-state={stage.state}

View file

@ -6,6 +6,7 @@ import { renderWithLens, stubGateway } from "@/../tests/lens-test-utils";
import { ApiError } from "@/lib/http/client";
import { lensKeys } from "../data/queries";
import { InvestigationsView } from "./InvestigationsView";
import { RunReport } from "./detail/RunReport";
import { briefMarkdown } from "../model/findings";
import { findingKey } from "../model/inbox";
import { runTime } from "../model/format";
@ -469,6 +470,40 @@ it("opens the saved results of an older batch", async () => {
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(<InvestigationsView readOnly />);
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) => (
<RunReport lens={lens} job={job} findings={[]} connected ready busy={false} picker={null} />
);
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" };

View file

@ -23,11 +23,23 @@ export interface HistoryTabProps {
export function HistoryTab({ lens }: HistoryTabProps) {
const [offset, setOffset] = useState(0);
const history = useRunHistory(lens, offset);
return (
<TabsContent value="activity" className="pt-4 space-y-4">
<HistoryContent lens={lens} offset={offset} setOffset={setOffset} />
</TabsContent>
);
}
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 (
<TabsContent value="activity" className="pt-4 space-y-4">
<>
{history.error && (
<p role="alert" className="text-sm text-destructive">
Could not load run history.{" "}
@ -85,6 +97,6 @@ export function HistoryTab({ lens }: HistoryTabProps) {
</Button>
</div>
)}
</TabsContent>
</>
);
}

View file

@ -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)}
>
<option value="latest">Latest run</option>
{job && outsideHistory && (
{outsideHistory && (
<option value={batchId}>
{when(job.created_at)} · {runStatus(job)}
</option>
@ -46,7 +47,7 @@ export function RunPicker({ lens, job }: RunPickerProps) {
))}
<option value="all">All accumulated findings</option>
</select>
{job && batchId !== "all" && (
{batchId !== "all" && (
<Popover key={job.id}>
<PopoverTrigger
aria-label="Run details"

View file

@ -156,7 +156,7 @@ function facts(job: Job | undefined, findings: readonly Finding[] | null | undef
};
}
function Stat({ label, value, note }: { label: string; value: string; note?: string }) {
function Stat({ label, value, note }: { label: string; value: ReactNode; note?: string }) {
return (
<div className="min-w-0">
<dt className="text-xs text-muted-foreground">{label}</dt>
@ -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 (
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4">
<Stat label="Cost" value={money(job.cost ?? 0)} note={plural(calls, "model call")} />
<Stat
label="Duration"
value={isActive(job) || job.finished_at ? analysisElapsed(job.created_at, end) : "–"}
note={`Started ${shortTime(job.created_at)}`}
/>
<Stat label="Duration" value={<RunDuration job={job} />} note={`Started ${shortTime(job.created_at)}`} />
<Stat
label="Runs reviewed"
value={`${screened.toLocaleString()} / ${selected.toLocaleString()}`}
@ -302,7 +303,6 @@ function nextAction(lens: Lens, job: Job | undefined, situation: RunSituation):
}
export function RunReport({ lens, job, findings, connected, ready, busy, picker, actions, queue }: RunReportProps) {
const now = useNow(job && isActive(job) ? 1000 : 60000);
const input: SituationInput = { lens, job, findings, connected };
const situation = runSituation(input);
const view = SITUATIONS[situation];
@ -316,16 +316,16 @@ export function RunReport({ lens, job, findings, connected, ready, busy, picker,
<h3 className="mt-0.5 text-base font-semibold">{view.headline(known)}</h3>
</div>
<div className="flex items-center gap-1">
{job && picker}
{picker}
{action && actions && (
<NextAction action={action} facts={known} ready={ready} busy={busy} onClick={actions[action]} />
)}
</div>
</header>
{job && view.body === "progress" && <InvestigationProgress key={job.id} job={job} now={now} queue={queue} />}
{job && view.body === "progress" && <InvestigationProgress key={job.id} job={job} queue={queue} />}
{job?.error && view.body === "partial" && <RunPartial job={job} connected={connected} />}
{job?.error && view.body === "error" && <RunFailure job={job} connected={connected} />}
{job && <RunStats job={job} findings={findings} now={now} />}
{job && <RunStats job={job} findings={findings} />}
{job && <RunLog job={job} />}
</section>
);

View file

@ -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 });
}

View file

@ -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<string> {
return flashing;
}
export function ConclusionsPanel({
export const ConclusionsPanel = memo(function ConclusionsPanel({
groups,
total,
scope,
@ -102,4 +102,4 @@ export function ConclusionsPanel({
{scope && <p className="text-xs text-muted-foreground">{scope}</p>}
</div>
);
}
});

View file

@ -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 <ActiveWork model={model} activities={activities} />;
@ -61,7 +65,7 @@ function Stage({
</p>
);
}
return <>{children(listed, conclusions(listed, checks), currentWork())}</>;
return <>{children(listed, groups, currentWork())}</>;
}
export function LiveDrawer({

View file

@ -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> = {}): 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(<LiveRun job={active} reviews={[reviewed]} name="Tool quality" />);
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<Activity> = {

View file

@ -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<string | null>(null);
const rows = newestFirst(reviews, LIMIT).filter((review) => inGroup(review, group));
@ -117,4 +123,4 @@ export function TraceList({ reviews, group }: { reviews: readonly Review[]; grou
))}
</ol>
);
}
});