perf(renderer): cache member runtime telemetry renders
This commit is contained in:
parent
0924a35682
commit
9a1c778d67
2 changed files with 121 additions and 6 deletions
|
|
@ -331,6 +331,83 @@ function areMemberRuntimeEntriesEquivalent(
|
||||||
return true;
|
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 {
|
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;
|
||||||
}
|
}
|
||||||
|
|
@ -730,6 +807,7 @@ export const MemberList = memo(function MemberList({
|
||||||
const [isWide, setIsWide] = useState(false);
|
const [isWide, setIsWide] = useState(false);
|
||||||
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>());
|
||||||
memberRuntimeEntriesRef.current = memberRuntimeEntries;
|
memberRuntimeEntriesRef.current = memberRuntimeEntries;
|
||||||
|
|
||||||
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
|
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
|
||||||
|
|
@ -793,6 +871,17 @@ export const MemberList = memo(function MemberList({
|
||||||
: undefined,
|
: undefined,
|
||||||
[activeMembers, memberRuntimeEntries, runtimeTelemetryPreviewActive]
|
[activeMembers, memberRuntimeEntries, 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),
|
||||||
[activeMembers, memberRuntimeEntries]
|
[activeMembers, memberRuntimeEntries]
|
||||||
|
|
@ -969,7 +1058,10 @@ export const MemberList = memo(function MemberList({
|
||||||
<div className={gridClass}>
|
<div className={gridClass}>
|
||||||
{activeMembers.map((member) => {
|
{activeMembers.map((member) => {
|
||||||
const spawnEntry = memberSpawnStatuses?.get(member.name);
|
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 =
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
const hasUnsafeProvisionedButNotAliveEvidence =
|
const hasUnsafeProvisionedButNotAliveEvidence =
|
||||||
|
|
@ -1062,14 +1154,14 @@ export const MemberList = memo(function MemberList({
|
||||||
reviewTask={reviewTask}
|
reviewTask={reviewTask}
|
||||||
currentTaskTimer={currentTaskTimer}
|
currentTaskTimer={currentTaskTimer}
|
||||||
reviewTaskTimer={reviewTaskTimer}
|
reviewTaskTimer={reviewTaskTimer}
|
||||||
currentTaskTimerRunning={activityTimerRunning}
|
currentTaskTimerRunning={currentTask !== null && activityTimerRunning}
|
||||||
reviewTaskTimerRunning={activityTimerRunning}
|
reviewTaskTimerRunning={reviewTask !== null && activityTimerRunning}
|
||||||
awaitingReply={
|
awaitingReply={
|
||||||
isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name])
|
isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name])
|
||||||
}
|
}
|
||||||
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
|
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
|
||||||
runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)}
|
runtimeSummary={buildRuntimeSummary(member, spawnEntry, displayRuntimeEntry)}
|
||||||
runtimeEntry={runtimeEntry}
|
runtimeEntry={displayRuntimeEntry}
|
||||||
runtimeRunId={runtimeRunId}
|
runtimeRunId={runtimeRunId}
|
||||||
spawnStatus={effectiveSpawnStatus}
|
spawnStatus={effectiveSpawnStatus}
|
||||||
spawnEntry={spawnEntry}
|
spawnEntry={spawnEntry}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
@ -435,6 +435,7 @@ describe('MemberList spawn-status memoization', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(memberCardRenderSpy).not.toHaveBeenCalled();
|
expect(memberCardRenderSpy).not.toHaveBeenCalled();
|
||||||
|
memberCardRenderSpy.mockClear();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -461,6 +462,28 @@ describe('MemberList spawn-status memoization', () => {
|
||||||
await Promise.resolve();
|
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);
|
expect(memberCardRenderSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue