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;
|
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 {
|
function isFiniteNonNegative(value: number | undefined): value is number {
|
||||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
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 [runtimeTelemetryPreviewActive, setRuntimeTelemetryPreviewActive] = useState(false);
|
||||||
const memberRuntimeEntriesRef = useRef(memberRuntimeEntries);
|
const memberRuntimeEntriesRef = useRef(memberRuntimeEntries);
|
||||||
const memberRuntimeEntryCacheRef = useRef(new Map<string, CachedMemberRuntimeEntry>());
|
const memberRuntimeEntryCacheRef = useRef(new Map<string, CachedMemberRuntimeEntry>());
|
||||||
|
const displayedRuntimeEntriesRef = useRef<Map<string, TeamAgentRuntimeEntry> | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
memberRuntimeEntriesRef.current = memberRuntimeEntries;
|
memberRuntimeEntriesRef.current = memberRuntimeEntries;
|
||||||
|
|
||||||
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
|
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
|
||||||
|
|
@ -864,23 +882,25 @@ export const MemberList = memo(function MemberList({
|
||||||
);
|
);
|
||||||
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
||||||
const avatarMap = useMemo(() => buildMemberAvatarMap(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(
|
const runtimeTelemetryScale = useMemo(
|
||||||
() =>
|
() =>
|
||||||
runtimeTelemetryPreviewActive
|
runtimeTelemetryPreviewActive
|
||||||
? buildRuntimeTelemetryScale(activeMembers, memberRuntimeEntries)
|
? buildRuntimeTelemetryScale(activeMembers, cardRuntimeEntries)
|
||||||
: undefined,
|
: undefined,
|
||||||
[activeMembers, memberRuntimeEntries, runtimeTelemetryPreviewActive]
|
[activeMembers, cardRuntimeEntries, runtimeTelemetryPreviewActive]
|
||||||
);
|
|
||||||
const cardRuntimeEntries = useMemo(
|
|
||||||
() =>
|
|
||||||
runtimeTelemetryPreviewActive
|
|
||||||
? memberRuntimeEntries
|
|
||||||
: buildCachedMemberRuntimeEntries(
|
|
||||||
memberRuntimeEntries,
|
|
||||||
memberRuntimeEntryCacheRef.current,
|
|
||||||
Date.now()
|
|
||||||
),
|
|
||||||
[memberRuntimeEntries, runtimeTelemetryPreviewActive]
|
|
||||||
);
|
);
|
||||||
const activityTimerRuntimeSignature = useMemo(
|
const activityTimerRuntimeSignature = useMemo(
|
||||||
() => buildActivityTimerRuntimeSignature(activeMembers, memberRuntimeEntries),
|
() => 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 () => {
|
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