Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ const config: ForgeConfig = {
js: path.join(root, './src/isolated-run-button.ts'),
name: 'isolated_run_button',
},
{
html: path.join(root, './static/process-monitor.html'),
js: path.join(root, './src/renderer/process-monitor-entry.tsx'),
name: 'process_monitor_window',
preload: {
js: path.join(root, 'src/preload/preload.ts'),
},
},
],
},
},
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"node-watch": "^0.7.3",
"p-debounce": "^2.0.0",
"parse-env-string": "^1.0.1",
"pidtree": "^1.0.0",
"pidusage": "^4.0.1",
"prettier": "^3.6.2",
"react": "^16.14.0",
"react-dom": "^16.14.0",
Expand Down Expand Up @@ -94,6 +96,7 @@
"@types/fs-extra": "^9.0.7",
"@types/node": "^22.19.1",
"@types/parse-env-string": "^1.0.2",
"@types/pidusage": "^2.0.5",
"@types/react": "^16.14.0",
"@types/react-dom": "^16.9.11",
"@types/react-window": "^1.8.8",
Expand Down
12 changes: 11 additions & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ export type FiddleEvent =
| 'toggle-monaco-option'
| 'undo-in-editor'
| 'version-download-progress'
| 'version-state-changed';
| 'version-state-changed'
| 'process-metrics-update';

