mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(infra) - Add logging for slow rendering (#11147)
Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user