perf(renderer): cache member telemetry preview snapshots
This commit is contained in:
parent
db2c82475b
commit
a4ad5f6297
2 changed files with 101 additions and 13 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue