Task Manager
Example apps / taskmanager / v0.1.0
Overlay appadmin permissionfront permissionneeds rednode
Every running package and what it costs: CPU, memory, its thread's load and heap, game calls and service calls. End task stops one that misbehaves; Restart starts it again.
The packages on this page are made up: in the game the table lists what really runs.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Task Manager</title>
<script src="rednode://ui/rednode-ui.js"></script>
<style>
html, body { margin: 0; width: 900px; height: 520px; overflow: hidden; background: transparent; }
rn-window { position: absolute; inset: 0; }
[hidden] { display: none !important; }
.status { font: 11px var(--rn-mono); letter-spacing: .08em; color: var(--rn-dim); }
.bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.chip { font: 600 11px var(--rn-mono); letter-spacing: .08em; text-transform: uppercase; cursor: pointer;
padding: 4px 8px; color: var(--rn-dim); border: 1px solid var(--rn-line); background: transparent; }
.chip:hover { color: var(--rn-fg); }
.chip.on { color: var(--rn-on-accent); background: var(--rn-accent); border-color: var(--rn-accent); }
.spacer { flex: 1; }
.totals { font: 11px var(--rn-mono); color: var(--rn-dim); }
.totals b { color: var(--rn-fg); font-weight: 600; }
rn-button::part(button) { min-height: 24px; padding: 2px 9px; font-size: 11px; }
.table-wrap { flex: 1; min-height: 0; overflow-x: hidden; overflow-y: auto; border: 1px solid var(--rn-line); background: var(--rn-panel); }
table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 13px; }
th { position: sticky; top: 0; z-index: 1; background: var(--rn-panel-2); text-align: left; padding: 6px 8px;
font: 600 10px var(--rn-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--rn-dim);
border-bottom: 1px solid var(--rn-line); white-space: nowrap; cursor: pointer; user-select: none; }
th.num, td.num { text-align: right; }
th.sorted { color: var(--rn-accent); }
th.nosort { cursor: default; }
td { padding: 5px 8px; border-bottom: 1px solid color-mix(in srgb, var(--rn-line) 60%, transparent);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
td.mono, td.num { font: 12px var(--rn-mono); }
tr { cursor: pointer; }
tbody tr:hover td { background: color-mix(in srgb, var(--rn-accent-2) 6%, transparent); }
tr.selected td { background: color-mix(in srgb, var(--rn-accent) 12%, transparent); }
.dim { color: var(--rn-dim); }
.hot { color: var(--rn-danger); }
.warm { color: var(--rn-accent-2); }
.st-running { color: var(--rn-ok); }
.st-crashed, .st-not-responding { color: var(--rn-danger); }
.st-starting { color: var(--rn-accent-2); }
.st-stopped, .st-ended { color: var(--rn-dim); }
.empty { padding: 16px; color: var(--rn-dim); }
.details { display: flex; gap: 12px; align-items: flex-start; padding: 8px 10px; border: 1px solid var(--rn-line);
background: var(--rn-panel); font: 11px var(--rn-mono); color: var(--rn-dim); min-height: 52px; }
.details .what { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; overflow-wrap: anywhere; }
.details .what b { color: var(--rn-fg); font: 700 13px var(--rn-display); letter-spacing: .06em; text-transform: uppercase; }
.details .acts { display: flex; gap: 6px; flex: none; }
</style>
</head>
<body>
<rn-window title="Task Manager" draggable closable>
<span class="status" slot="status" id="count">0 RUNNING</span>
<div class="bar">
<span class="totals" id="totals"></span>
<span class="spacer"></span>
<span class="dim status">REFRESH</span>
<button class="chip on" data-every="1000">1 s</button>
<button class="chip" data-every="5000">5 s</button>
<button class="chip" id="pause">Pause</button>
</div>
<div class="table-wrap">
<table>
<colgroup>
<col style="width: 23%"><col style="width: 11%"><col style="width: 8%"><col style="width: 9%"><col style="width: 9%">
<col style="width: 8%"><col style="width: 11%"><col style="width: 11%"><col style="width: 10%">
</colgroup>
<thead><tr id="head"></tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="empty" id="empty" hidden>Nothing is running.</div>
</div>
<div class="details" id="details">
<div class="what" id="what">Select a row to see more, or to end it.</div>
<div class="acts">
<rn-button id="restart" variant="ghost" hidden>Restart</rn-button>
<rn-button id="end" variant="danger" hidden>End task</rn-button>
</div>
</div>
</rn-window>
<script src="app.js"></script>
</body>
</html>
// The Task Manager (SV1, docs/services.md): every running package and what it costs.
// One row per package: its Node mod's process (CPU, memory), the mod's own thread (how
// busy its event loop is, its JavaScript heap), its windows, and its game and service
// calls. End task stops it now; Restart starts it again.
// Part of the Admin Tools pack. Uses window.rednode.tasks, which the host answers only
// for a package the player allowed the "admin" permission; until then the details pane
// says so.
'use strict';
const $ = (id) => document.getElementById(id);
const STATUS = {
starting: 'STARTING',
running: 'RUNNING',
stopped: 'STOPPED',
crashed: 'CRASHED',
'not-responding': 'NOT RESPONDING',
};
// Columns: key, label, how to read a row's value (for sorting), how to show it.
const COLUMNS = [
['name', 'Package', (r) => r.name.toLowerCase(), (r) => r.name],
['status', 'Status', (r) => statusOf(r), (r) => STATUS[statusOf(r)] ?? statusOf(r).toUpperCase()],
['cpu', 'CPU %', (r) => cpuOf(r) ?? -1, (r) => num(cpuOf(r), 1)],
['memory', 'Memory', (r) => memoryOf(r) ?? -1, (r) => (memoryOf(r) == null ? '-' : `${memoryOf(r)} MB`)],
['load', 'Thread', (r) => r.mod?.load ?? -1, (r) => (r.mod?.load == null ? '-' : `${Math.round(r.mod.load * 100)}%`)],
['heap', 'Heap', (r) => r.mod?.heapMB ?? -1, (r) => (r.mod?.heapMB == null ? '-' : `${r.mod.heapMB} MB`)],
['game', 'Game calls/s', (r) => r.gameCallsPerSec, (r) => `${num(r.gameCallsPerSec, 1)}${r.gameCallMs != null ? ` · ${num(r.gameCallMs, 1)} ms` : ''}`],
['svc', 'Service', (r) => r.serviceInPerSec + r.serviceOutPerSec, (r) => (r.service || r.serviceOutPerSec ? `${num(r.serviceInPerSec, 1)} / ${num(r.serviceOutPerSec, 1)}` : '-')],
['up', 'Up', (r) => r.mod?.uptimeMs ?? -1, (r) => uptime(r.mod?.uptimeMs)],
];
const NUMERIC = new Set(['cpu', 'memory', 'load', 'heap', 'game', 'svc', 'up']);
let rows = [];
let total = null;
const view = { sort: 'cpu', desc: true };
let current = null;
let every = 1000;
let paused = false;
let timer = null;
function cleanError(err) {
return String(err?.message || err).replace(/^Error invoking remote method '[^']+': (Error: )?/, '');
}
const num = (v, digits = 0) => (v == null ? '-' : Number(v).toFixed(digits));
function uptime(ms) {
if (ms == null) return '-';
const s = Math.floor(ms / 1000);
if (s < 60) return `${s} s`;
if (s < 3600) return `${Math.floor(s / 60)} min`;
return `${Math.floor(s / 3600)} h ${Math.floor((s % 3600) / 60)} min`;
}
function statusOf(r) {
if (r.mod?.ended) return 'ended';
if (r.mod) return r.mod.status;
return r.windows.length ? 'running' : 'stopped';
}
// A mod's process plus its windows' renderers. A mod in the shared process shows the
// whole shared process (the system sees no finer): its own thread's load and heap, in
// the Thread and Heap columns, tell the mods in it apart.
function cpuOf(r) {
const win = r.windows.reduce((n, w) => n + (w.cpuPercent ?? 0), 0);
if (r.mod?.cpuPercent == null && !r.windows.length) return null;
return (r.mod?.cpuPercent ?? 0) + win;
}
function memoryOf(r) {
const win = r.windows.reduce((n, w) => n + (w.memoryMB ?? 0), 0);
if (r.mod?.memoryMB == null && !r.windows.length) return null;
return (r.mod?.memoryMB ?? 0) + win;
}
function heat(key, r) {
if (key === 'load' && r.mod?.load != null) return r.mod.load > 0.8 ? 'hot' : r.mod.load > 0.3 ? 'warm' : '';
if (key === 'cpu') {
const c = cpuOf(r);
return c == null ? '' : c > 50 ? 'hot' : c > 15 ? 'warm' : '';
}
return '';
}
function el(tag, className, text) {
const e = document.createElement(tag);
if (className) e.className = className;
if (text !== undefined) e.textContent = text;
return e;
}
function renderHead() {
const head = $('head');
head.replaceChildren();
for (const [key, label] of COLUMNS) {
const th = el('th', `${NUMERIC.has(key) ? 'num' : ''}${view.sort === key ? ' sorted' : ''}`, `${label}${view.sort === key ? (view.desc ? ' ▾' : ' ▴') : ''}`);
th.addEventListener('click', () => {
if (view.sort === key) view.desc = !view.desc;
else {
view.sort = key;
view.desc = NUMERIC.has(key);
}
render();
});
head.appendChild(th);
}
}
function render() {
renderHead();
const col = COLUMNS.find((c) => c[0] === view.sort);
const sorted = rows.slice().sort((a, b) => {
const x = col[2](a);
const y = col[2](b);
const d = x < y ? -1 : x > y ? 1 : 0;
return view.desc ? -d : d;
});
const body = $('rows');
body.replaceChildren();
for (const r of sorted) {
const tr = el('tr', r.id === current ? 'selected' : '');
for (const [key, , , show] of COLUMNS) {
const td = el('td', `${NUMERIC.has(key) ? 'num' : ''} ${heat(key, r)}`.trim(), show(r));
if (key === 'status') td.className = `mono st-${statusOf(r)}`;
if (key === 'name') {
td.textContent = '';
td.append(el('b', '', r.name));
if (r.mod?.process === 'shared') {
td.append(el('span', 'dim', ' · shared'));
td.title = 'In the shared process: CPU and memory are the whole process; Thread and Heap are this mod alone';
}
if (r.builtin) td.append(el('span', 'dim', ' · built in'));
}
tr.appendChild(td);
}
tr.addEventListener('click', () => {
current = r.id;
render();
});
body.appendChild(tr);
}
$('empty').hidden = rows.length > 0;
const running = rows.filter((r) => statusOf(r) === 'running').length;
$('count').textContent = `${running} RUNNING`;
if (total) {
$('totals').innerHTML = '';
$('totals').append(
'REDnode: ',
el('b', '', `${num(total.cpuPercent, 1)}% CPU`),
' · ',
el('b', '', `${total.memoryMB} MB`),
` in ${total.processes} processes`,
);
}
renderDetails();
}
function renderDetails() {
const r = rows.find((x) => x.id === current);
const what = $('what');
what.replaceChildren();
$('end').hidden = $('restart').hidden = !r;
if (!r) {
what.append('Select a row to see more, or to end it.');
return;
}
what.append(el('b', '', `${r.name} ${r.version ?? ''}`.trim()));
const lines = [];
if (r.mod) {
const where = r.mod.process === 'shared' ? `the shared process with ${r.mod.processMods.filter((m) => m !== r.id).join(', ') || 'no other mod'}` : r.mod.process ? 'its own process' : 'no process';
lines.push(`Node mod in ${where}${r.mod.pid ? ` (pid ${r.mod.pid})` : ''}. Its thread: ${r.mod.load == null ? 'load unknown' : `${Math.round(r.mod.load * 100)}% busy`}, heap ${r.mod.heapMB ?? '?'} of ${r.mod.heapLimitMB ?? '?'} MB${r.mod.cpuMs != null ? `, ${(r.mod.cpuMs / 1000).toFixed(1)} s of CPU so far` : ''}.`);
if (r.mod.ended) lines.push('Ended from here. Restart starts it again; it also starts with the next game.');
else if (r.mod.error) lines.push(`Last error: ${r.mod.error.split('\n')[0]}`);
}
if (r.windows.length) lines.push(`Windows: ${r.windows.map((w) => `${w.title} ${w.memoryMB ?? '?'} MB`).join(', ')}.`);
if (r.service) lines.push(`Service: ${r.service.functions ? r.service.functions.join(', ') || 'no functions' : 'not ready'}${r.serviceErrors ? `; ${r.serviceErrors} failed calls in the last 5 s` : ''}${r.serviceMs != null ? `; answers in ${num(r.serviceMs, 1)} ms` : ''}.`);
for (const l of lines) what.append(el('span', '', l));
$('end').hidden = r.builtin || statusOf(r) === 'ended' || statusOf(r) === 'stopped';
$('restart').hidden = r.builtin || !r.mod;
}
async function refresh() {
try {
const list = await window.rednode.tasks.list();
rows = list.rows;
total = list.total;
render();
} catch (err) {
$('what').textContent = cleanError(err);
}
}
function schedule() {
clearInterval(timer);
timer = paused ? null : setInterval(refresh, every);
}
async function act(name, fn) {
try {
await fn();
} catch (err) {
RednodeUI.notify(cleanError(err), { type: 'error', title: name });
}
setTimeout(refresh, 300);
}
$('end').addEventListener('click', () => {
const r = rows.find((x) => x.id === current);
if (r) act('End task', () => window.rednode.tasks.end(r.id));
});
$('restart').addEventListener('click', () => {
const r = rows.find((x) => x.id === current);
if (r) act('Restart', () => window.rednode.tasks.restart(r.id));
});
for (const chip of document.querySelectorAll('[data-every]')) {
chip.addEventListener('click', () => {
every = Number(chip.dataset.every);
for (const c of document.querySelectorAll('[data-every]')) c.classList.toggle('on', c === chip);
schedule();
});
}
$('pause').addEventListener('click', () => {
paused = !paused;
$('pause').classList.toggle('on', paused);
schedule();
});
refresh();
schedule();
REDnode runs JavaScript. The rednode package also has TypeScript types, so an editor checks these calls; compile TypeScript to .mjs first.
{
"id": "taskmanager",
"name": "Task Manager",
"version": "0.1.0",
"author": "Edgemodders",
"description": "Every running package and what it costs: CPU, memory, its thread's load, game calls and service calls. End task stops one that misbehaves; Restart starts it again.",
"entry": "index.html",
"permissions": ["admin", "front"],
"requires": { "rednode": ">=0.37.0" },
"kind": "window",
"width": 900,
"height": 520,
"anchor": "center",
"x": 0,
"y": 0,
"open": false
}
The manifest names the package and says how REDnode runs it. Its fields are in Overlay apps.
How it works
The host measures each package from outside: its Node mod's process and its own thread (so a mod stuck in an endless loop still shows, at about 100 % Thread), and its windows' renderers. Calls are counted over the last 5 seconds.
End task stops a mod at once; one that does not stop within 2 s has its process ended. A mod in the shared process is stopped alone, and the others in it run on.
Managing other packages needs the admin permission, which the player allows in the Mod Manager after a warning. front keeps the window above other packages' windows. Without the grant, every call is refused and the details pane says why.
Write your own admin tool with admin from the rednode package, in a page: admin.tasks(), admin.endTask(id), admin.restartTask(id).
Part of REDnode Admin Tools.
What it uses
"admin" permission window.rednode.tasks admin.tasks admin.endTask "front" permission
Try this
- Sort by CPU %.
- Select Crash mod, stuck at 100 % Thread, and press End task.
- Press Restart to bring it back.
Use it
Copy the files into red4ext/plugins/REDnode/apps/taskmanager/ in the game folder, or start your own package from them: change id and name in the manifest first. The whole folder is on GitHub.