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
+91 -32
View File
@@ -21,7 +21,33 @@ import {
} from './gemini.js'; } from './gemini.js';
import { type LoadedSettings } from './config/settings.js'; import { type LoadedSettings } from './config/settings.js';
import { appEvents, AppEvent } from './utils/events.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 // Custom error to identify mock process.exit calls
class MockProcessExitError extends Error { class MockProcessExitError extends Error {
@@ -295,6 +321,7 @@ describe('gemini.tsx main function kitty protocol', () => {
} else { } else {
delete process.env['GEMINI_CLI_NO_RELAUNCH']; delete process.env['GEMINI_CLI_NO_RELAUNCH'];
} }
vi.restoreAllMocks();
}); });
it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => { it('should call setRawMode and detectAndEnableKittyProtocol when isInteractive is true', async () => {
@@ -367,7 +394,9 @@ describe('gemini.tsx main function kitty protocol', () => {
recordResponses: undefined, recordResponses: undefined,
}); });
await act(async () => {
await main(); await main();
});
expect(setRawModeSpy).toHaveBeenCalledWith(true); expect(setRawModeSpy).toHaveBeenCalledWith(true);
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1); expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
@@ -382,7 +411,7 @@ describe('validateDnsResolutionOrder', () => {
}); });
afterEach(() => { afterEach(() => {
consoleWarnSpy.mockRestore(); vi.restoreAllMocks();
}); });
it('should return "ipv4first" when the input is "ipv4first"', () => { it('should return "ipv4first" when the input is "ipv4first"', () => {
@@ -423,6 +452,12 @@ describe('startInteractiveUI', () => {
} as LoadedSettings; } as LoadedSettings;
const mockStartupWarnings = ['warning1']; const mockStartupWarnings = ['warning1'];
const mockWorkspaceRoot = '/root'; const mockWorkspaceRoot = '/root';
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
vi.mock('./utils/version.js', () => ({ vi.mock('./utils/version.js', () => ({
getCliVersion: vi.fn(() => Promise.resolve('1.0.0')), getCliVersion: vi.fn(() => Promise.resolve('1.0.0')),
@@ -442,26 +477,33 @@ describe('startInteractiveUI', () => {
runExitCleanup: vi.fn(), runExitCleanup: vi.fn(),
})); }));
vi.mock('ink', () => ({ afterEach(() => {
render: vi.fn().mockReturnValue({ unmount: vi.fn() }), vi.restoreAllMocks();
}));
beforeEach(() => {
vi.clearAllMocks();
}); });
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 () => { it('should render the UI with proper React context and exitOnCtrlC disabled', async () => {
const { render } = await import('ink'); const { render } = await import('ink');
const renderSpy = vi.mocked(render); const renderSpy = vi.mocked(render);
const mockInitializationResult = { await startTestInteractiveUI(
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
mockConfig, mockConfig,
mockSettings, mockSettings,
mockStartupWarnings, mockStartupWarnings,
@@ -477,6 +519,7 @@ describe('startInteractiveUI', () => {
expect(options).toEqual({ expect(options).toEqual({
exitOnCtrlC: false, exitOnCtrlC: false,
isScreenReaderEnabled: false, isScreenReaderEnabled: false,
onRender: expect.any(Function),
}); });
// Verify React element structure is valid (but don't deep dive into JSX internals) // 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 { checkForUpdates } = await import('./ui/utils/updateCheck.js');
const { registerCleanup } = await import('./utils/cleanup.js'); const { registerCleanup } = await import('./utils/cleanup.js');
const mockInitializationResult = { await startTestInteractiveUI(
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
mockConfig, mockConfig,
mockSettings, mockSettings,
mockStartupWarnings, mockStartupWarnings,
@@ -517,6 +553,36 @@ describe('startInteractiveUI', () => {
expect(checkForUpdates).toHaveBeenCalledTimes(1); 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([ it.each([
{ {
screenReader: true, screenReader: true,
@@ -537,14 +603,7 @@ describe('startInteractiveUI', () => {
getScreenReader: () => screenReader, getScreenReader: () => screenReader,
} as Config; } as Config;
const mockInitializationResult = { await startTestInteractiveUI(
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
await startInteractiveUI(
mockConfigWithScreenReader, mockConfigWithScreenReader,
mockSettings, mockSettings,
mockStartupWarnings, mockStartupWarnings,
+10 -2
View File
@@ -5,7 +5,7 @@
*/ */
import React from 'react'; import React from 'react';
import { render } from 'ink'; import { render, type RenderOptions } from 'ink';
import { AppContainer } from './ui/AppContainer.js'; import { AppContainer } from './ui/AppContainer.js';
import { loadCliConfig, parseArguments } from './config/config.js'; import { loadCliConfig, parseArguments } from './config/config.js';
import * as cliConfig from './config/config.js'; import * as cliConfig from './config/config.js';
@@ -32,7 +32,7 @@ import {
runExitCleanup, runExitCleanup,
} from './utils/cleanup.js'; } from './utils/cleanup.js';
import { getCliVersion } from './utils/version.js'; import { getCliVersion } from './utils/version.js';
import type { Config } from '@google/gemini-cli-core'; import { type Config } from '@google/gemini-cli-core';
import { import {
sessionId, sessionId,
logUserPrompt, logUserPrompt,
@@ -40,6 +40,7 @@ import {
getOauthClient, getOauthClient,
UserPromptEvent, UserPromptEvent,
debugLogger, debugLogger,
recordSlowRender,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { import {
initializeApp, initializeApp,
@@ -70,6 +71,8 @@ import { ExtensionManager } from './config/extension-manager.js';
import { createPolicyUpdater } from './config/policy.js'; import { createPolicyUpdater } from './config/policy.js';
import { requestConsentNonInteractive } from './config/extensions/consent.js'; import { requestConsentNonInteractive } from './config/extensions/consent.js';
const SLOW_RENDER_MS = 200;
export function validateDnsResolutionOrder( export function validateDnsResolutionOrder(
order: string | undefined, order: string | undefined,
): DnsResolutionOrder { ): DnsResolutionOrder {
@@ -205,7 +208,12 @@ export async function startInteractiveUI(
{ {
exitOnCtrlC: false, exitOnCtrlC: false,
isScreenReaderEnabled: config.getScreenReader(), isScreenReaderEnabled: config.getScreenReader(),
onRender: ({ renderTime }: { renderTime: number }) => {
if (renderTime > SLOW_RENDER_MS) {
recordSlowRender(config, renderTime);
}
}, },
} as RenderOptions,
); );
checkForUpdates(settings) checkForUpdates(settings)