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:
Mohamed Aziz Mejri
2025-12-14 21:07:56 +01:00
committed by GitHub
parent a4ab1a7f84
commit 9d33f3757d
38 changed files with 606 additions and 48 deletions

View File

@@ -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",

View 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>
);
}

View File

@@ -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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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";

View File

@@ -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">

View File

@@ -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",

View 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();
}
}