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';
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue