mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -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';
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user