logging and presenting cpu/memory usage when app is force-closed (#1894)
closes #1803 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Detects when the app was force-closed and shows a dialog with the last known CPU and memory usage. Adds background performance monitoring so we can surface metrics on next launch. - **New Features** - Start a performance monitor at app launch; captures process and system memory/CPU every 30s and on quit. - Persist metrics in settings.lastKnownPerformance and track settings.isRunning to detect improper shutdowns. - On startup, if the previous run was force-closed, send a "force-close-detected" IPC event after the window loads. - Add ForceCloseDialog to display timestamped process/system metrics. - Whitelist the new IPC channel in preload and listen for it on the home page. <sup>Written for commit 0543cdc234da7f94024e8506749aaa9ca36ef916. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
committed by
GitHub
parent
a4ab1a7f84
commit
9d33f3757d
@@ -59,6 +59,8 @@ describe("readSettings", () => {
|
||||
"enableProSmartFilesContextMode": true,
|
||||
"experiments": {},
|
||||
"hasRunBefore": false,
|
||||
"isRunning": false,
|
||||
"lastKnownPerformance": undefined,
|
||||
"providerSettings": {},
|
||||
"releaseChannel": "stable",
|
||||
"selectedChatMode": "build",
|
||||
@@ -305,6 +307,8 @@ describe("readSettings", () => {
|
||||
"enableProSmartFilesContextMode": true,
|
||||
"experiments": {},
|
||||
"hasRunBefore": false,
|
||||
"isRunning": false,
|
||||
"lastKnownPerformance": undefined,
|
||||
"providerSettings": {},
|
||||
"releaseChannel": "stable",
|
||||
"selectedChatMode": "build",
|
||||
|
||||
128
src/components/ForceCloseDialog.tsx
Normal file
128
src/components/ForceCloseDialog.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
interface ForceCloseDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
performanceData?: {
|
||||
timestamp: number;
|
||||
memoryUsageMB: number;
|
||||
cpuUsagePercent?: number;
|
||||
systemMemoryUsageMB?: number;
|
||||
systemMemoryTotalMB?: number;
|
||||
systemCpuPercent?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function ForceCloseDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
performanceData,
|
||||
}: ForceCloseDialogProps) {
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<AlertDialogContent className="max-w-2xl">
|
||||
<AlertDialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
<AlertDialogTitle>Force Close Detected</AlertDialogTitle>
|
||||
</div>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="text-base">
|
||||
The app was not closed properly the last time it was running.
|
||||
This could indicate a crash or unexpected termination.
|
||||
</div>
|
||||
|
||||
{performanceData && (
|
||||
<div className="rounded-lg border bg-muted/50 p-4 space-y-3">
|
||||
<div className="font-semibold text-sm text-foreground">
|
||||
Last Known State:{" "}
|
||||
<span className="font-normal text-muted-foreground">
|
||||
{formatTimestamp(performanceData.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
{/* Process Metrics */}
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-foreground">
|
||||
Process Metrics
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Memory:</span>
|
||||
<span className="font-mono">
|
||||
{performanceData.memoryUsageMB} MB
|
||||
</span>
|
||||
</div>
|
||||
{performanceData.cpuUsagePercent !== undefined && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">CPU:</span>
|
||||
<span className="font-mono">
|
||||
{performanceData.cpuUsagePercent}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Metrics */}
|
||||
{(performanceData.systemMemoryUsageMB !== undefined ||
|
||||
performanceData.systemCpuPercent !== undefined) && (
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-foreground">
|
||||
System Metrics
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{performanceData.systemMemoryUsageMB !== undefined &&
|
||||
performanceData.systemMemoryTotalMB !==
|
||||
undefined && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Memory:
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
{performanceData.systemMemoryUsageMB} /{" "}
|
||||
{performanceData.systemMemoryTotalMB} MB
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{performanceData.systemCpuPercent !== undefined && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
CPU:
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
{performanceData.systemCpuPercent}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={onClose}>OK</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -1064,6 +1064,28 @@ export class IpcClient {
|
||||
};
|
||||
}
|
||||
|
||||
// Listen for force close detected events
|
||||
public onForceCloseDetected(
|
||||
callback: (data: {
|
||||
performanceData?: {
|
||||
timestamp: number;
|
||||
memoryUsageMB: number;
|
||||
cpuUsagePercent?: number;
|
||||
systemMemoryUsageMB?: number;
|
||||
systemMemoryTotalMB?: number;
|
||||
systemCpuPercent?: number;
|
||||
};
|
||||
}) => void,
|
||||
): () => void {
|
||||
const listener = (data: any) => {
|
||||
callback(data);
|
||||
};
|
||||
this.ipcRenderer.on("force-close-detected", listener);
|
||||
return () => {
|
||||
this.ipcRenderer.removeListener("force-close-detected", listener);
|
||||
};
|
||||
}
|
||||
|
||||
// Count tokens for a chat and input
|
||||
public async countTokens(
|
||||
params: TokenCountParams,
|
||||
|
||||
@@ -257,6 +257,17 @@ export const UserSettingsSchema = z.object({
|
||||
releaseChannel: ReleaseChannelSchema,
|
||||
runtimeMode2: RuntimeMode2Schema.optional(),
|
||||
customNodePath: z.string().optional().nullable(),
|
||||
isRunning: z.boolean().optional(),
|
||||
lastKnownPerformance: z
|
||||
.object({
|
||||
timestamp: z.number(),
|
||||
memoryUsageMB: z.number(),
|
||||
cpuUsagePercent: z.number().optional(),
|
||||
systemMemoryUsageMB: z.number().optional(),
|
||||
systemMemoryTotalMB: z.number().optional(),
|
||||
systemCpuPercent: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
////////////////////////////////
|
||||
// E2E TESTING ONLY.
|
||||
|
||||
43
src/main.ts
43
src/main.ts
@@ -24,6 +24,10 @@ import {
|
||||
AddPromptDataSchema,
|
||||
AddPromptPayload,
|
||||
} from "./ipc/deep_link_data";
|
||||
import {
|
||||
startPerformanceMonitoring,
|
||||
stopPerformanceMonitoring,
|
||||
} from "./utils/performance_monitor";
|
||||
import fs from "fs";
|
||||
|
||||
log.errorHandler.startCatching();
|
||||
@@ -82,6 +86,24 @@ export async function onReady() {
|
||||
}
|
||||
initializeDatabase();
|
||||
const settings = readSettings();
|
||||
|
||||
// Check if app was force-closed
|
||||
if (settings.isRunning) {
|
||||
logger.warn("App was force-closed on previous run");
|
||||
|
||||
// Store performance data to send after window is created
|
||||
if (settings.lastKnownPerformance) {
|
||||
logger.warn("Last known performance:", settings.lastKnownPerformance);
|
||||
pendingForceCloseData = settings.lastKnownPerformance;
|
||||
}
|
||||
}
|
||||
|
||||
// Set isRunning to true at startup
|
||||
writeSettings({ isRunning: true });
|
||||
|
||||
// Start performance monitoring
|
||||
startPerformanceMonitoring();
|
||||
|
||||
await onFirstRunMaybe(settings);
|
||||
createWindow();
|
||||
|
||||
@@ -151,6 +173,7 @@ declare global {
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let pendingForceCloseData: any = null;
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
@@ -187,6 +210,16 @@ const createWindow = () => {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// Send force-close event if it was detected
|
||||
if (pendingForceCloseData) {
|
||||
mainWindow.webContents.once("did-finish-load", () => {
|
||||
mainWindow?.webContents.send("force-close-detected", {
|
||||
performanceData: pendingForceCloseData,
|
||||
});
|
||||
pendingForceCloseData = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Enable native context menu on right-click
|
||||
mainWindow.webContents.on("context-menu", (event, params) => {
|
||||
// Prevent any default behavior and show our own menu
|
||||
@@ -414,6 +447,16 @@ app.on("window-all-closed", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Only set isRunning to false when the app is properly quit by the user
|
||||
app.on("will-quit", () => {
|
||||
logger.info("App is quitting, setting isRunning to false");
|
||||
|
||||
// Stop performance monitoring and capture final metrics
|
||||
stopPerformanceMonitoring();
|
||||
|
||||
writeSettings({ isRunning: false });
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
||||
@@ -34,6 +34,8 @@ const DEFAULT_SETTINGS: UserSettings = {
|
||||
enableAutoUpdate: true,
|
||||
releaseChannel: "stable",
|
||||
selectedTemplateId: DEFAULT_TEMPLATE_ID,
|
||||
isRunning: false,
|
||||
lastKnownPerformance: undefined,
|
||||
};
|
||||
|
||||
const SETTINGS_FILE = "user-settings.json";
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ImportAppButton } from "@/components/ImportAppButton";
|
||||
import { showError } from "@/lib/toast";
|
||||
import { invalidateAppQuery } from "@/hooks/useLoadApp";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ForceCloseDialog } from "@/components/ForceCloseDialog";
|
||||
|
||||
import type { FileAttachment } from "@/ipc/ipc_types";
|
||||
import { NEON_TEMPLATE_IDS } from "@/shared/templates";
|
||||
@@ -48,6 +49,8 @@ export default function HomePage() {
|
||||
const { settings, updateSettings } = useSettings();
|
||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [forceCloseDialogOpen, setForceCloseDialogOpen] = useState(false);
|
||||
const [performanceData, setPerformanceData] = useState<any>(undefined);
|
||||
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||
const posthog = usePostHog();
|
||||
const appVersion = useAppVersion();
|
||||
@@ -55,6 +58,17 @@ export default function HomePage() {
|
||||
const [releaseUrl, setReleaseUrl] = useState("");
|
||||
const { theme } = useTheme();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Listen for force-close events
|
||||
useEffect(() => {
|
||||
const ipc = IpcClient.getInstance();
|
||||
const unsubscribe = ipc.onForceCloseDetected((data) => {
|
||||
setPerformanceData(data.performanceData);
|
||||
setForceCloseDialogOpen(true);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateLastVersionLaunched = async () => {
|
||||
if (
|
||||
@@ -189,6 +203,11 @@ export default function HomePage() {
|
||||
// Main Home Page Content
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center max-w-3xl w-full m-auto p-8">
|
||||
<ForceCloseDialog
|
||||
isOpen={forceCloseDialogOpen}
|
||||
onClose={() => setForceCloseDialogOpen(false)}
|
||||
performanceData={performanceData}
|
||||
/>
|
||||
<SetupBanner />
|
||||
|
||||
<div className="w-full">
|
||||
|
||||
@@ -154,6 +154,7 @@ const validReceiveChannels = [
|
||||
"github:flow-success",
|
||||
"github:flow-error",
|
||||
"deep-link-received",
|
||||
"force-close-detected",
|
||||
// Help bot
|
||||
"help:chat:response:chunk",
|
||||
"help:chat:response:end",
|
||||
|
||||
201
src/utils/performance_monitor.ts
Normal file
201
src/utils/performance_monitor.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import log from "electron-log";
|
||||
import { writeSettings } from "../main/settings";
|
||||
import os from "node:os";
|
||||
|
||||
const logger = log.scope("performance-monitor");
|
||||
|
||||
// Constants
|
||||
const MONITOR_INTERVAL_MS = 30000; // 30 seconds
|
||||
const BYTES_PER_MB = 1024 * 1024;
|
||||
|
||||
let monitorInterval: NodeJS.Timeout | null = null;
|
||||
let lastCpuUsage: NodeJS.CpuUsage | null = null;
|
||||
let lastTimestamp: number | null = null;
|
||||
let lastSystemCpuInfo: os.CpuInfo[] | null = null;
|
||||
let lastSystemTimestamp: number | null = null;
|
||||
|
||||
/**
|
||||
* Get current memory usage in MB
|
||||
*/
|
||||
function getMemoryUsageMB(): number {
|
||||
const memoryUsage = process.memoryUsage();
|
||||
// Use RSS (Resident Set Size) for total memory used by the process
|
||||
return Math.round(memoryUsage.rss / BYTES_PER_MB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CPU usage percentage
|
||||
* This measures CPU time used by this process relative to wall clock time
|
||||
*/
|
||||
function getCpuUsagePercent(): number | null {
|
||||
const currentCpuUsage = process.cpuUsage();
|
||||
const currentTimestamp = Date.now();
|
||||
|
||||
// On first call, just initialize and return null
|
||||
if (lastCpuUsage === null || lastTimestamp === null) {
|
||||
lastCpuUsage = currentCpuUsage;
|
||||
lastTimestamp = currentTimestamp;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate elapsed wall clock time in microseconds
|
||||
const elapsedTimeMs = currentTimestamp - lastTimestamp;
|
||||
const elapsedTimeMicros = elapsedTimeMs * 1000;
|
||||
|
||||
// Calculate CPU time used (user + system) in microseconds
|
||||
const cpuTimeMicros =
|
||||
currentCpuUsage.user -
|
||||
lastCpuUsage.user +
|
||||
(currentCpuUsage.system - lastCpuUsage.system);
|
||||
|
||||
// CPU percentage = (CPU time / wall clock time) * 100
|
||||
// This gives percentage across all cores (can exceed 100% on multi-core systems)
|
||||
const cpuPercent = (cpuTimeMicros / elapsedTimeMicros) * 100;
|
||||
|
||||
// Update for next calculation
|
||||
lastCpuUsage = currentCpuUsage;
|
||||
lastTimestamp = currentTimestamp;
|
||||
|
||||
return Math.round(cpuPercent * 100) / 100; // Round to 2 decimal places
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system memory usage
|
||||
*/
|
||||
function getSystemMemoryUsage(): {
|
||||
totalMemoryMB: number;
|
||||
usedMemoryMB: number;
|
||||
freeMemoryMB: number;
|
||||
usagePercent: number;
|
||||
} {
|
||||
const totalMemory = os.totalmem();
|
||||
const freeMemory = os.freemem();
|
||||
const usedMemory = totalMemory - freeMemory;
|
||||
|
||||
return {
|
||||
totalMemoryMB: Math.round(totalMemory / BYTES_PER_MB),
|
||||
usedMemoryMB: Math.round(usedMemory / BYTES_PER_MB),
|
||||
freeMemoryMB: Math.round(freeMemory / BYTES_PER_MB),
|
||||
usagePercent: Math.round((usedMemory / totalMemory) * 100 * 100) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system CPU usage percentage
|
||||
*/
|
||||
function getSystemCpuUsagePercent(): number | null {
|
||||
const cpus = os.cpus();
|
||||
const currentTimestamp = Date.now();
|
||||
|
||||
// On first call, just initialize and return null
|
||||
if (lastSystemCpuInfo === null || lastSystemTimestamp === null) {
|
||||
lastSystemCpuInfo = cpus;
|
||||
lastSystemTimestamp = currentTimestamp;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate total CPU time for all cores
|
||||
let totalIdle = 0;
|
||||
let totalTick = 0;
|
||||
let lastTotalIdle = 0;
|
||||
let lastTotalTick = 0;
|
||||
|
||||
// Current CPU times
|
||||
for (const cpu of cpus) {
|
||||
for (const type in cpu.times) {
|
||||
totalTick += cpu.times[type as keyof typeof cpu.times];
|
||||
}
|
||||
totalIdle += cpu.times.idle;
|
||||
}
|
||||
|
||||
// Last CPU times
|
||||
for (const cpu of lastSystemCpuInfo) {
|
||||
for (const type in cpu.times) {
|
||||
lastTotalTick += cpu.times[type as keyof typeof cpu.times];
|
||||
}
|
||||
lastTotalIdle += cpu.times.idle;
|
||||
}
|
||||
|
||||
// Calculate differences
|
||||
const totalTickDiff = totalTick - lastTotalTick;
|
||||
const idleDiff = totalIdle - lastTotalIdle;
|
||||
|
||||
// Calculate usage percentage
|
||||
const usage = 100 - (100 * idleDiff) / totalTickDiff;
|
||||
|
||||
// Update for next calculation
|
||||
lastSystemCpuInfo = cpus;
|
||||
lastSystemTimestamp = currentTimestamp;
|
||||
|
||||
return Math.round(usage * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture and save current performance metrics
|
||||
*/
|
||||
function capturePerformanceMetrics() {
|
||||
try {
|
||||
const memoryUsageMB = getMemoryUsageMB();
|
||||
const cpuUsagePercent = getCpuUsagePercent();
|
||||
const systemMemory = getSystemMemoryUsage();
|
||||
const systemCpuPercent = getSystemCpuUsagePercent();
|
||||
|
||||
// Skip saving if CPU is null (first call for either metric)
|
||||
if (cpuUsagePercent === null || systemCpuPercent === null) {
|
||||
logger.debug(
|
||||
`Performance: Memory=${memoryUsageMB}MB, CPU=initializing, System Memory=${systemMemory.usagePercent}%, System CPU=initializing`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Performance: Memory=${memoryUsageMB}MB, CPU=${cpuUsagePercent}%, System Memory=${systemMemory.usedMemoryMB}/${systemMemory.totalMemoryMB}MB (${systemMemory.usagePercent}%), System CPU=${systemCpuPercent}%`,
|
||||
);
|
||||
|
||||
writeSettings({
|
||||
lastKnownPerformance: {
|
||||
timestamp: Date.now(),
|
||||
memoryUsageMB,
|
||||
cpuUsagePercent,
|
||||
systemMemoryUsageMB: systemMemory.usedMemoryMB,
|
||||
systemMemoryTotalMB: systemMemory.totalMemoryMB,
|
||||
systemCpuPercent,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error capturing performance metrics:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring performance metrics
|
||||
* Captures metrics every 30 seconds
|
||||
*/
|
||||
export function startPerformanceMonitoring() {
|
||||
if (monitorInterval) {
|
||||
logger.warn("Performance monitoring already started");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Starting performance monitoring");
|
||||
|
||||
// Capture initial metrics
|
||||
capturePerformanceMetrics();
|
||||
|
||||
// Capture every 30 seconds
|
||||
monitorInterval = setInterval(capturePerformanceMetrics, MONITOR_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring performance metrics
|
||||
*/
|
||||
export function stopPerformanceMonitoring() {
|
||||
if (monitorInterval) {
|
||||
logger.info("Stopping performance monitoring");
|
||||
clearInterval(monitorInterval);
|
||||
monitorInterval = null;
|
||||
|
||||
// Capture final metrics before stopping
|
||||
capturePerformanceMetrics();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user