From b6ea569623cc2fb90904e6f7d96d4bf5774a85cb Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 04:08:51 +0300 Subject: [PATCH] perf(renderer): ignore member telemetry history churn --- .../components/team/members/MemberList.tsx | 35 +---- .../team/members/MemberList.test.ts | 147 ++++++++++++++++-- 2 files changed, 133 insertions(+), 49 deletions(-) diff --git a/src/renderer/components/team/members/MemberList.tsx b/src/renderer/components/team/members/MemberList.tsx index ce8207fb..d9d2a89e 100644 --- a/src/renderer/components/team/members/MemberList.tsx +++ b/src/renderer/components/team/members/MemberList.tsx @@ -279,28 +279,6 @@ function isRuntimeResourceSampleLike(value: unknown): value is TeamAgentRuntimeR return Boolean(value) && typeof value === 'object'; } -function areRuntimeResourceSamplesEquivalent(left: unknown, right: unknown): boolean { - if (left === right) return true; - if (!isRuntimeResourceSampleLike(left) || !isRuntimeResourceSampleLike(right)) { - return false; - } - return ( - left.timestamp === right.timestamp && - left.cpuPercent === right.cpuPercent && - left.rssBytes === right.rssBytes && - left.primaryCpuPercent === right.primaryCpuPercent && - left.primaryRssBytes === right.primaryRssBytes && - left.childCpuPercent === right.childCpuPercent && - left.childRssBytes === right.childRssBytes && - left.processCount === right.processCount && - left.runtimeLoadScope === right.runtimeLoadScope && - left.runtimeLoadTruncated === right.runtimeLoadTruncated && - left.pidSource === right.pidSource && - left.pid === right.pid && - left.runtimePid === right.runtimePid - ); -} - function areMemberRuntimeEntriesEquivalent( left: Map | undefined, right: Map | undefined @@ -312,13 +290,6 @@ function areMemberRuntimeEntriesEquivalent( const rightEntry = right.get(key); const leftDiagnostics = Array.isArray(leftEntry.diagnostics) ? leftEntry.diagnostics : []; const rightDiagnostics = Array.isArray(rightEntry?.diagnostics) ? rightEntry.diagnostics : []; - const rightResourceHistoryCandidate = rightEntry?.resourceHistory; - const leftResourceHistory = Array.isArray(leftEntry.resourceHistory) - ? leftEntry.resourceHistory - : []; - const rightResourceHistory = Array.isArray(rightResourceHistoryCandidate) - ? rightResourceHistoryCandidate - : []; if ( leftEntry.memberName !== rightEntry?.memberName || leftEntry.alive !== rightEntry?.alive || @@ -352,11 +323,7 @@ function areMemberRuntimeEntriesEquivalent( leftEntry.runtimeLastSeenAt !== rightEntry?.runtimeLastSeenAt || leftEntry.historicalBootstrapConfirmed !== rightEntry?.historicalBootstrapConfirmed || leftDiagnostics.length !== rightDiagnostics.length || - !leftDiagnostics.every((value, index) => value === rightDiagnostics[index]) || - leftResourceHistory.length !== rightResourceHistory.length || - !leftResourceHistory.every((value, index) => - areRuntimeResourceSamplesEquivalent(value, rightResourceHistory[index]) - ) + !leftDiagnostics.every((value, index) => value === rightDiagnostics[index]) ) { return false; } diff --git a/test/renderer/components/team/members/MemberList.test.ts b/test/renderer/components/team/members/MemberList.test.ts index f2bcb0d9..2eb90758 100644 --- a/test/renderer/components/team/members/MemberList.test.ts +++ b/test/renderer/components/team/members/MemberList.test.ts @@ -10,31 +10,36 @@ import type { TeamTaskWithKanban, } from '@shared/types'; +const memberCardRenderSpy = vi.hoisted(() => vi.fn()); + vi.mock('@renderer/components/team/members/MemberCard', () => ({ - MemberCard: ({ - member, - spawnError, - spawnStatus, - spawnLaunchState, - currentTask, - reviewTask, - onRestartMember, - onSkipMemberForLaunch, - onRestoreMember, - isRemoved, - }: { + MemberCard: (props: { member: ResolvedTeamMember; spawnError?: string; spawnStatus?: string; spawnLaunchState?: string; currentTask?: TeamTaskWithKanban | null; reviewTask?: TeamTaskWithKanban | null; + runtimeEntry?: TeamAgentRuntimeEntry; onRestartMember?: (memberName: string) => void; onSkipMemberForLaunch?: (memberName: string) => void; onRestoreMember?: (memberName: string) => void; isRemoved?: boolean; - }) => - React.createElement( + }) => { + memberCardRenderSpy(props); + const { + member, + spawnError, + spawnStatus, + spawnLaunchState, + currentTask, + reviewTask, + onRestartMember, + onSkipMemberForLaunch, + onRestoreMember, + isRemoved, + } = props; + return React.createElement( 'div', { 'data-testid': `member-${member.name}` }, spawnError ?? '', @@ -77,7 +82,8 @@ vi.mock('@renderer/components/team/members/MemberCard', () => ({ 'restore' ) : null - ), + ); + }, })); import { MemberList } from '@renderer/components/team/members/MemberList'; @@ -141,8 +147,34 @@ function activeTask(id = 'task-active'): TeamTaskWithKanban { }; } +function liveRuntimeEntry( + overrides: Partial = {} +): TeamAgentRuntimeEntry { + return { + memberName: 'bob', + alive: true, + restartable: true, + providerId: 'opencode', + pid: 222, + rssBytes: 220 * 1024 * 1024, + cpuPercent: 5, + processCount: 2, + runtimeLoadScope: 'process-tree', + resourceHistory: [ + { + timestamp: '2026-05-31T10:00:00.000Z', + rssBytes: 220 * 1024 * 1024, + cpuPercent: 5, + }, + ], + updatedAt: '2026-05-31T10:00:00.000Z', + ...overrides, + }; +} + describe('MemberList spawn-status memoization', () => { beforeEach(() => { + memberCardRenderSpy.mockClear(); vi.stubGlobal( 'ResizeObserver', class ResizeObserver { @@ -352,6 +384,91 @@ describe('MemberList spawn-status memoization', () => { }); }); + it('does not rerender cards when only runtime telemetry history changes', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const members = [member]; + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + memberRuntimeEntries: new Map([['bob', liveRuntimeEntry()]]), + }) + ); + await Promise.resolve(); + }); + + expect(memberCardRenderSpy).toHaveBeenCalledTimes(1); + memberCardRenderSpy.mockClear(); + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + memberRuntimeEntries: new Map([ + [ + 'bob', + liveRuntimeEntry({ + resourceHistory: [ + { + timestamp: '2026-05-31T10:00:00.000Z', + rssBytes: 220 * 1024 * 1024, + cpuPercent: 5, + }, + { + timestamp: '2026-05-31T10:00:05.000Z', + rssBytes: 220 * 1024 * 1024, + cpuPercent: 5, + }, + ], + }), + ], + ]), + }) + ); + await Promise.resolve(); + }); + + expect(memberCardRenderSpy).not.toHaveBeenCalled(); + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + memberRuntimeEntries: new Map([ + [ + 'bob', + liveRuntimeEntry({ + cpuPercent: 7, + resourceHistory: [ + { + timestamp: '2026-05-31T10:00:05.000Z', + rssBytes: 220 * 1024 * 1024, + cpuPercent: 7, + }, + ], + }), + ], + ]), + }) + ); + await Promise.resolve(); + }); + + expect(memberCardRenderSpy).toHaveBeenCalledTimes(1); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('passes retry callbacks to failed member cards and rerenders when the callback changes', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div');