Emit a warning when memory usage exceeds 7GB (#7613)

This commit is contained in:
Jacob MacDonald
2025-09-17 14:32:46 -07:00
committed by GitHub
parent 0cae7caaab
commit 13a65ad94f
7 changed files with 156 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ import { validateAuthMethod } from '../config/auth.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
import process from 'node:process';
import { useHistory } from './hooks/useHistoryManager.js';
import { useMemoryMonitor } from './hooks/useMemoryMonitor.js';
import { useThemeCommand } from './hooks/useThemeCommand.js';
import { useAuthCommand } from './auth/useAuth.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
@@ -123,6 +124,7 @@ const SHELL_HEIGHT_PADDING = 10;
export const AppContainer = (props: AppContainerProps) => {
const { settings, config, initializationResult } = props;
const historyManager = useHistory();
useMemoryMonitor(historyManager);
const [corgiMode, setCorgiMode] = useState(false);
const [debugMessage, setDebugMessage] = useState<string>('');
const [quittingMessages, setQuittingMessages] = useState<

View File

@@ -14,6 +14,7 @@ import { ErrorMessage } from './messages/ErrorMessage.js';
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { CompressionMessage } from './messages/CompressionMessage.js';
import { WarningMessage } from './messages/WarningMessage.js';
import { Box } from 'ink';
import { AboutBox } from './AboutBox.js';
import { StatsDisplay } from './StatsDisplay.js';
@@ -66,6 +67,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
/>
)}
{item.type === 'info' && <InfoMessage text={item.text} />}
{item.type === 'warning' && <WarningMessage text={item.text} />}
{item.type === 'error' && <ErrorMessage text={item.text} />}
{item.type === 'about' && (
<AboutBox

View File

@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../../colors.js';
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
interface WarningMessageProps {
text: string;
}
export const WarningMessage: React.FC<WarningMessageProps> = ({ text }) => {
const prefix = '⚠ ';
const prefixWidth = 3;
return (
<Box flexDirection="row" marginTop={1}>
<Box width={prefixWidth}>
<Text color={Colors.AccentYellow}>{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color={Colors.AccentYellow}>
<RenderInline text={text} />
</Text>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderHook } from '@testing-library/react';
import { vi } from 'vitest';
import {
useMemoryMonitor,
MEMORY_CHECK_INTERVAL,
MEMORY_WARNING_THRESHOLD,
} from './useMemoryMonitor.js';
import process from 'node:process';
import { MessageType } from '../types.js';
describe('useMemoryMonitor', () => {
const memoryUsageSpy = vi.spyOn(process, 'memoryUsage');
const addItem = vi.fn();
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
it('should not warn when memory usage is below threshold', () => {
memoryUsageSpy.mockReturnValue({
rss: MEMORY_WARNING_THRESHOLD / 2,
} as NodeJS.MemoryUsage);
renderHook(() => useMemoryMonitor({ addItem }));
vi.advanceTimersByTime(10000);
expect(addItem).not.toHaveBeenCalled();
});
it('should warn when memory usage is above threshold', () => {
memoryUsageSpy.mockReturnValue({
rss: MEMORY_WARNING_THRESHOLD * 1.5,
} as NodeJS.MemoryUsage);
renderHook(() => useMemoryMonitor({ addItem }));
vi.advanceTimersByTime(MEMORY_CHECK_INTERVAL);
expect(addItem).toHaveBeenCalledTimes(1);
expect(addItem).toHaveBeenCalledWith(
{
type: MessageType.WARNING,
text: 'High memory usage detected: 10.50 GB. If you experience a crash, please file a bug report by running `/bug`',
},
expect.any(Number),
);
});
it('should only warn once', () => {
memoryUsageSpy.mockReturnValue({
rss: MEMORY_WARNING_THRESHOLD * 1.5,
} as NodeJS.MemoryUsage);
const { rerender } = renderHook(() => useMemoryMonitor({ addItem }));
vi.advanceTimersByTime(MEMORY_CHECK_INTERVAL);
expect(addItem).toHaveBeenCalledTimes(1);
// Rerender and advance timers, should not warn again
memoryUsageSpy.mockReturnValue({
rss: MEMORY_WARNING_THRESHOLD * 1.5,
} as NodeJS.MemoryUsage);
rerender();
vi.advanceTimersByTime(MEMORY_CHECK_INTERVAL);
expect(addItem).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useEffect } from 'react';
import process from 'node:process';
import { type HistoryItemWithoutId, MessageType } from '../types.js';
export const MEMORY_WARNING_THRESHOLD = 7 * 1024 * 1024 * 1024; // 7GB in bytes
export const MEMORY_CHECK_INTERVAL = 60 * 1000; // one minute
interface MemoryMonitorOptions {
addItem: (item: HistoryItemWithoutId, timestamp: number) => void;
}
export const useMemoryMonitor = ({ addItem }: MemoryMonitorOptions) => {
useEffect(() => {
const intervalId = setInterval(() => {
const usage = process.memoryUsage().rss;
if (usage > MEMORY_WARNING_THRESHOLD) {
addItem(
{
type: MessageType.WARNING,
text:
`High memory usage detected: ${(
usage /
(1024 * 1024 * 1024)
).toFixed(2)} GB. ` +
'If you experience a crash, please file a bug report by running `/bug`',
},
Date.now(),
);
clearInterval(intervalId);
}
}, MEMORY_CHECK_INTERVAL);
return () => clearInterval(intervalId);
}, [addItem]);
};

View File

@@ -106,6 +106,11 @@ export type HistoryItemError = HistoryItemBase & {
text: string;
};
export type HistoryItemWarning = HistoryItemBase & {
type: 'warning';
text: string;
};
export type HistoryItemAbout = HistoryItemBase & {
type: 'about';
cliVersion: string;
@@ -170,6 +175,7 @@ export type HistoryItemWithoutId =
| HistoryItemGeminiContent
| HistoryItemInfo
| HistoryItemError
| HistoryItemWarning
| HistoryItemAbout
| HistoryItemHelp
| HistoryItemToolGroup
@@ -186,6 +192,7 @@ export type HistoryItem = HistoryItemWithoutId & { id: number };
export enum MessageType {
INFO = 'info',
ERROR = 'error',
WARNING = 'warning',
USER = 'user',
ABOUT = 'about',
HELP = 'help',

View File

@@ -66,6 +66,7 @@
"src/ui/components/shared/vim-buffer-actions.test.ts",
"src/ui/components/StatsDisplay.test.tsx",
"src/ui/components/ToolStatsDisplay.test.tsx",
"src/ui/components/WarningMessage.test.tsx",
"src/ui/contexts/SessionContext.test.tsx",
"src/ui/hooks/slashCommandProcessor.test.ts",
"src/ui/hooks/useAtCompletion.test.ts",