export interface MessageOptions {
message: string;
Expand Down Expand Up @@ -284,6 +285,7 @@ export enum GlobalSetting {
localVersion = 'local-electron-versions',
packageAuthor = 'packageAuthor',
isShowingGistHistory = 'isShowingGistHistory',
isProcessMonitorEnabled = 'isProcessMonitorEnabled',
packageManager = 'packageManager',
showObsoleteVersions = 'showObsoleteVersions',
showUndownloadedVersions = 'showUndownloadedVersions',
Expand Down Expand Up @@ -332,8 +334,16 @@ export interface StartFiddleOptions {
packageManager: IPackageManager;
useSocketFirewall: boolean;
isKeepingUserDataDirs: boolean;
isProcessMonitorEnabled: boolean;
}

export interface DownloadVersionParams {
mirror: Mirrors;
}
export interface ProcessMetric {
pid: number;
name?: string;
type?: string;
cpu: number;
memory: number;
}
1 change: 1 addition & 0 deletions src/ipc-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export enum IpcEvents {
GITHUB_TOKEN_CHECK_AUTH = 'GITHUB_TOKEN_CHECK_AUTH',
GITHUB_TOKEN_SIGN_IN = 'GITHUB_TOKEN_SIGN_IN',
GITHUB_TOKEN_SIGN_OUT = 'GITHUB_TOKEN_SIGN_OUT',
PROCESS_METRICS_UPDATE = 'PROCESS_METRICS_UPDATE',
}

export const ipcMainEvents = [
Expand Down
173 changes: 173 additions & 0 deletions src/less/components/process-monitor.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
@import '../variables.less';

// ─── Body reset ────────────────────────────────────────────────────────────
body {
margin: 0;
padding: 0;
background: @background-2;
overflow: hidden;
font-family: @fonts-common;
color: @text-color-1;
}

// ─── Root container ────────────────────────────────────────────────────────
.process-monitor {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: @background-2;
color: @text-color-1;
overflow: hidden;

// ─── Header bar ──────────────────────────────────────────────────────
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 14px;
background: @background-4;
border-bottom: @border;
user-select: none;
-webkit-app-region: drag;
flex-shrink: 0;
min-height: 36px;
}

&__title {
font-size: 12px;
font-weight: 600;
color: @foreground-1;
letter-spacing: 0.04em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 7px;
}

&__icon {
font-size: 14px;
opacity: 0.75;
color: @foreground-2;
}

&__live-badge {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
color: #4caf7d;
animation: live-pulse 1.6s ease-in-out infinite;
text-transform: uppercase;
}

// ─── Table area ──────────────────────────────────────────────────────
&__table-container {
flex: 1;
overflow-y: auto;

&::-webkit-scrollbar {
width: 6px;
}

&::-webkit-scrollbar-track {
background: @background-2;
}

&::-webkit-scrollbar-thumb {
background: @border-color-1;
border-radius: 3px;
}
}

&__table {
width: 100%;
border-collapse: collapse;
font-size: 12px;

thead tr {
background: @background-3;
}

th {
padding: 6px 14px;
text-align: left;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: @foreground-3;
border-bottom: @border;
}

td {
padding: 7px 14px;
border-bottom: 1px solid @border-color-2;
color: @text-color-3;
white-space: nowrap;
}

tbody tr:last-child td {
border-bottom: none;
}
}

// ─── Rows ─────────────────────────────────────────────────────────────
&__row {
transition: background 0.1s ease;

&:hover {
background: @background-1;
}
}

// ─── Cell accents ─────────────────────────────────────────────────────
&__name {
color: @foreground-1;
font-weight: 500;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}

&__pid {
color: @foreground-3;
font-size: 11px;
font-family: 'Cascadia Code', 'Fira Code', monospace;
}

&__cpu {
color: #4caf7d;
font-weight: 600;
font-family: 'Cascadia Code', 'Fira Code', monospace;

&--high {
color: var(--error-color);
}
}

&__memory {
color: @foreground-2;
font-family: 'Cascadia Code', 'Fira Code', monospace;
}

// ─── Empty state ──────────────────────────────────────────────────────
&__empty {
text-align: center;
padding: 24px 14px !important;
color: @foreground-3;
font-style: italic;
font-size: 12px;
}
}

// ─── Animations ─────────────────────────────────────────────────────────────
@keyframes live-pulse {
0%,
100% {
opacity: 1;
}

50% {
opacity: 0.35;
}
}
52 changes: 52 additions & 0 deletions src/main/fiddle-core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ChildProcess } from 'node:child_process';
import * as fs from 'node:fs';

import pidusage from 'pidusage';
import pidtree from 'pidtree';

import { ElectronVersions, Installer, Runner } from '@electron/fiddle-core';
import {
BrowserWindow,
Expand All @@ -14,6 +17,10 @@ import {
import { ELECTRON_DOWNLOAD_PATH, ELECTRON_INSTALL_PATH } from './constants';
import { cleanupDirectory, deleteUserData, saveFilesToTemp } from './files';
import { ipcMainManager } from './ipc';
import {
closeProcessMonitorWindow,
createProcessMonitorWindow,
} from './process-monitor-window';
import {
ISOLATED_ACTIONS_SCHEME,
getIsolatedRunButtonFrame,
Expand All @@ -27,6 +34,7 @@ import {
DownloadVersionParams,
PACKAGE_NAME,
PMOperationOptions,
ProcessMetric,
ProgressObject,
RunResult,
StartFiddleOptions,
Expand Down Expand Up @@ -165,6 +173,7 @@ async function startFiddleImpl(webContents: WebContents): Promise<RunResult> {
env: rendererEnv,
executionFlags,
isKeepingUserDataDirs,
isProcessMonitorEnabled,
modules,
packageManager,
useSocketFirewall,
Expand Down Expand Up @@ -303,8 +312,51 @@ async function startFiddleImpl(webContents: WebContents): Promise<RunResult> {
child.stdout?.on('data', (data) => pushOutput(webContents, data.toString()));
child.stderr?.on('data', (data) => pushOutput(webContents, data.toString()));

// --- Process Monitor polling ---
let metricsInterval: ReturnType<typeof setInterval> | null = null;

if (isProcessMonitorEnabled && child.pid) {
const rootPid = child.pid;

const monitorWindow = createProcessMonitorWindow();

metricsInterval = setInterval(async () => {
try {
// Get all descendant PIDs
const children = await pidtree(rootPid, { root: true });
const pids = [rootPid, ...children];

// Get CPU + memory for each PID
const stats = await pidusage(pids);
const metrics: ProcessMetric[] = pids
.filter((pid) => stats[pid])
.map((pid, idx) => ({
pid,
type: idx === 0 ? 'Main' : `Renderer/Helper ${idx}`,
cpu: stats[pid].cpu,
// pidusage returns memory in bytes; convert to KB for display
memory: Math.round(stats[pid].memory / 1024),
}));

if (!monitorWindow.isDestroyed()) {
monitorWindow.webContents.send(
IpcEvents.PROCESS_METRICS_UPDATE,
metrics,
);
}
} catch {
// Process may have already exited; ignore errors.
}
}, 1500);
}

return new Promise<RunResult>((resolve) => {
child.on('close', async (code, signal) => {
if (metricsInterval) {
clearInterval(metricsInterval);
metricsInterval = null;
closeProcessMonitorWindow();
}
fiddleProcesses.delete(webContents);
await cleanup();

Expand Down
Loading