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';
}
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<string, TeamAgentRuntimeEntry> | undefined,
right: Map<string, TeamAgentRuntimeEntry> | 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;
}

View file

@ -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> = {}
): 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');