mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 23:51:16 -07:00
244 lines
7.0 KiB
TypeScript
244 lines
7.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { Text } from 'ink';
|
|
import { useEffect, useState } from 'react';
|
|
import { FixedDeque } from 'mnemonist';
|
|
import { theme } from '../semantic-colors.js';
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
|
import { debugState } from '../debug.js';
|
|
import { appEvents, AppEvent } from '../../utils/events.js';
|
|
import { coreEvents, CoreEvent, debugLogger } from '@google/gemini-cli-core';
|
|
|
|
// Frames that render at least this far before or after an action are considered
|
|
// idle frames.
|
|
const MIN_TIME_FROM_ACTION_TO_BE_IDLE = 500;
|
|
|
|
export const ACTION_TIMESTAMP_CAPACITY = 2048;
|
|
export const FRAME_TIMESTAMP_CAPACITY = 2048;
|
|
|
|
// Exported for testing purposes.
|
|
export const profiler = {
|
|
profilersActive: 0,
|
|
numFrames: 0,
|
|
totalIdleFrames: 0,
|
|
totalFlickerFrames: 0,
|
|
hasLoggedFirstFlicker: false,
|
|
lastFrameStartTime: 0,
|
|
openedDebugConsole: false,
|
|
lastActionTimestamp: 0,
|
|
|
|
possiblyIdleFrameTimestamps: new FixedDeque<number>(
|
|
Array,
|
|
FRAME_TIMESTAMP_CAPACITY,
|
|
),
|
|
actionTimestamps: new FixedDeque<number>(Array, ACTION_TIMESTAMP_CAPACITY),
|
|
|
|
reportAction() {
|
|
const now = Date.now();
|
|
if (now - this.lastActionTimestamp > 16) {
|
|
if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
|
|
this.actionTimestamps.shift();
|
|
}
|
|
this.actionTimestamps.push(now);
|
|
this.lastActionTimestamp = now;
|
|
}
|
|
},
|
|
|
|
reportFrameRendered() {
|
|
if (this.profilersActive === 0) {
|
|
return;
|
|
}
|
|
const now = Date.now();
|
|
this.lastFrameStartTime = now;
|
|
this.numFrames++;
|
|
if (debugState.debugNumAnimatedComponents === 0) {
|
|
if (this.possiblyIdleFrameTimestamps.size >= FRAME_TIMESTAMP_CAPACITY) {
|
|
this.possiblyIdleFrameTimestamps.shift();
|
|
}
|
|
this.possiblyIdleFrameTimestamps.push(now);
|
|
} else {
|
|
// If a spinner is present, consider this an action that both prevents
|
|
// this frame from being idle and also should prevent a follow on frame
|
|
// from being considered idle.
|
|
if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
|
|
this.actionTimestamps.shift();
|
|
}
|
|
this.actionTimestamps.push(now);
|
|
}
|
|
},
|
|
|
|
checkForIdleFrames() {
|
|
const now = Date.now();
|
|
const judgementCutoff = now - MIN_TIME_FROM_ACTION_TO_BE_IDLE;
|
|
const oneSecondIntervalFromJudgementCutoff = judgementCutoff - 1000;
|
|
|
|
let idleInPastSecond = 0;
|
|
|
|
while (
|
|
this.possiblyIdleFrameTimestamps.size > 0 &&
|
|
this.possiblyIdleFrameTimestamps.peekFirst()! <= judgementCutoff
|
|
) {
|
|
const frameTime = this.possiblyIdleFrameTimestamps.shift()!;
|
|
const start = frameTime - MIN_TIME_FROM_ACTION_TO_BE_IDLE;
|
|
const end = frameTime + MIN_TIME_FROM_ACTION_TO_BE_IDLE;
|
|
|
|
while (
|
|
this.actionTimestamps.size > 0 &&
|
|
this.actionTimestamps.peekFirst()! < start
|
|
) {
|
|
this.actionTimestamps.shift();
|
|
}
|
|
|
|
const hasAction =
|
|
this.actionTimestamps.size > 0 &&
|
|
this.actionTimestamps.peekFirst()! <= end;
|
|
|
|
if (!hasAction) {
|
|
if (frameTime >= oneSecondIntervalFromJudgementCutoff) {
|
|
idleInPastSecond++;
|
|
}
|
|
this.totalIdleFrames++;
|
|
}
|
|
}
|
|
|
|
if (idleInPastSecond >= 5) {
|
|
if (this.openedDebugConsole === false) {
|
|
this.openedDebugConsole = true;
|
|
appEvents.emit(AppEvent.OpenDebugConsole);
|
|
}
|
|
debugLogger.error(
|
|
`${idleInPastSecond} frames rendered while the app was ` +
|
|
`idle in the past second. This likely indicates severe infinite loop ` +
|
|
`React state management bugs.`,
|
|
);
|
|
}
|
|
},
|
|
|
|
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;
|
|
debugLogger.error(
|
|
'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, constrainHeight } = useUIState();
|
|
const [forceRefresh, setForceRefresh] = useState(0);
|
|
|
|
// Effect for listening to stdin for keypresses and stdout for resize events.
|
|
useEffect(() => {
|
|
profiler.profilersActive++;
|
|
const stdin = process.stdin;
|
|
const stdout = process.stdout;
|
|
|
|
const handler = () => {
|
|
profiler.reportAction();
|
|
};
|
|
|
|
stdin.on('data', handler);
|
|
stdout.on('resize', handler);
|
|
|
|
// Register handlers for all core and app events to ensure they are
|
|
// considered "actions" and don't trigger spurious idle frame warnings.
|
|
// These events are expected to trigger UI renders.
|
|
for (const eventName of Object.values(CoreEvent)) {
|
|
coreEvents.on(eventName, handler);
|
|
}
|
|
|
|
for (const eventName of Object.values(AppEvent)) {
|
|
appEvents.on(eventName, handler);
|
|
}
|
|
|
|
// Register handlers for extension lifecycle events emitted on coreEvents
|
|
// but not part of the CoreEvent enum, to prevent false-positive idle warnings.
|
|
const extensionEvents = [
|
|
'extensionsStarting',
|
|
'extensionsStopping',
|
|
] as const;
|
|
for (const eventName of extensionEvents) {
|
|
coreEvents.on(eventName, handler);
|
|
}
|
|
|
|
return () => {
|
|
stdin.off('data', handler);
|
|
stdout.off('resize', handler);
|
|
|
|
for (const eventName of Object.values(CoreEvent)) {
|
|
coreEvents.off(eventName, handler);
|
|
}
|
|
|
|
for (const eventName of Object.values(AppEvent)) {
|
|
appEvents.off(eventName, handler);
|
|
}
|
|
|
|
for (const eventName of extensionEvents) {
|
|
coreEvents.off(eventName, handler);
|
|
}
|
|
|
|
profiler.profilersActive--;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const updateInterval = setInterval(() => {
|
|
profiler.checkForIdleFrames();
|
|
}, 1000);
|
|
return () => clearInterval(updateInterval);
|
|
}, []);
|
|
|
|
useEffect(
|
|
() => profiler.registerFlickerHandler(constrainHeight),
|
|
[constrainHeight],
|
|
);
|
|
|
|
// Effect for updating stats
|
|
useEffect(() => {
|
|
if (!showDebugProfiler) {
|
|
return;
|
|
}
|
|
// Only update the UX infrequently as updating the UX itself will cause
|
|
// frames to run so can disturb what we are measuring.
|
|
const forceRefreshInterval = setInterval(() => {
|
|
setForceRefresh((f) => f + 1);
|
|
profiler.reportAction();
|
|
}, 4000);
|
|
return () => clearInterval(forceRefreshInterval);
|
|
}, [showDebugProfiler]);
|
|
|
|
if (!showDebugProfiler) {
|
|
return null;
|
|
}
|
|
|
|
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.totalFlickerFrames} (flicker)
|
|
</Text>
|
|
</Text>
|
|
);
|
|
};
|