perf(renderer): cache member runtime telemetry renders

This commit is contained in:
777genius 2026-05-31 05:05:54 +03:00
parent 0924a35682
commit 9a1c778d67
2 changed files with 121 additions and 6 deletions

View file

@ -331,6 +331,83 @@ function areMemberRuntimeEntriesEquivalent(
return true;
}
const MEMBER_CARD_RUNTIME_TELEMETRY_CACHE_MS = 30_000;
interface CachedMemberRuntimeEntry {
signature: string;
cachedAt: number;
entry: TeamAgentRuntimeEntry;
}
function buildMemberRuntimeCardSignature(entry: TeamAgentRuntimeEntry): string {
const diagnostics = Array.isArray(entry.diagnostics) ? entry.diagnostics : [];
return [
entry.memberName,
entry.alive,
entry.restartable,
entry.backendType,
entry.providerId,
entry.providerBackendId,
entry.laneId,
entry.laneKind,
entry.pid,
entry.runtimeModel,
entry.processCount,
entry.runtimeLoadScope,
entry.runtimeLoadTruncated,
entry.livenessKind,
entry.pidSource,
entry.processCommand,
entry.paneId,
entry.panePid,
entry.paneCurrentCommand,
entry.runtimePid,
entry.runtimeSessionId,
entry.runtimeDiagnostic,
entry.runtimeDiagnosticSeverity,
entry.runtimeLastSeenAt,
entry.historicalBootstrapConfirmed,
diagnostics.join('\u001f'),
].join('\u001e');
}
function buildCachedMemberRuntimeEntries(
runtimeEntries: Map<string, TeamAgentRuntimeEntry> | undefined,
cache: Map<string, CachedMemberRuntimeEntry>,
nowMs: number
): Map<string, TeamAgentRuntimeEntry> | undefined {
if (!runtimeEntries) {
cache.clear();
return undefined;
}
const nextEntries = new Map<string, TeamAgentRuntimeEntry>();
const seenMemberNames = new Set<string>();
for (const [memberName, entry] of runtimeEntries) {
seenMemberNames.add(memberName);
const signature = buildMemberRuntimeCardSignature(entry);
const cached = cache.get(memberName);
if (
!cached ||
cached.signature !== signature ||
nowMs - cached.cachedAt >= MEMBER_CARD_RUNTIME_TELEMETRY_CACHE_MS
) {
cache.set(memberName, { signature, cachedAt: nowMs, entry });
nextEntries.set(memberName, entry);
continue;
}
nextEntries.set(memberName, cached.entry);
}
for (const memberName of cache.keys()) {
if (!seenMemberNames.has(memberName)) {
cache.delete(memberName);
}
}
return nextEntries.size > 0 ? nextEntries : undefined;
}
function isFiniteNonNegative(value: number | undefined): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
@ -730,6 +807,7 @@ export const MemberList = memo(function MemberList({
const [isWide, setIsWide] = useState(false);
const [runtimeTelemetryPreviewActive, setRuntimeTelemetryPreviewActive] = useState(false);
const memberRuntimeEntriesRef = useRef(memberRuntimeEntries);
const memberRuntimeEntryCacheRef = useRef(new Map<string, CachedMemberRuntimeEntry>());
memberRuntimeEntriesRef.current = memberRuntimeEntries;
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
@ -793,6 +871,17 @@ export const MemberList = memo(function MemberList({
: undefined,
[activeMembers, memberRuntimeEntries, runtimeTelemetryPreviewActive]
);
const cardRuntimeEntries = useMemo(
() =>
runtimeTelemetryPreviewActive
? memberRuntimeEntries
: buildCachedMemberRuntimeEntries(
memberRuntimeEntries,
memberRuntimeEntryCacheRef.current,
Date.now()
),
[memberRuntimeEntries, runtimeTelemetryPreviewActive]
);
const activityTimerRuntimeSignature = useMemo(
() => buildActivityTimerRuntimeSignature(activeMembers, memberRuntimeEntries),
[activeMembers, memberRuntimeEntries]
@ -969,7 +1058,10 @@ export const MemberList = memo(function MemberList({
<div className={gridClass}>
{activeMembers.map((member) => {
const spawnEntry = memberSpawnStatuses?.get(member.name);
const runtimeEntry = memberRuntimeEntries?.get(member.name);
const liveRuntimeEntry = memberRuntimeEntries?.get(member.name);
const cardRuntimeEntry = cardRuntimeEntries?.get(member.name);
const runtimeEntry = liveRuntimeEntry;
const displayRuntimeEntry = cardRuntimeEntry ?? liveRuntimeEntry;
const bootstrapConfirmedProvisionedButNotAlive =
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
const hasUnsafeProvisionedButNotAliveEvidence =
@ -1062,14 +1154,14 @@ export const MemberList = memo(function MemberList({
reviewTask={reviewTask}
currentTaskTimer={currentTaskTimer}
reviewTaskTimer={reviewTaskTimer}
currentTaskTimerRunning={activityTimerRunning}
reviewTaskTimerRunning={activityTimerRunning}
currentTaskTimerRunning={currentTask !== null && activityTimerRunning}
reviewTaskTimerRunning={reviewTask !== null && activityTimerRunning}
awaitingReply={
isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name])
}
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)}
runtimeEntry={runtimeEntry}
runtimeSummary={buildRuntimeSummary(member, spawnEntry, displayRuntimeEntry)}
runtimeEntry={displayRuntimeEntry}
runtimeRunId={runtimeRunId}
spawnStatus={effectiveSpawnStatus}
spawnEntry={spawnEntry}

View file

@ -384,7 +384,7 @@ describe('MemberList spawn-status memoization', () => {
});
});
it('does not rerender cards when only runtime telemetry history changes', async () => {
it('does not rerender cards when only cached runtime telemetry changes', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
@ -435,6 +435,7 @@ describe('MemberList spawn-status memoization', () => {
});
expect(memberCardRenderSpy).not.toHaveBeenCalled();
memberCardRenderSpy.mockClear();
await act(async () => {
root.render(
@ -461,6 +462,28 @@ describe('MemberList spawn-status memoization', () => {
await Promise.resolve();
});
expect(memberCardRenderSpy).not.toHaveBeenCalled();
memberCardRenderSpy.mockClear();
await act(async () => {
root.render(
React.createElement(MemberList, {
members,
isTeamAlive: true,
memberRuntimeEntries: new Map([
[
'bob',
liveRuntimeEntry({
runtimeDiagnosticSeverity: 'error',
runtimeDiagnostic: 'runtime went stale',
}),
],
]),
})
);
await Promise.resolve();
});
expect(memberCardRenderSpy).toHaveBeenCalledTimes(1);
await act(async () => {