From 9a1c778d67c79932aedf8263244dcbde98bd14e9 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 05:05:54 +0300 Subject: [PATCH] perf(renderer): cache member runtime telemetry renders --- .../components/team/members/MemberList.tsx | 102 +++++++++++++++++- .../team/members/MemberList.test.ts | 25 ++++- 2 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/team/members/MemberList.tsx b/src/renderer/components/team/members/MemberList.tsx index d9d2a89e..fa069a69 100644 --- a/src/renderer/components/team/members/MemberList.tsx +++ b/src/renderer/components/team/members/MemberList.tsx @@ -331,6 +331,83 @@ function areMemberRuntimeEntriesEquivalent( return true; } +const MEMBER_CARD_RUNTIME_TELEMETRY_CACHE_MS = 30_000; + +interface CachedMemberRuntimeEntry { + signature: string; + cachedAt: number; + entry: TeamAgentRuntimeEntry; +} + +function buildMemberRuntimeCardSignature(entry: TeamAgentRuntimeEntry): string { + const diagnostics = Array.isArray(entry.diagnostics) ? entry.diagnostics : []; + return [ + entry.memberName, + entry.alive, + entry.restartable, + entry.backendType, + entry.providerId, + entry.providerBackendId, + entry.laneId, + entry.laneKind, + entry.pid, + entry.runtimeModel, + entry.processCount, + entry.runtimeLoadScope, + entry.runtimeLoadTruncated, + entry.livenessKind, + entry.pidSource, + entry.processCommand, + entry.paneId, + entry.panePid, + entry.paneCurrentCommand, + entry.runtimePid, + entry.runtimeSessionId, + entry.runtimeDiagnostic, + entry.runtimeDiagnosticSeverity, + entry.runtimeLastSeenAt, + entry.historicalBootstrapConfirmed, + diagnostics.join('\u001f'), + ].join('\u001e'); +} + +function buildCachedMemberRuntimeEntries( + runtimeEntries: Map | undefined, + cache: Map, + nowMs: number +): Map | undefined { + if (!runtimeEntries) { + cache.clear(); + return undefined; + } + + const nextEntries = new Map(); + const seenMemberNames = new Set(); + for (const [memberName, entry] of runtimeEntries) { + seenMemberNames.add(memberName); + const signature = buildMemberRuntimeCardSignature(entry); + const cached = cache.get(memberName); + if ( + !cached || + cached.signature !== signature || + nowMs - cached.cachedAt >= MEMBER_CARD_RUNTIME_TELEMETRY_CACHE_MS + ) { + cache.set(memberName, { signature, cachedAt: nowMs, entry }); + nextEntries.set(memberName, entry); + continue; + } + nextEntries.set(memberName, cached.entry); + } + + for (const memberName of cache.keys()) { + if (!seenMemberNames.has(memberName)) { + cache.delete(memberName); + } + } + + return nextEntries.size > 0 ? nextEntries : undefined; +} + function isFiniteNonNegative(value: number | undefined): value is number { return typeof value === 'number' && Number.isFinite(value) && value >= 0; } @@ -730,6 +807,7 @@ export const MemberList = memo(function MemberList({ const [isWide, setIsWide] = useState(false); const [runtimeTelemetryPreviewActive, setRuntimeTelemetryPreviewActive] = useState(false); const memberRuntimeEntriesRef = useRef(memberRuntimeEntries); + const memberRuntimeEntryCacheRef = useRef(new Map()); memberRuntimeEntriesRef.current = memberRuntimeEntries; const handleResize = useCallback((entries: ResizeObserverEntry[]) => { @@ -793,6 +871,17 @@ export const MemberList = memo(function MemberList({ : undefined, [activeMembers, memberRuntimeEntries, runtimeTelemetryPreviewActive] ); + const cardRuntimeEntries = useMemo( + () => + runtimeTelemetryPreviewActive + ? memberRuntimeEntries + : buildCachedMemberRuntimeEntries( + memberRuntimeEntries, + memberRuntimeEntryCacheRef.current, + Date.now() + ), + [memberRuntimeEntries, runtimeTelemetryPreviewActive] + ); const activityTimerRuntimeSignature = useMemo( () => buildActivityTimerRuntimeSignature(activeMembers, memberRuntimeEntries), [activeMembers, memberRuntimeEntries] @@ -969,7 +1058,10 @@ export const MemberList = memo(function MemberList({
{activeMembers.map((member) => { const spawnEntry = memberSpawnStatuses?.get(member.name); - const runtimeEntry = memberRuntimeEntries?.get(member.name); + const liveRuntimeEntry = memberRuntimeEntries?.get(member.name); + const cardRuntimeEntry = cardRuntimeEntries?.get(member.name); + const runtimeEntry = liveRuntimeEntry; + const displayRuntimeEntry = cardRuntimeEntry ?? liveRuntimeEntry; const bootstrapConfirmedProvisionedButNotAlive = isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry); const hasUnsafeProvisionedButNotAliveEvidence = @@ -1062,14 +1154,14 @@ export const MemberList = memo(function MemberList({ reviewTask={reviewTask} currentTaskTimer={currentTaskTimer} reviewTaskTimer={reviewTaskTimer} - currentTaskTimerRunning={activityTimerRunning} - reviewTaskTimerRunning={activityTimerRunning} + currentTaskTimerRunning={currentTask !== null && activityTimerRunning} + reviewTaskTimerRunning={reviewTask !== null && activityTimerRunning} awaitingReply={ isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name]) } taskCounts={memberTaskCounts?.get(member.name.toLowerCase())} - runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)} - runtimeEntry={runtimeEntry} + runtimeSummary={buildRuntimeSummary(member, spawnEntry, displayRuntimeEntry)} + runtimeEntry={displayRuntimeEntry} runtimeRunId={runtimeRunId} spawnStatus={effectiveSpawnStatus} spawnEntry={spawnEntry} diff --git a/test/renderer/components/team/members/MemberList.test.ts b/test/renderer/components/team/members/MemberList.test.ts index 2eb90758..2e8794cd 100644 --- a/test/renderer/components/team/members/MemberList.test.ts +++ b/test/renderer/components/team/members/MemberList.test.ts @@ -384,7 +384,7 @@ describe('MemberList spawn-status memoization', () => { }); }); - it('does not rerender cards when only runtime telemetry history changes', async () => { + it('does not rerender cards when only cached runtime telemetry changes', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); @@ -435,6 +435,7 @@ describe('MemberList spawn-status memoization', () => { }); expect(memberCardRenderSpy).not.toHaveBeenCalled(); + memberCardRenderSpy.mockClear(); await act(async () => { root.render( @@ -461,6 +462,28 @@ describe('MemberList spawn-status memoization', () => { await Promise.resolve(); }); + expect(memberCardRenderSpy).not.toHaveBeenCalled(); + memberCardRenderSpy.mockClear(); + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + memberRuntimeEntries: new Map([ + [ + 'bob', + liveRuntimeEntry({ + runtimeDiagnosticSeverity: 'error', + runtimeDiagnostic: 'runtime went stale', + }), + ], + ]), + }) + ); + await Promise.resolve(); + }); + expect(memberCardRenderSpy).toHaveBeenCalledTimes(1); await act(async () => {