From 078cb24b4d30f2f8d59338bfccf5907d625c6b40 Mon Sep 17 00:00:00 2001 From: limityan Date: Fri, 5 Jun 2026 19:38:19 +0800 Subject: [PATCH] perf(web-ui): defer inactive session metadata startup --- .../sections/sessions/SessionsSection.tsx | 109 +++++++++++++++++- .../sessions/sessionMetadataStartup.test.ts | 84 ++++++++++++++ .../sessions/sessionMetadataStartup.ts | 55 +++++++++ .../startupPerformanceContract.test.ts | 17 +++ .../performance/startup-session-perf.spec.ts | 108 ++++++++++++----- 5 files changed, 341 insertions(+), 32 deletions(-) create mode 100644 src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.test.ts create mode 100644 src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.ts diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index 8d0a50c97..c934c9780 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -44,6 +44,15 @@ import { computeFixedPopoverPosition } from '@/shared/utils/fixedPopoverViewport import { sessionAPI } from '@/infrastructure/api/service-api/SessionAPI'; import { confirmWarning } from '@/component-library/components/ConfirmDialog/confirmService'; import ScheduledJobsModal from '@/app/components/scheduled-jobs/ScheduledJobsModal'; +import { scheduleAfterStartupPaint, scheduleAfterStartupSignal } from '@/shared/utils/startupTaskScheduling'; +import { + SESSION_METADATA_DEFERRED_FALLBACK_MS, + SESSION_METADATA_DEFERRED_FRAME_COUNT, + SESSION_METADATA_DEFERRED_SIGNAL, + getDeferredSessionMetadataDelayMs, + getInitialSessionMetadataLoadMode, + hasStartupOverlayHandedOff, +} from './sessionMetadataStartup'; import './SessionsSection.scss'; /** Top-level parent sessions shown at each expand step (children still nest under visible parents). */ @@ -127,7 +136,7 @@ const SessionsSection: React.FC = ({ workspacePath, remoteConnectionId = null, remoteSshHost = null, - isActiveWorkspace: _isActiveWorkspace = true, + isActiveWorkspace = true, assistantLabel, showSessionModeIcon = true, isVisible = true, @@ -164,6 +173,7 @@ const SessionsSection: React.FC = ({ const sessionMenuPopoverRef = useRef(null); const sessionMenuAnchorRef = useRef(null); const metadataLoadRequestIdRef = useRef(0); + const initialMetadataLoadKeyRef = useRef(null); // Subscribe to state machine changes for running status useEffect(() => { @@ -203,6 +213,7 @@ const SessionsSection: React.FC = ({ useEffect(() => { metadataLoadRequestIdRef.current += 1; + initialMetadataLoadKeyRef.current = null; setExpandLevel(0); setMetadataPageState({ totalTopLevelCount: null, @@ -251,13 +262,98 @@ const SessionsSection: React.FC = ({ [workspacePath, remoteConnectionId, remoteSshHost] ); + const initialMetadataKey = useMemo( + () => [ + workspacePath ?? '', + remoteConnectionId ?? '', + remoteSshHost ?? '', + ].join('\n'), + [workspacePath, remoteConnectionId, remoteSshHost], + ); + + const loadInitialMetadataPage = useCallback( + async (source: string) => { + if (!workspacePath) { + return; + } + if (initialMetadataLoadKeyRef.current === initialMetadataKey) { + return; + } + + initialMetadataLoadKeyRef.current = initialMetadataKey; + const page = await loadMetadataPage(SESSIONS_LEVEL_0, undefined, source); + if (!page && initialMetadataLoadKeyRef.current === initialMetadataKey) { + initialMetadataLoadKeyRef.current = null; + } + }, + [initialMetadataKey, loadMetadataPage, workspacePath], + ); + useEffect(() => { if (!isVisible || !workspacePath) { return; } - void loadMetadataPage(SESSIONS_LEVEL_0, undefined, 'sessions_nav_initial'); - }, [isVisible, workspacePath, remoteConnectionId, remoteSshHost, loadMetadataPage]); + const loadMode = getInitialSessionMetadataLoadMode({ + hasWorkspacePath: Boolean(workspacePath), + isActiveWorkspace, + isVisible, + startupOverlayHandedOff: hasStartupOverlayHandedOff(), + }); + + if (loadMode === 'skip') { + return; + } + + if (loadMode === 'immediate') { + void loadInitialMetadataPage('sessions_nav_initial_active'); + return; + } + + let cancelled = false; + let delayTimer: number | null = null; + const scheduleDeferredMetadataLoad = () => { + if (cancelled) { + return; + } + const delayMs = getDeferredSessionMetadataDelayMs(workspaceId ?? workspacePath); + const runDeferredLoad = () => { + delayTimer = null; + if (!cancelled) { + void loadInitialMetadataPage('sessions_nav_initial_deferred'); + } + }; + + if (delayMs > 0) { + delayTimer = window.setTimeout(runDeferredLoad, delayMs); + return; + } + runDeferredLoad(); + }; + const cancelStartupSchedule = loadMode === 'after-startup-paint' + ? scheduleAfterStartupPaint(scheduleDeferredMetadataLoad, { + frameCount: SESSION_METADATA_DEFERRED_FRAME_COUNT, + }) + : scheduleAfterStartupSignal(scheduleDeferredMetadataLoad, { + signalName: SESSION_METADATA_DEFERRED_SIGNAL, + fallbackTimeoutMs: SESSION_METADATA_DEFERRED_FALLBACK_MS, + frameCount: SESSION_METADATA_DEFERRED_FRAME_COUNT, + }); + + return () => { + cancelled = true; + cancelStartupSchedule(); + if (delayTimer !== null) { + window.clearTimeout(delayTimer); + } + }; + }, [ + isActiveWorkspace, + isVisible, + loadInitialMetadataPage, + workspaceId, + workspacePath, + ]); useEffect(() => { if (!openMenuSessionId) return; @@ -379,6 +475,12 @@ const SessionsSection: React.FC = ({ const totalTopLevelSessionCount = metadataPageState.totalTopLevelCount ?? topLevelSessions.length; const hasMoreUnloadedSessions = metadataPageState.hasMore || topLevelSessions.length < totalTopLevelSessionCount; + const expandToggleAction = + expandLevel === 0 + ? 'show-more' + : expandLevel === 1 && totalTopLevelSessionCount > SESSIONS_LEVEL_1 + ? 'show-all' + : 'show-less'; const visibleItems = useMemo(() => { const visibleParents = topLevelSessions.slice(0, sessionDisplayLimit); @@ -961,6 +1063,7 @@ const SessionsSection: React.FC = ({ type="button" className={`bitfun-nav-panel__inline-toggle${metadataPageState.isLoading ? ' is-loading' : ''}`} data-testid="session-nav-show-more" + data-session-nav-toggle-action={expandToggleAction} disabled={metadataPageState.isLoading} onClick={() => { void handleExpandToggle(); }} > diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.test.ts b/src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.test.ts new file mode 100644 index 000000000..a9422a7ea --- /dev/null +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.test.ts @@ -0,0 +1,84 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { + SESSION_METADATA_DEFERRED_FALLBACK_MS, + SESSION_METADATA_DEFERRED_FRAME_COUNT, + SESSION_METADATA_DEFERRED_SIGNAL, + getDeferredSessionMetadataDelayMs, + getInitialSessionMetadataLoadMode, + hasStartupOverlayHandedOff, +} from './sessionMetadataStartup'; + +describe('session metadata startup scheduling', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('chooses the startup gate for each initial metadata load path', () => { + expect(getInitialSessionMetadataLoadMode({ + hasWorkspacePath: false, + isActiveWorkspace: true, + isVisible: true, + startupOverlayHandedOff: false, + })).toBe('skip'); + + expect(getInitialSessionMetadataLoadMode({ + hasWorkspacePath: true, + isActiveWorkspace: true, + isVisible: true, + startupOverlayHandedOff: false, + })).toBe('immediate'); + + expect(getInitialSessionMetadataLoadMode({ + hasWorkspacePath: true, + isActiveWorkspace: false, + isVisible: true, + startupOverlayHandedOff: false, + })).toBe('after-startup-signal'); + + expect(getInitialSessionMetadataLoadMode({ + hasWorkspacePath: true, + isActiveWorkspace: false, + isVisible: true, + startupOverlayHandedOff: true, + })).toBe('after-startup-paint'); + + expect(getInitialSessionMetadataLoadMode({ + hasWorkspacePath: true, + isActiveWorkspace: false, + isVisible: false, + startupOverlayHandedOff: true, + })).toBe('skip'); + }); + + it('keeps deferred metadata tied to the startup overlay handoff', () => { + expect(SESSION_METADATA_DEFERRED_SIGNAL).toBe('bitfun:startup-overlay-hidden'); + expect(SESSION_METADATA_DEFERRED_FALLBACK_MS).toBe(10000); + expect(SESSION_METADATA_DEFERRED_FRAME_COUNT).toBe(1); + }); + + it('uses a stable bounded stagger for background workspace metadata', () => { + const first = getDeferredSessionMetadataDelayMs('D:\\workspace\\BitFun'); + const second = getDeferredSessionMetadataDelayMs('D:\\workspace\\BitFun'); + const other = getDeferredSessionMetadataDelayMs('D:\\workspace\\Other'); + + expect(first).toBe(second); + expect(first).toBeGreaterThanOrEqual(0); + expect(first).toBeLessThanOrEqual(120); + expect(other).toBeGreaterThanOrEqual(0); + expect(other).toBeLessThanOrEqual(120); + expect(getDeferredSessionMetadataDelayMs()).toBe(0); + }); + + it('detects when a late-mounted section has already missed the overlay handoff event', () => { + vi.stubGlobal('document', { + getElementById: vi.fn(() => ({})), + }); + expect(hasStartupOverlayHandedOff()).toBe(false); + + vi.stubGlobal('document', { + getElementById: vi.fn(() => null), + }); + expect(hasStartupOverlayHandedOff()).toBe(true); + }); +}); diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.ts b/src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.ts new file mode 100644 index 000000000..89277eb12 --- /dev/null +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/sessionMetadataStartup.ts @@ -0,0 +1,55 @@ +import { STARTUP_OVERLAY_HIDDEN_EVENT } from '@/app/startup/startupSignals'; +import { isStartupOverlayPresent } from '@/app/startup/startupOverlay'; + +export const SESSION_METADATA_DEFERRED_SIGNAL = STARTUP_OVERLAY_HIDDEN_EVENT; +export const SESSION_METADATA_DEFERRED_FALLBACK_MS = 10000; +export const SESSION_METADATA_DEFERRED_FRAME_COUNT = 1; + +export type InitialSessionMetadataLoadMode = + | 'skip' + | 'immediate' + | 'after-startup-signal' + | 'after-startup-paint'; + +export function getInitialSessionMetadataLoadMode({ + hasWorkspacePath, + isActiveWorkspace, + isVisible, + startupOverlayHandedOff, +}: { + hasWorkspacePath: boolean; + isActiveWorkspace: boolean; + isVisible: boolean; + startupOverlayHandedOff: boolean; +}): InitialSessionMetadataLoadMode { + if (!hasWorkspacePath || !isVisible) { + return 'skip'; + } + + if (isActiveWorkspace) { + return 'immediate'; + } + + return startupOverlayHandedOff ? 'after-startup-paint' : 'after-startup-signal'; +} + +export function getDeferredSessionMetadataDelayMs(workspaceKey?: string | null): number { + const normalized = workspaceKey?.trim(); + if (!normalized) { + return 0; + } + + let hash = 0; + for (let index = 0; index < normalized.length; index += 1) { + hash = (hash * 31 + normalized.charCodeAt(index)) >>> 0; + } + + return (hash % 4) * 40; +} + +export function hasStartupOverlayHandedOff(): boolean { + if (typeof document === 'undefined') { + return true; + } + return !isStartupOverlayPresent(); +} diff --git a/src/web-ui/src/app/startup/startupPerformanceContract.test.ts b/src/web-ui/src/app/startup/startupPerformanceContract.test.ts index 649aa1d19..349d6945d 100644 --- a/src/web-ui/src/app/startup/startupPerformanceContract.test.ts +++ b/src/web-ui/src/app/startup/startupPerformanceContract.test.ts @@ -234,6 +234,23 @@ describe('startup performance contract', () => { expect(getSource).not.toContain('restore_session'); }); + it('keeps non-active workspace session metadata out of the first startup window', () => { + const source = readSource('../../app/components/NavPanel/sections/sessions/SessionsSection.tsx'); + + expect(source).toContain('isActiveWorkspace = true'); + expect(source).not.toContain('isActiveWorkspace: _isActiveWorkspace'); + expect(source).toContain('getInitialSessionMetadataLoadMode'); + expect(source).toContain("loadMode === 'immediate'"); + expect(source).toContain("loadMode === 'after-startup-paint'"); + expect(source).toContain('scheduleAfterStartupSignal'); + expect(source).toContain('scheduleAfterStartupPaint'); + expect(source).toContain('hasStartupOverlayHandedOff'); + expect(source).toContain('SESSION_METADATA_DEFERRED_SIGNAL'); + expect(source).toContain('sessions_nav_initial_active'); + expect(source).toContain('sessions_nav_initial_deferred'); + expect(source).toContain('data-session-nav-toggle-action'); + }); + it('keeps Git diff editor from importing the broad editor barrel', () => { const source = readSource('../../tools/git/components/GitDiffEditor/GitDiffEditor.tsx'); diff --git a/tests/e2e/specs/performance/startup-session-perf.spec.ts b/tests/e2e/specs/performance/startup-session-perf.spec.ts index 4f349a363..ae4c84b91 100644 --- a/tests/e2e/specs/performance/startup-session-perf.spec.ts +++ b/tests/e2e/specs/performance/startup-session-perf.spec.ts @@ -603,43 +603,93 @@ async function waitForOptionalTracePhaseForSessionSince( } async function findSessionItem(sessionId: string): Promise | null> { - let lastVisibleSessionIds: string[] = []; - for (let attempt = 0; attempt < 40; attempt += 1) { - const item = await $(`[data-testid="session-nav-item"][data-session-id="${sessionId}"]`); - if (await item.isExisting()) { - return item; - } - - lastVisibleSessionIds = await browser.execute(() => + const readVisibleSessionIds = async (): Promise => + browser.execute(() => Array.from(document.querySelectorAll('[data-testid="session-nav-item"]')) .map(element => element.getAttribute('data-session-id') || '') .filter(Boolean) ); - const showMore = await $('[data-testid="session-nav-show-more"]'); - if (!(await showMore.isExisting()) || !(await showMore.isEnabled())) { + + const findTarget = async (): Promise | null> => { + const item = await $(`[data-testid="session-nav-item"][data-session-id="${sessionId}"]`); + return await item.isExisting() ? item : null; + }; + + const findExpandableToggles = async (): Promise>> => { + const toggles = await browser.$$('[data-testid="session-nav-show-more"]'); + const expandable: Array> = []; + for (const toggle of toggles) { + if ( + !(await toggle.isExisting()) || + !(await toggle.isDisplayed()) || + !(await toggle.isEnabled()) + ) { + continue; + } + + const action = await toggle.getAttribute('data-session-nav-toggle-action').catch(() => null); + if (action === 'show-less') { + continue; + } + expandable.push(toggle); + } + return expandable; + }; + + let lastVisibleSessionIds: string[] = []; + for (let attempt = 0; attempt < 12; attempt += 1) { + const existing = await findTarget(); + if (existing) { + return existing; + } + + lastVisibleSessionIds = await readVisibleSessionIds(); + const toggles = await findExpandableToggles(); + if (toggles.length === 0) { break; } - const beforeCount = lastVisibleSessionIds.length; - await showMore.click(); - await browser.waitUntil(async () => { - const ids = await browser.execute(() => - Array.from(document.querySelectorAll('[data-testid="session-nav-item"]')) - .map(element => element.getAttribute('data-session-id') || '') - .filter(Boolean) - ); - const toggle = await $('[data-testid="session-nav-show-more"]'); - const toggleReady = !(await toggle.isExisting()) || (await toggle.isEnabled()); - return ids.length !== beforeCount && toggleReady; - }, { timeout: 3000, interval: 100 }).catch(() => undefined); + let clickedAny = false; + for (let toggleIndex = 0; toggleIndex < toggles.length; toggleIndex += 1) { + const item = await findTarget(); + if (item) { + return item; + } - const currentVisibleSessionIds = await browser.execute(() => - Array.from(document.querySelectorAll('[data-testid="session-nav-item"]')) - .map(element => element.getAttribute('data-session-id') || '') - .filter(Boolean) - ); - if (currentVisibleSessionIds.length <= beforeCount && attempt > 0) { - lastVisibleSessionIds = currentVisibleSessionIds; + const currentToggles = await findExpandableToggles(); + const toggle = currentToggles[toggleIndex]; + if (!toggle) { + break; + } + + if ( + !(await toggle.isExisting()) || + !(await toggle.isDisplayed()) || + !(await toggle.isEnabled()) + ) { + continue; + } + + const action = await toggle.getAttribute('data-session-nav-toggle-action').catch(() => null); + if (action === 'show-less') { + continue; + } + + const beforeCount = lastVisibleSessionIds.length; + clickedAny = true; + await toggle.click(); + await browser.waitUntil(async () => { + if (await findTarget()) { + return true; + } + const ids = await readVisibleSessionIds(); + const nextToggles = await findExpandableToggles(); + return ids.length !== beforeCount || nextToggles.length !== toggles.length; + }, { timeout: 3000, interval: 100 }).catch(() => undefined); + lastVisibleSessionIds = await readVisibleSessionIds(); + } + + if (!clickedAny) { break; } }