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

@@ -0,0 +1,150 @@
import { expect } from "@playwright/test";
import { Timeout, testWithConfig } from "./helpers/test_helper";
import * as fs from "node:fs";
import * as path from "node:path";
testWithConfig({
preLaunchHook: async ({ userDataDir }) => {
// Set up a force-close scenario by creating settings with isRunning: true
// and lastKnownPerformance data
const settingsPath = path.join(userDataDir, "user-settings.json");
const settings = {
hasRunBefore: true,
isRunning: true, // Simulate force-close
enableAutoUpdate: false,
releaseChannel: "stable",
lastKnownPerformance: {
timestamp: Date.now() - 5000, // 5 seconds ago
memoryUsageMB: 256,
cpuUsagePercent: 45.5,
systemMemoryUsageMB: 8192,
systemMemoryTotalMB: 16384,
systemMemoryPercent: 50.0,
systemCpuPercent: 35.2,
},
};
fs.mkdirSync(userDataDir, { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
},
})(
"force-close detection shows dialog with performance data",
async ({ po }) => {
// Wait for the home page to be visible first
await expect(po.getHomeChatInputContainer()).toBeVisible({
timeout: Timeout.LONG,
});
// Check if the force-close dialog is visible by looking for the heading
await expect(
po.page.getByRole("heading", { name: "Force Close Detected" }),
).toBeVisible({ timeout: Timeout.MEDIUM });
// Verify the warning message
await expect(
po.page.getByText(
"The app was not closed properly the last time it was running",
),
).toBeVisible();
// Verify performance data is displayed
await expect(po.page.getByText("Last Known State:")).toBeVisible();
// Check Process Metrics section
await expect(po.page.getByText("Process Metrics")).toBeVisible();
await expect(po.page.getByText("256 MB")).toBeVisible();
await expect(po.page.getByText("45.5%")).toBeVisible();
// Check System Metrics section
await expect(po.page.getByText("System Metrics")).toBeVisible();
await expect(po.page.getByText("8192 / 16384 MB")).toBeVisible();
await expect(po.page.getByText("35.2%")).toBeVisible();
// Close the dialog
await po.page.getByRole("button", { name: "OK" }).click();
// Verify dialog is closed by checking the heading is no longer visible
await expect(
po.page.getByRole("heading", { name: "Force Close Detected" }),
).not.toBeVisible();
},
);
testWithConfig({
preLaunchHook: async ({ userDataDir }) => {
// Set up scenario without force-close (proper shutdown)
const settingsPath = path.join(userDataDir, "user-settings.json");
const settings = {
hasRunBefore: true,
isRunning: false, // Proper shutdown - no force-close
enableAutoUpdate: false,
releaseChannel: "stable",
lastKnownPerformance: {
timestamp: Date.now() - 5000,
memoryUsageMB: 256,
cpuUsagePercent: 45.5,
systemMemoryUsageMB: 8192,
systemMemoryTotalMB: 16384,
systemMemoryPercent: 50.0,
systemCpuPercent: 35.2,
},
};
fs.mkdirSync(userDataDir, { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
},
})("no force-close dialog when app was properly shut down", async ({ po }) => {
// Verify the home page loaded normally
await expect(po.getHomeChatInputContainer()).toBeVisible({
timeout: Timeout.LONG,
});
// Verify that the force-close dialog is NOT shown
await expect(
po.page.getByRole("heading", { name: "Force Close Detected" }),
).not.toBeVisible();
});
testWithConfig({})(
"performance information is being captured during normal operation",
async ({ po, electronApp }) => {
// Wait for the app to load
await expect(po.getHomeChatInputContainer()).toBeVisible({
timeout: Timeout.LONG,
});
// Get the user data directory
const userDataDir = (electronApp as any).$dyadUserDataDir;
const settingsPath = path.join(userDataDir, "user-settings.json");
// Wait a bit to allow performance monitoring to capture at least one data point
// Performance monitoring runs every 30 seconds, but we'll wait 35 seconds to be safe
await po.page.waitForTimeout(35000);
// Read the settings file
const settingsContent = fs.readFileSync(settingsPath, "utf-8");
const settings = JSON.parse(settingsContent);
// Verify that lastKnownPerformance exists and has all required fields
expect(settings.lastKnownPerformance).toBeDefined();
expect(settings.lastKnownPerformance.timestamp).toBeGreaterThan(0);
expect(settings.lastKnownPerformance.memoryUsageMB).toBeGreaterThan(0);
expect(
settings.lastKnownPerformance.cpuUsagePercent,
).toBeGreaterThanOrEqual(0);
expect(settings.lastKnownPerformance.systemMemoryUsageMB).toBeGreaterThan(
0,
);
expect(settings.lastKnownPerformance.systemMemoryTotalMB).toBeGreaterThan(
0,
);
expect(
settings.lastKnownPerformance.systemCpuPercent,
).toBeGreaterThanOrEqual(0);
// Verify the timestamp is recent (within the last minute)
const now = Date.now();
const timeDiff = now - settings.lastKnownPerformance.timestamp;
expect(timeDiff).toBeLessThan(60000); // Less than 1 minute old
},
);

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": false, "enableAutoUpdate": false,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -18,5 +18,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "beta", "releaseChannel": "beta",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -24,5 +24,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -24,5 +24,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -15,5 +15,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -15,5 +15,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -15,5 +15,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -16,5 +16,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -17,5 +17,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -17,5 +17,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -8,14 +8,6 @@
"role": "system", "role": "system",
"content": "[[SYSTEM_MESSAGE]]" "content": "[[SYSTEM_MESSAGE]]"
}, },
{
"role": "user",
"content": "tc=1"
},
{
"role": "assistant",
"content": "Error: Test case file not found: 1.md"
},
{ {
"role": "user", "role": "user",
"content": "[dump] hi" "content": "[dump] hi"

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -8,22 +8,6 @@
"role": "system", "role": "system",
"content": "[[SYSTEM_MESSAGE]]" "content": "[[SYSTEM_MESSAGE]]"
}, },
{
"role": "user",
"content": "tc=1"
},
{
"role": "assistant",
"content": "Error: Test case file not found: 1.md"
},
{
"role": "user",
"content": "[dump] hi"
},
{
"role": "assistant",
"content": "[[dyad-dump-path=*]]"
},
{ {
"role": "user", "role": "user",
"content": "[dump] hi" "content": "[dump] hi"

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -8,30 +8,6 @@
"role": "system", "role": "system",
"content": "[[SYSTEM_MESSAGE]]" "content": "[[SYSTEM_MESSAGE]]"
}, },
{
"role": "user",
"content": "tc=1"
},
{
"role": "assistant",
"content": "Error: Test case file not found: 1.md"
},
{
"role": "user",
"content": "[dump] hi"
},
{
"role": "assistant",
"content": "[[dyad-dump-path=*]]"
},
{
"role": "user",
"content": "[dump] hi"
},
{
"role": "assistant",
"content": "[[dyad-dump-path=*]]"
},
{ {
"role": "user", "role": "user",
"content": "[dump] hi" "content": "[dump] hi"

View File

@@ -24,5 +24,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -25,5 +25,6 @@
"enableAutoFixProblems": false, "enableAutoFixProblems": false,
"enableAutoUpdate": true, "enableAutoUpdate": true,
"releaseChannel": "stable", "releaseChannel": "stable",
"isRunning": true,
"isTestMode": true "isTestMode": true
} }

View File

@@ -59,6 +59,8 @@ describe("readSettings", () => {
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"experiments": {}, "experiments": {},
"hasRunBefore": false, "hasRunBefore": false,
"isRunning": false,
"lastKnownPerformance": undefined,
"providerSettings": {}, "providerSettings": {},
"releaseChannel": "stable", "releaseChannel": "stable",
"selectedChatMode": "build", "selectedChatMode": "build",
@@ -305,6 +307,8 @@ describe("readSettings", () => {
"enableProSmartFilesContextMode": true, "enableProSmartFilesContextMode": true,
"experiments": {}, "experiments": {},
"hasRunBefore": false, "hasRunBefore": false,
"isRunning": false,
"lastKnownPerformance": undefined,
"providerSettings": {}, "providerSettings": {},
"releaseChannel": "stable", "releaseChannel": "stable",
"selectedChatMode": "build", "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 // Count tokens for a chat and input
public async countTokens( public async countTokens(
params: TokenCountParams, params: TokenCountParams,

View File

@@ -257,6 +257,17 @@ export const UserSettingsSchema = z.object({
releaseChannel: ReleaseChannelSchema, releaseChannel: ReleaseChannelSchema,
runtimeMode2: RuntimeMode2Schema.optional(), runtimeMode2: RuntimeMode2Schema.optional(),
customNodePath: z.string().optional().nullable(), 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. // E2E TESTING ONLY.

View File

@@ -24,6 +24,10 @@ import {
AddPromptDataSchema, AddPromptDataSchema,
AddPromptPayload, AddPromptPayload,
} from "./ipc/deep_link_data"; } from "./ipc/deep_link_data";
import {
startPerformanceMonitoring,
stopPerformanceMonitoring,
} from "./utils/performance_monitor";
import fs from "fs"; import fs from "fs";
log.errorHandler.startCatching(); log.errorHandler.startCatching();
@@ -82,6 +86,24 @@ export async function onReady() {
} }
initializeDatabase(); initializeDatabase();
const settings = readSettings(); 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); await onFirstRunMaybe(settings);
createWindow(); createWindow();
@@ -151,6 +173,7 @@ declare global {
} }
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
let pendingForceCloseData: any = null;
const createWindow = () => { const createWindow = () => {
// Create the browser window. // Create the browser window.
@@ -187,6 +210,16 @@ const createWindow = () => {
mainWindow.webContents.openDevTools(); 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 // Enable native context menu on right-click
mainWindow.webContents.on("context-menu", (event, params) => { mainWindow.webContents.on("context-menu", (event, params) => {
// Prevent any default behavior and show our own menu // 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", () => { app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the // 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. // dock icon is clicked and there are no other windows open.

View File

@@ -34,6 +34,8 @@ const DEFAULT_SETTINGS: UserSettings = {
enableAutoUpdate: true, enableAutoUpdate: true,
releaseChannel: "stable", releaseChannel: "stable",
selectedTemplateId: DEFAULT_TEMPLATE_ID, selectedTemplateId: DEFAULT_TEMPLATE_ID,
isRunning: false,
lastKnownPerformance: undefined,
}; };
const SETTINGS_FILE = "user-settings.json"; const SETTINGS_FILE = "user-settings.json";

View File

@@ -28,6 +28,7 @@ import { ImportAppButton } from "@/components/ImportAppButton";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { invalidateAppQuery } from "@/hooks/useLoadApp"; import { invalidateAppQuery } from "@/hooks/useLoadApp";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { ForceCloseDialog } from "@/components/ForceCloseDialog";
import type { FileAttachment } from "@/ipc/ipc_types"; import type { FileAttachment } from "@/ipc/ipc_types";
import { NEON_TEMPLATE_IDS } from "@/shared/templates"; import { NEON_TEMPLATE_IDS } from "@/shared/templates";
@@ -48,6 +49,8 @@ export default function HomePage() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [forceCloseDialogOpen, setForceCloseDialogOpen] = useState(false);
const [performanceData, setPerformanceData] = useState<any>(undefined);
const { streamMessage } = useStreamChat({ hasChatId: false }); const { streamMessage } = useStreamChat({ hasChatId: false });
const posthog = usePostHog(); const posthog = usePostHog();
const appVersion = useAppVersion(); const appVersion = useAppVersion();
@@ -55,6 +58,17 @@ export default function HomePage() {
const [releaseUrl, setReleaseUrl] = useState(""); const [releaseUrl, setReleaseUrl] = useState("");
const { theme } = useTheme(); const { theme } = useTheme();
const queryClient = useQueryClient(); 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(() => { useEffect(() => {
const updateLastVersionLaunched = async () => { const updateLastVersionLaunched = async () => {
if ( if (
@@ -189,6 +203,11 @@ export default function HomePage() {
// Main Home Page Content // Main Home Page Content
return ( return (
<div className="flex flex-col items-center justify-center max-w-3xl w-full m-auto p-8"> <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 /> <SetupBanner />
<div className="w-full"> <div className="w-full">

View File

@@ -154,6 +154,7 @@ const validReceiveChannels = [
"github:flow-success", "github:flow-success",
"github:flow-error", "github:flow-error",
"deep-link-received", "deep-link-received",
"force-close-detected",
// Help bot // Help bot
"help:chat:response:chunk", "help:chat:response:chunk",
"help:chat:response:end", "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();
}
}