perf(renderer): ignore member telemetry history churn
This commit is contained in:
parent
a2a4f99fce
commit
b6ea569623
2 changed files with 133 additions and 49 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue