feat(infra) - Add logging for slow rendering (#11147)

Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
This commit is contained in:
shishu314
2025-10-31 15:51:05 -04:00
committed by GitHub
parent adddafe6d0
commit 6ee7165e39
2 changed files with 103 additions and 36 deletions

View File

@@ -21,7 +21,33 @@ import {
} from './gemini.js';
import { type LoadedSettings } from './config/settings.js';
import { appEvents, AppEvent } from './utils/events.js';
import type { Config } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';
import { act } from 'react';
import { type InitializationResult } from './core/initializer.js';
const performance = vi.hoisted(() => ({
now: vi.fn(),
}));
vi.stubGlobal('performance', performance);
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
recordSlowRender: vi.fn(),
};
});
vi.mock('ink', async (importOriginal) => {
const actual = await importOriginal<typeof import('ink')>();
return {
...actual,
// Mock here so we can spyOn the render function. ink uses ESM which doesn't
// allow us to spyOn it directly.
render: vi.fn((_node, options) => actual.render(_node, options)),
};
});
// Custom error to identify mock process.exit calls
class MockProcessExitError extends Error {
@@ -295,6 +321,7 @@ describe('gemini.tsx main function kitty protocol', () => {
} else {
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
}
vi.restoreAllMocks();
});
it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => {
@@ -367,7 +394,9 @@ describe('gemini.tsx main function kitty protocol', () => {
recordResponses: undefined,
});
await main();
await act(async () => {
await main();
});
expect(setRawModeSpy).toHaveBeenCalledWith(true);
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
@@ -382,7 +411,7 @@ describe('validateDnsResolutionOrder', () => {
});
afterEach(() => {
consoleWarnSpy.mockRestore();
vi.restoreAllMocks();
});
it('should return "ipv4first" when the input is "ipv4first"', () => {
@@ -423,6 +452,12 @@ describe('startInteractiveUI', () => {
} as LoadedSettings;
const mockStartupWarnings = ['warning1'];
const mockWorkspaceRoot = '/root';
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
vi.mock('./utils/version.js', () => ({
getCliVersion: vi.fn(() => Promise.resolve('1.0.0')),
@@ -442,26 +477,33 @@ describe('startInteractiveUI', () => {
runExitCleanup: vi.fn(),
}));
vi.mock('ink', () => ({
render: vi.fn().mockReturnValue({ unmount: vi.fn() }),
}));
beforeEach(() => {
vi.clearAllMocks();
afterEach(() => {
vi.restoreAllMocks();
});
async function startTestInteractiveUI(
config: Config,
settings: LoadedSettings,
startupWarnings: string[],
workspaceRoot: string,
initializationResult: InitializationResult,
) {
await act(async () => {
await startInteractiveUI(
config,
settings,
startupWarnings,
workspaceRoot,
initializationResult,
);
});
}
it('should render the UI with proper React context and exitOnCtrlC disabled', async () => {
const { render } = await import('ink');
const renderSpy = vi.mocked(render);
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
await startTestInteractiveUI(
mockConfig,
mockSettings,
mockStartupWarnings,
@@ -477,6 +519,7 @@ describe('startInteractiveUI', () => {
expect(options).toEqual({
exitOnCtrlC: false,
isScreenReaderEnabled: false,
onRender: expect.any(Function),
});
// Verify React element structure is valid (but don't deep dive into JSX internals)
@@ -488,14 +531,7 @@ describe('startInteractiveUI', () => {
const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
const { registerCleanup } = await import('./utils/cleanup.js');
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
await startTestInteractiveUI(
mockConfig,
mockSettings,
mockStartupWarnings,
@@ -517,6 +553,36 @@ describe('startInteractiveUI', () => {
expect(checkForUpdates).toHaveBeenCalledTimes(1);
});
it('should not recordSlowRender when less than threshold', async () => {
const { recordSlowRender } = await import('@google/gemini-cli-core');
performance.now.mockReturnValueOnce(0);
await startTestInteractiveUI(
mockConfig,
mockSettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);
expect(recordSlowRender).not.toHaveBeenCalled();
});
it('should call recordSlowRender when more than threshold', async () => {
const { recordSlowRender } = await import('@google/gemini-cli-core');
performance.now.mockReturnValueOnce(0);
performance.now.mockReturnValueOnce(300);
await startTestInteractiveUI(
mockConfig,
mockSettings,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);
expect(recordSlowRender).toHaveBeenCalledWith(mockConfig, 300);
});
it.each([
{
screenReader: true,
@@ -537,14 +603,7 @@ describe('startInteractiveUI', () => {
getScreenReader: () => screenReader,
} as Config;
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
await startTestInteractiveUI(
mockConfigWithScreenReader,
mockSettings,
mockStartupWarnings,

View File

@@ -5,7 +5,7 @@
*/
import React from 'react';
import { render } from 'ink';
import { render, type RenderOptions } from 'ink';
import { AppContainer } from './ui/AppContainer.js';
import { loadCliConfig, parseArguments } from './config/config.js';
import * as cliConfig from './config/config.js';
@@ -32,7 +32,7 @@ import {
runExitCleanup,
} from './utils/cleanup.js';
import { getCliVersion } from './utils/version.js';
import type { Config } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';
import {
sessionId,
logUserPrompt,
@@ -40,6 +40,7 @@ import {
getOauthClient,
UserPromptEvent,
debugLogger,
recordSlowRender,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -70,6 +71,8 @@ import { ExtensionManager } from './config/extension-manager.js';
import { createPolicyUpdater } from './config/policy.js';
import { requestConsentNonInteractive } from './config/extensions/consent.js';
const SLOW_RENDER_MS = 200;
export function validateDnsResolutionOrder(
order: string | undefined,
): DnsResolutionOrder {
@@ -205,7 +208,12 @@ export async function startInteractiveUI(
{
exitOnCtrlC: false,
isScreenReaderEnabled: config.getScreenReader(),
},
onRender: ({ renderTime }: { renderTime: number }) => {
if (renderTime > SLOW_RENDER_MS) {
recordSlowRender(config, renderTime);
}
},
} as RenderOptions,
);
checkForUpdates(settings)