mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-02 17:31:05 -07:00
refactor(ui): Optimize rendering performance (#8239)
This commit is contained in:
11
packages/cli/src/ui/contexts/FocusContext.tsx
Normal file
11
packages/cli/src/ui/contexts/FocusContext.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const FocusContext = createContext<boolean>(true);
|
||||
|
||||
export const useFocusState = () => useContext(FocusContext);
|
||||
@@ -113,6 +113,88 @@ describe('SessionStatsContext', () => {
|
||||
expect(stats?.lastPromptTokenCount).toBe(100);
|
||||
});
|
||||
|
||||
it('should not update metrics if the data is the same', () => {
|
||||
const contextRef: MutableRefObject<
|
||||
ReturnType<typeof useSessionStats> | undefined
|
||||
> = { current: undefined };
|
||||
|
||||
let renderCount = 0;
|
||||
const CountingTestHarness = () => {
|
||||
contextRef.current = useSessionStats();
|
||||
renderCount++;
|
||||
return null;
|
||||
};
|
||||
|
||||
render(
|
||||
<SessionStatsProvider>
|
||||
<CountingTestHarness />
|
||||
</SessionStatsProvider>,
|
||||
);
|
||||
|
||||
expect(renderCount).toBe(1);
|
||||
|
||||
const metrics: SessionMetrics = {
|
||||
models: {
|
||||
'gemini-pro': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
tokens: {
|
||||
prompt: 10,
|
||||
candidates: 20,
|
||||
total: 30,
|
||||
cached: 0,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: { accept: 0, reject: 0, modify: 0 },
|
||||
byName: {},
|
||||
},
|
||||
};
|
||||
|
||||
act(() => {
|
||||
uiTelemetryService.emit('update', { metrics, lastPromptTokenCount: 10 });
|
||||
});
|
||||
|
||||
expect(renderCount).toBe(2);
|
||||
|
||||
act(() => {
|
||||
uiTelemetryService.emit('update', { metrics, lastPromptTokenCount: 10 });
|
||||
});
|
||||
|
||||
expect(renderCount).toBe(2);
|
||||
|
||||
const newMetrics = {
|
||||
...metrics,
|
||||
models: {
|
||||
'gemini-pro': {
|
||||
api: { totalRequests: 2, totalErrors: 0, totalLatencyMs: 200 },
|
||||
tokens: {
|
||||
prompt: 20,
|
||||
candidates: 40,
|
||||
total: 60,
|
||||
cached: 0,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
act(() => {
|
||||
uiTelemetryService.emit('update', {
|
||||
metrics: newMetrics,
|
||||
lastPromptTokenCount: 20,
|
||||
});
|
||||
});
|
||||
|
||||
expect(renderCount).toBe(3);
|
||||
});
|
||||
|
||||
it('should throw an error when useSessionStats is used outside of a provider', () => {
|
||||
// Suppress console.error for this test since we expect an error
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
@@ -14,10 +14,129 @@ import {
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import type { SessionMetrics, ModelMetrics } from '@google/gemini-cli-core';
|
||||
import type {
|
||||
SessionMetrics,
|
||||
ModelMetrics,
|
||||
ToolCallStats,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { uiTelemetryService, sessionId } from '@google/gemini-cli-core';
|
||||
|
||||
// --- Interface Definitions ---
|
||||
export enum ToolCallDecision {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
MODIFY = 'modify',
|
||||
AUTO_ACCEPT = 'auto_accept',
|
||||
}
|
||||
|
||||
function areModelMetricsEqual(a: ModelMetrics, b: ModelMetrics): boolean {
|
||||
if (
|
||||
a.api.totalRequests !== b.api.totalRequests ||
|
||||
a.api.totalErrors !== b.api.totalErrors ||
|
||||
a.api.totalLatencyMs !== b.api.totalLatencyMs
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
a.tokens.prompt !== b.tokens.prompt ||
|
||||
a.tokens.candidates !== b.tokens.candidates ||
|
||||
a.tokens.total !== b.tokens.total ||
|
||||
a.tokens.cached !== b.tokens.cached ||
|
||||
a.tokens.thoughts !== b.tokens.thoughts ||
|
||||
a.tokens.tool !== b.tokens.tool
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function areToolCallStatsEqual(a: ToolCallStats, b: ToolCallStats): boolean {
|
||||
if (
|
||||
a.count !== b.count ||
|
||||
a.success !== b.success ||
|
||||
a.fail !== b.fail ||
|
||||
a.durationMs !== b.durationMs
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
a.decisions[ToolCallDecision.ACCEPT] !==
|
||||
b.decisions[ToolCallDecision.ACCEPT] ||
|
||||
a.decisions[ToolCallDecision.REJECT] !==
|
||||
b.decisions[ToolCallDecision.REJECT] ||
|
||||
a.decisions[ToolCallDecision.MODIFY] !==
|
||||
b.decisions[ToolCallDecision.MODIFY] ||
|
||||
a.decisions[ToolCallDecision.AUTO_ACCEPT] !==
|
||||
b.decisions[ToolCallDecision.AUTO_ACCEPT]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function areMetricsEqual(a: SessionMetrics, b: SessionMetrics): boolean {
|
||||
if (a === b) return true;
|
||||
if (!a || !b) return false;
|
||||
|
||||
// Compare files
|
||||
if (
|
||||
a.files.totalLinesAdded !== b.files.totalLinesAdded ||
|
||||
a.files.totalLinesRemoved !== b.files.totalLinesRemoved
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare tools
|
||||
const toolsA = a.tools;
|
||||
const toolsB = b.tools;
|
||||
if (
|
||||
toolsA.totalCalls !== toolsB.totalCalls ||
|
||||
toolsA.totalSuccess !== toolsB.totalSuccess ||
|
||||
toolsA.totalFail !== toolsB.totalFail ||
|
||||
toolsA.totalDurationMs !== toolsB.totalDurationMs
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare tool decisions
|
||||
if (
|
||||
toolsA.totalDecisions[ToolCallDecision.ACCEPT] !==
|
||||
toolsB.totalDecisions[ToolCallDecision.ACCEPT] ||
|
||||
toolsA.totalDecisions[ToolCallDecision.REJECT] !==
|
||||
toolsB.totalDecisions[ToolCallDecision.REJECT] ||
|
||||
toolsA.totalDecisions[ToolCallDecision.MODIFY] !==
|
||||
toolsB.totalDecisions[ToolCallDecision.MODIFY] ||
|
||||
toolsA.totalDecisions[ToolCallDecision.AUTO_ACCEPT] !==
|
||||
toolsB.totalDecisions[ToolCallDecision.AUTO_ACCEPT]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare tools.byName
|
||||
const toolsByNameAKeys = Object.keys(toolsA.byName);
|
||||
const toolsByNameBKeys = Object.keys(toolsB.byName);
|
||||
if (toolsByNameAKeys.length !== toolsByNameBKeys.length) return false;
|
||||
|
||||
for (const key of toolsByNameAKeys) {
|
||||
const toolA = toolsA.byName[key];
|
||||
const toolB = toolsB.byName[key];
|
||||
if (!toolB || !areToolCallStatsEqual(toolA, toolB)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare models
|
||||
const modelsAKeys = Object.keys(a.models);
|
||||
const modelsBKeys = Object.keys(b.models);
|
||||
if (modelsAKeys.length !== modelsBKeys.length) return false;
|
||||
|
||||
for (const key of modelsAKeys) {
|
||||
if (!b.models[key] || !areModelMetricsEqual(a.models[key], b.models[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type { SessionMetrics, ModelMetrics };
|
||||
|
||||
@@ -80,11 +199,19 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
metrics: SessionMetrics;
|
||||
lastPromptTokenCount: number;
|
||||
}) => {
|
||||
setStats((prevState) => ({
|
||||
...prevState,
|
||||
metrics,
|
||||
lastPromptTokenCount,
|
||||
}));
|
||||
setStats((prevState) => {
|
||||
if (
|
||||
prevState.lastPromptTokenCount === lastPromptTokenCount &&
|
||||
areMetricsEqual(prevState.metrics, metrics)
|
||||
) {
|
||||
return prevState;
|
||||
}
|
||||
return {
|
||||
...prevState,
|
||||
metrics,
|
||||
lastPromptTokenCount,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
uiTelemetryService.on('update', handleUpdate);
|
||||
|
||||
@@ -78,7 +78,6 @@ export interface UIState {
|
||||
ctrlCPressedOnce: boolean;
|
||||
ctrlDPressedOnce: boolean;
|
||||
showEscapePrompt: boolean;
|
||||
isFocused: boolean;
|
||||
elapsedTime: number;
|
||||
currentLoadingPhrase: string;
|
||||
historyRemountKey: number;
|
||||
|
||||
Reference in New Issue
Block a user