feat(ui): add flicker detection and metrics (#10821)

This commit is contained in:
Shreya Keshive
2025-10-10 13:18:38 -07:00
committed by GitHub
parent ab3804d823
commit ae48e964f0
13 changed files with 297 additions and 39 deletions

View File

@@ -5,6 +5,7 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { appEvents, AppEvent } from '../../utils/events.js';
import {
profiler,
ACTION_TIMESTAMP_CAPACITY,
@@ -157,6 +158,18 @@ describe('DebugProfiler', () => {
expect(profiler.totalIdleFrames).toBe(3);
});
it('should report flicker frames', () => {
const reportActionSpy = vi.spyOn(profiler, 'reportAction');
const cleanup = profiler.registerFlickerHandler(true);
appEvents.emit(AppEvent.Flicker);
expect(profiler.totalFlickerFrames).toBe(1);
expect(reportActionSpy).toHaveBeenCalled();
cleanup();
});
it('should not report idle frames when actions are interleaved', async () => {
const startTime = Date.now();
vi.setSystemTime(startTime);

View File

@@ -23,6 +23,8 @@ export const FRAME_TIMESTAMP_CAPACITY = 2048;
export const profiler = {
numFrames: 0,
totalIdleFrames: 0,
totalFlickerFrames: 0,
hasLoggedFirstFlicker: false,
lastFrameStartTime: 0,
openedDebugConsole: false,
lastActionTimestamp: 0,
@@ -114,10 +116,35 @@ export const profiler = {
);
}
},
registerFlickerHandler(constrainHeight: boolean) {
const flickerHandler = () => {
// If we are not constraining the height, we are intentionally
// overflowing the screen.
if (!constrainHeight) {
return;
}
this.totalFlickerFrames++;
this.reportAction();
if (!this.hasLoggedFirstFlicker) {
this.hasLoggedFirstFlicker = true;
appEvents.emit(
AppEvent.LogError,
'A flicker frame was detected. This will cause UI instability. Type `/profile` for more info.',
);
}
};
appEvents.on(AppEvent.Flicker, flickerHandler);
return () => {
appEvents.off(AppEvent.Flicker, flickerHandler);
};
},
};
export const DebugProfiler = () => {
const { showDebugProfiler } = useUIState();
const { showDebugProfiler, constrainHeight } = useUIState();
const [forceRefresh, setForceRefresh] = useState(0);
// Effect for listening to stdin for keypresses and stdout for resize events.
@@ -170,6 +197,11 @@ export const DebugProfiler = () => {
return () => clearInterval(updateInterval);
}, []);
useEffect(
() => profiler.registerFlickerHandler(constrainHeight),
[constrainHeight],
);
// Effect for updating stats
useEffect(() => {
if (!showDebugProfiler) {
@@ -191,7 +223,10 @@ export const DebugProfiler = () => {
return (
<Text color={theme.status.warning} key={forceRefresh}>
Renders: {profiler.numFrames} (total),{' '}
<Text color={theme.status.error}>{profiler.totalIdleFrames} (idle) </Text>
<Text color={theme.status.error}>{profiler.totalIdleFrames} (idle)</Text>,{' '}
<Text color={theme.status.error}>
{profiler.totalFlickerFrames} (flicker)
</Text>
</Text>
);
};