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
150
e2e-tests/performance_monitor.spec.ts
Normal file
150
e2e-tests/performance_monitor.spec.ts
Normal 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
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": false,
|
"enableAutoUpdate": false,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -18,5 +18,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "beta",
|
"releaseChannel": "beta",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -17,5 +17,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -17,5 +17,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -24,5 +24,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,6 @@
|
|||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
"releaseChannel": "stable",
|
"releaseChannel": "stable",
|
||||||
|
"isRunning": true,
|
||||||
"isTestMode": true
|
"isTestMode": true
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
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
|
// Count tokens for a chat and input
|
||||||
public async countTokens(
|
public async countTokens(
|
||||||
params: TokenCountParams,
|
params: TokenCountParams,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
43
src/main.ts
43
src/main.ts
@@ -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.
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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