perf(renderer): ignore member telemetry history churn

This commit is contained in:
777genius 2026-05-31 04:08:51 +03:00
parent a2a4f99fce
commit b6ea569623
2 changed files with 133 additions and 49 deletions

View file

@ -279,28 +279,6 @@ function isRuntimeResourceSampleLike(value: unknown): value is TeamAgentRuntimeR
return Boolean(value) && typeof value === 'object'; 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( function areMemberRuntimeEntriesEquivalent(
left: Map<string, TeamAgentRuntimeEntry> | undefined, left: Map<string, TeamAgentRuntimeEntry> | undefined,
right: Map<string, TeamAgentRuntimeEntry> | undefined right: Map<string, TeamAgentRuntimeEntry> | undefined
@ -312,13 +290,6 @@ function areMemberRuntimeEntriesEquivalent(
const rightEntry = right.get(key); const rightEntry = right.get(key);
const leftDiagnostics = Array.isArray(leftEntry.diagnostics) ? leftEntry.diagnostics : []; const leftDiagnostics = Array.isArray(leftEntry.diagnostics) ? leftEntry.diagnostics : [];
const rightDiagnostics = Array.isArray(rightEntry?.diagnostics) ? rightEntry.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 ( if (
leftEntry.memberName !== rightEntry?.memberName || leftEntry.memberName !== rightEntry?.memberName ||
leftEntry.alive !== rightEntry?.alive || leftEntry.alive !== rightEntry?.alive ||
@ -352,11 +323,7 @@ function areMemberRuntimeEntriesEquivalent(
leftEntry.runtimeLastSeenAt !== rightEntry?.runtimeLastSeenAt || leftEntry.runtimeLastSeenAt !== rightEntry?.runtimeLastSeenAt ||
leftEntry.historicalBootstrapConfirmed !== rightEntry?.historicalBootstrapConfirmed || leftEntry.historicalBootstrapConfirmed !== rightEntry?.historicalBootstrapConfirmed ||
leftDiagnostics.length !== rightDiagnostics.length || leftDiagnostics.length !== rightDiagnostics.length ||
!leftDiagnostics.every((value, index) => value === rightDiagnostics[index]) || !leftDiagnostics.every((value, index) => value === rightDiagnostics[index])
leftResourceHistory.length !== rightResourceHistory.length ||
!leftResourceHistory.every((value, index) =>
areRuntimeResourceSamplesEquivalent(value, rightResourceHistory[index])
)
) { ) {
return false; return false;
} }

View file

@ -10,31 +10,36 @@ import type {
TeamTaskWithKanban, TeamTaskWithKanban,
} from '@shared/types'; } from '@shared/types';
const memberCardRenderSpy = vi.hoisted(() => vi.fn());
vi.mock('@renderer/components/team/members/MemberCard', () => ({ vi.mock('@renderer/components/team/members/MemberCard', () => ({
MemberCard: ({ MemberCard: (props: {
member,
spawnError,
spawnStatus,
spawnLaunchState,
currentTask,
reviewTask,
onRestartMember,
onSkipMemberForLaunch,
onRestoreMember,
isRemoved,
}: {
member: ResolvedTeamMember; member: ResolvedTeamMember;
spawnError?: string; spawnError?: string;
spawnStatus?: string; spawnStatus?: string;
spawnLaunchState?: string; spawnLaunchState?: string;
currentTask?: TeamTaskWithKanban | null; currentTask?: TeamTaskWithKanban | null;
reviewTask?: TeamTaskWithKanban | null; reviewTask?: TeamTaskWithKanban | null;
runtimeEntry?: TeamAgentRuntimeEntry;
onRestartMember?: (memberName: string) => void; onRestartMember?: (memberName: string) => void;
onSkipMemberForLaunch?: (memberName: string) => void; onSkipMemberForLaunch?: (memberName: string) => void;
onRestoreMember?: (memberName: string) => void; onRestoreMember?: (memberName: string) => void;
isRemoved?: boolean; isRemoved?: boolean;
}) => }) => {
React.createElement( memberCardRenderSpy(props);
const {
member,
spawnError,
spawnStatus,
spawnLaunchState,
currentTask,
reviewTask,
onRestartMember,
onSkipMemberForLaunch,
onRestoreMember,
isRemoved,
} = props;
return React.createElement(
'div', 'div',
{ 'data-testid': `member-${member.name}` }, { 'data-testid': `member-${member.name}` },
spawnError ?? '', spawnError ?? '',
@ -77,7 +82,8 @@ vi.mock('@renderer/components/team/members/MemberCard', () => ({
'restore' 'restore'
) )
: null : null
), );
},
})); }));
import { MemberList } from '@renderer/components/team/members/MemberList'; import { MemberList } from '@renderer/components/team/members/MemberList';
@ -141,8 +147,34 @@ function activeTask(id = 'task-active'): TeamTaskWithKanban {
}; };
} }
function liveRuntimeEntry(
overrides: Partial<TeamAgentRuntimeEntry> = {}
): 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', () => { describe('MemberList spawn-status memoization', () => {
beforeEach(() => { beforeEach(() => {
memberCardRenderSpy.mockClear();
vi.stubGlobal( vi.stubGlobal(
'ResizeObserver', 'ResizeObserver',
class 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 () => { it('passes retry callbacks to failed member cards and rerenders when the callback changes', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div'); const host = document.createElement('div');