From a4ad5f629721266a80e64e14496b54bcbba3e83f Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 05:58:49 +0300 Subject: [PATCH] perf(renderer): cache member telemetry preview snapshots --- .../components/team/members/MemberList.tsx | 46 +++++++++---- .../team/members/MemberList.test.ts | 68 +++++++++++++++++++ 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/renderer/components/team/members/MemberList.tsx b/src/renderer/components/team/members/MemberList.tsx index fa069a69..d960bfb6 100644 --- a/src/renderer/components/team/members/MemberList.tsx +++ b/src/renderer/components/team/members/MemberList.tsx @@ -408,6 +408,21 @@ function buildCachedMemberRuntimeEntries( return nextEntries.size > 0 ? nextEntries : undefined; } +function reuseRuntimeEntriesMapIfUnchanged( + previous: Map | undefined, + next: Map | undefined +): Map | 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()); + const displayedRuntimeEntriesRef = useRef | 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), diff --git a/test/renderer/components/team/members/MemberList.test.ts b/test/renderer/components/team/members/MemberList.test.ts index 2e8794cd..7fd861db 100644 --- a/test/renderer/components/team/members/MemberList.test.ts +++ b/test/renderer/components/team/members/MemberList.test.ts @@ -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');