perf(renderer): cache member telemetry preview snapshots

This commit is contained in:
777genius 2026-05-31 05:58:49 +03:00
parent db2c82475b
commit a4ad5f6297
2 changed files with 101 additions and 13 deletions

View file

@ -408,6 +408,21 @@ function buildCachedMemberRuntimeEntries(
return nextEntries.size > 0 ? nextEntries : undefined;
}
function reuseRuntimeEntriesMapIfUnchanged(
previous: Map<string, TeamAgentRuntimeEntry> | undefined,
next: Map<string, TeamAgentRuntimeEntry> | undefined
): Map<string, TeamAgentRuntimeEntry> | undefined {
if (previous === next) return previous;
if (!previous || !next) return next;
if (previous.size !== next.size) return next;
for (const [memberName, entry] of next) {
if (previous.get(memberName) !== entry) {
return next;
}
}
return previous;
}
function isFiniteNonNegative(value: number | undefined): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
@ -808,6 +823,9 @@ export const MemberList = memo(function MemberList({
const [runtimeTelemetryPreviewActive, setRuntimeTelemetryPreviewActive] = useState(false);
const memberRuntimeEntriesRef = useRef(memberRuntimeEntries);
const memberRuntimeEntryCacheRef = useRef(new Map<string, CachedMemberRuntimeEntry>());
const displayedRuntimeEntriesRef = useRef<Map<string, TeamAgentRuntimeEntry> | undefined>(
undefined
);
memberRuntimeEntriesRef.current = memberRuntimeEntries;
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
@ -864,23 +882,25 @@ export const MemberList = memo(function MemberList({
);
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
const avatarMap = useMemo(() => buildMemberAvatarMap(members), [members]);
const cardRuntimeEntries = useMemo(() => {
const nextEntries = buildCachedMemberRuntimeEntries(
memberRuntimeEntries,
memberRuntimeEntryCacheRef.current,
Date.now()
);
const reusedEntries = reuseRuntimeEntriesMapIfUnchanged(
displayedRuntimeEntriesRef.current,
nextEntries
);
displayedRuntimeEntriesRef.current = reusedEntries;
return reusedEntries;
}, [memberRuntimeEntries]);
const runtimeTelemetryScale = useMemo(
() =>
runtimeTelemetryPreviewActive
? buildRuntimeTelemetryScale(activeMembers, memberRuntimeEntries)
? buildRuntimeTelemetryScale(activeMembers, cardRuntimeEntries)
: undefined,
[activeMembers, memberRuntimeEntries, runtimeTelemetryPreviewActive]
);
const cardRuntimeEntries = useMemo(
() =>
runtimeTelemetryPreviewActive
? memberRuntimeEntries
: buildCachedMemberRuntimeEntries(
memberRuntimeEntries,
memberRuntimeEntryCacheRef.current,
Date.now()
),
[memberRuntimeEntries, runtimeTelemetryPreviewActive]
[activeMembers, cardRuntimeEntries, runtimeTelemetryPreviewActive]
);
const activityTimerRuntimeSignature = useMemo(
() => buildActivityTimerRuntimeSignature(activeMembers, memberRuntimeEntries),

View file

@ -492,6 +492,74 @@ describe('MemberList spawn-status memoization', () => {
});
});
it('keeps hovered runtime telemetry preview on cached snapshots', 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();
});
const list = host.querySelector('.runtime-telemetry-list');
expect(list).not.toBeNull();
await act(async () => {
list?.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
await Promise.resolve();
});
expect(memberCardRenderSpy).toHaveBeenCalledTimes(2);
memberCardRenderSpy.mockClear();
await act(async () => {
root.render(
React.createElement(MemberList, {
members,
isTeamAlive: true,
memberRuntimeEntries: new Map([
[
'bob',
liveRuntimeEntry({
cpuPercent: 12,
rssBytes: 240 * 1024 * 1024,
resourceHistory: [
{
timestamp: '2026-05-31T10:00:05.000Z',
rssBytes: 230 * 1024 * 1024,
cpuPercent: 10,
},
{
timestamp: '2026-05-31T10:00:10.000Z',
rssBytes: 240 * 1024 * 1024,
cpuPercent: 12,
},
],
}),
],
]),
})
);
await Promise.resolve();
});
expect(memberCardRenderSpy).not.toHaveBeenCalled();
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');