Update btop
This commit is contained in:
@@ -2,4 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **`baretop` / `btop`**: Scrollable overview tab with pinned HUD (activity sparklines, session summary, delegate histogram); compact **`metrics_live`** panels (replication, kernel counters with delta mode, warm cache, IPC, boot budget, swarm lifecycle, initd readiness, worker hints, protomux collaboration); capped flatten and delegate inflight table; health score breakdown (**`BARE_TOP_HEALTH_DETAIL`**); optional lite snapshot batch (**`BARE_TOP_SNAPSHOT_LITE`**); export metadata + **`BARE_TOP_EXPORT_REDACT`**; process tab **TIME** column, **F6**/**S** sort, **V** tree view, state-based row color, optional mouse wheel/click; many layout/telemetry env toggles documented in **`src/baretop.js`** and **`man baretop`**.
|
||||
- **`baretop` / `btop`**: **`BARE_TOP_FETCH_EWMA=1`** lowers parallel **`/proc`** batch concurrency when the previous fetch-time EWMA is high; **`BARE_TOP_SCROLL_REGION=1`** sets an experimental DECSTBM region around the process list (reset each frame). Sample fixture **`baretop-sample-metrics.json`** includes **`netSummaryNestedExample`** for nested net UI tests.
|
||||
|
||||
- **`baretop` / `btop`**: Network tab formats nested **`net_summary.json`** ( **`replicationQueue`**, **`peerFirewallStats`**, **`bsdSocketGuestBridge`** ) without **`[object Object]`**; optional **`BARE_TOP_NET_FILTER`** applies **`/`** on that tab; wide two-column merge when **`BARE_TOP_LAYOUT=even`** and cols ≥ 100; net byte-delta spark from iface counters. Overview gains **nethealth**, **diskstats**, **syscalls** summary, **delegate_red**, **security_posture**, **boot_budget_summary**, **peer health**, **capabilities diff**, **extensions**, **git** deltas (with **`d`** + prev snap), larger kernel counter sample, **IPC** backpressure line from proc JSON, **session wall** spark, **worker/sandbox** hints, **readErrPaths** listing, **MemUsed** bar + swap line + logical CPU count in HUD; process columns **NI**/**PRI**/**CPU%** when present, **`BARE_TOP_CMD_ELLIPSIS_MIDDLE`**, extended sort keys, tree ancestor dimming + ASCII **`+--`**, **x** toggles initd raw JSON, features flattened table, catalog version line, pear pack depth **8**, host-tab worker/sandbox one-liners, mouse click-scroll on list tabs **2–9**. Snapshot batch adds **`security_posture.json`**; **`readErrPaths`** on failed **`readFile`** paths.
|
||||
|
||||
- **`baretop` / `btop`**: Health score no longer penalizes operational **`replicationLive.stallHint`** values (**`no_peers`**, **`length_unavailable`**, **`ok`**); delegate inflight penalties use active (count > 0) kinds with a combined cap; optional **`replicationStall: true`** forces the stall penalty. **`bareOsReadBareTopSnapshot`** may return **`metricsLiveText`** (same payload as **`metrics_live.json`**) to avoid an extra read when the booter builds both. **`BARE_TOP_INCREMENTAL=2`** experimental CSI line-diff redraws; **`BARE_TOP_LAYOUT_AUTO`**, **F2** setup overlay, **F3**/**F4** process row jump, **F10** quit; overview **limits** / **p2p** sections, network tab interface sort, tab intro lines, filter-match row highlight, **`layoutVersion` `baretop-ui-3`**. Default **`BARE_TOP_FETCH_CONCURRENCY`** **8**. QA script **`scripts/qa-baretop-health.mjs`**.
|
||||
|
||||
- **`baretop` / `btop`** (prior): Scrollable overview tab with pinned HUD (activity sparklines, session summary, delegate histogram); compact **`metrics_live`** panels (replication, kernel counters with delta mode, warm cache, IPC, boot budget, swarm lifecycle, initd readiness, worker hints, protomux collaboration); capped flatten and delegate inflight table; health score breakdown (**`BARE_TOP_HEALTH_DETAIL`**); optional lite snapshot batch (**`BARE_TOP_SNAPSHOT_LITE`**); export metadata + **`BARE_TOP_EXPORT_REDACT`**; process tab **TIME** column, **F6**/**S** sort, **V** tree view, state-based row color, optional mouse wheel/click; many layout/telemetry env toggles documented in **`src/baretop.js`** and **`man baretop`**.
|
||||
|
||||
@@ -39,7 +39,7 @@ Or `node packages/bare-os-coreutils/build.mjs`.
|
||||
|
||||
**`edit`** is a full-screen TTY buffer editor (syntax highlighting, search, save). **`nano`** is built from the same **`src/edit.js`** with the same **`lib/edit-*.js`** preamble; **`/bin/nano`** exists for familiarity, and the stock shell alias **`nano` → `edit`** routes **`nano`** to that utility (see **`packages/bare-os-booter/lib/shell.js`**). Both require a real TTY (**`stdout.isTTY`**).
|
||||
|
||||
**`baretop`** is the stock **`top`** alias target: a multi-tab TTY dashboard over **`/proc/bare_os/*`** (session metrics, Pear, replication, initd, scrollable overview, process tree/sort, optional mouse, …). **`btop`** is the same bundle as **`/bin/baretop`** (short name); the stock shell alias **`btop` → `baretop`** matches **`nano` → `edit`**. The booter may expose **`ctx.bareOsReadBareTopSnapshot`** (optional **`{ lite: true }`**) to batch-read the same mirrors **`baretop`** would **`vfs.readFile`** individually. Rebuild **`kernel/bin/baretop`** and **`kernel/bin/btop`** with **`node packages/bare-os-coreutils/build.mjs`** after editing **`lib/baretop-snapshot.js`**, **`lib/baretop-ui-helpers.js`**, or **`lib/baretop-tui.js`**.
|
||||
**`baretop`** is the stock **`top`** alias target: a multi-tab TTY dashboard over **`/proc/bare_os/*`** (session metrics, Pear, replication, initd, scrollable overview, process tree/sort, optional mouse, …). **`btop`** is the same bundle as **`/bin/baretop`** (short name); the stock shell alias **`btop` → `baretop`** matches **`nano` → `edit`**. The booter may expose **`ctx.bareOsReadBareTopSnapshot`** (optional **`{ lite: true }`**) to batch-read the same mirrors **`baretop`** would **`vfs.readFile`** individually; the return value can include **`metricsLiveText`** so **`metrics_live.json`** need not be read twice. **`BARE_TOP_INCREMENTAL=2`** enables experimental line-diff redraws; **`BARE_TOP_LAYOUT_AUTO=1`** picks a wide split on large terminals. Rebuild **`kernel/bin/baretop`** and **`kernel/bin/btop`** with **`node packages/bare-os-coreutils/build.mjs`** after editing **`lib/baretop-snapshot.js`**, **`lib/baretop-ui-helpers.js`**, or **`lib/baretop-tui.js`**.
|
||||
|
||||
**`ls`** prepends **[`bare-os-lscolors`](../bare-os-lscolors/bare-os-lscolors.js)** for **`LS_COLORS`** / dircolors parsing. **`dircolors`** and **`theme`** integrate with the booter’s **`bare-os-theme-presets.js`** (see [docs/themes/README.md](../../docs/themes/README.md)).
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/** Fetch /proc and ctx snapshots for /bin/baretop (preamble; no import). */
|
||||
|
||||
/** EWMA of last fetch wall time (ms); used when `BARE_TOP_FETCH_EWMA=1`. */
|
||||
var bareTopFetchEwmaMs = -1
|
||||
|
||||
/**
|
||||
* Keep in sync with `bareOsReadBareTopSnapshot` path list in packages/bare-os-booter/index.js
|
||||
* @type {readonly [string, string][]}
|
||||
@@ -49,7 +52,8 @@ var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
|
||||
['processTable', '/proc/bare_os/process_table.json'],
|
||||
['syscalls', '/proc/bare_os/syscalls.json'],
|
||||
['metricsProm', '/proc/bare_os/metrics.prom'],
|
||||
['protomuxWire', '/proc/bare_os/protomux.json']
|
||||
['protomuxWire', '/proc/bare_os/protomux.json'],
|
||||
['securityPosture', '/proc/bare_os/security_posture.json']
|
||||
]
|
||||
|
||||
/** Subset for BARE_TOP_SNAPSHOT_LITE=1 — keep keys subset of full list above. */
|
||||
@@ -115,15 +119,32 @@ async function bareTopReadProc(ctx, path) {
|
||||
* @param {number} concurrency
|
||||
* @returns {Promise<Record<string, string>>}
|
||||
*/
|
||||
async function bareTopReadProcBatch(ctx, entries, concurrency) {
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {readonly [string, string][]} entries
|
||||
* @param {number} concurrency
|
||||
* @param {string[] | null} [errPaths] push failed paths (optional)
|
||||
*/
|
||||
async function bareTopReadProcBatch(ctx, entries, concurrency, errPaths) {
|
||||
/** @type {Record<string, string>} */
|
||||
const out = {}
|
||||
const vfs = ctx.vfs
|
||||
const conc = Math.max(1, Math.min(16, concurrency | 0) || 6)
|
||||
for (let i = 0; i < entries.length; i += conc) {
|
||||
const slice = entries.slice(i, i + conc)
|
||||
await Promise.all(
|
||||
slice.map(async ([key, path]) => {
|
||||
out[key] = await bareTopReadProc(ctx, path)
|
||||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||||
out[key] = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
const buf = await vfs.readFile(path)
|
||||
out[key] = bareTopBufToString(buf, ctx.b4a)
|
||||
} catch {
|
||||
out[key] = ''
|
||||
if (errPaths) errPaths.push(path)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -172,9 +193,16 @@ function bareTopHealthScore(m, peers, readErr) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtractive score from 100: healthy sessions read ~95–100; issues pull it down.
|
||||
* Weights: read/metrics gaps, delegate inflight keys + sum, replication stall,
|
||||
* boot budgets, initd failures, peer sanity / mismatch.
|
||||
* Subtractive score from 100 (penalty table):
|
||||
* - read_err: -28
|
||||
* - no_metrics: -22
|
||||
* - delegate_inflight: capped combined from active keys (count>0) + total inflight sum; max -12
|
||||
* - replication stall: -20 when stallHint is a fault token OR replicationStall===true;
|
||||
* benign hints no_peers | length_unavailable | ok do not penalize (operational, not fault)
|
||||
* - boot_cold exceeded: -12; boot_stdlib exceeded: -12
|
||||
* - initd_readiness.failed: up to -18; kernel initd.unit_failed_final: up to -10
|
||||
* - peer_mismatch (replication.peerCount vs metrics peers): -8 when both finite and |Δ|>2
|
||||
* - peers_invalid (peers<0): -5
|
||||
* @param {Record<string, unknown> | null} m
|
||||
* @param {number} peers
|
||||
* @param {string | null | undefined} readErr
|
||||
@@ -205,20 +233,28 @@ function bareTopHealthScoreDetail(m, peers, readErr) {
|
||||
if (delI != null && typeof delI === 'object') {
|
||||
const o = /** @type {Record<string, unknown>} */ (delI)
|
||||
const keys = Object.keys(o)
|
||||
const n = keys.length
|
||||
const penK = Math.min(14, n * 2)
|
||||
if (penK > 0) {
|
||||
score -= penK
|
||||
parts.push('inflight_k_' + n + '=-' + penK)
|
||||
}
|
||||
let nActive = 0
|
||||
let sum = 0
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
sum += Math.max(0, Number(o[keys[i]]) || 0)
|
||||
const v = Math.max(0, Number(o[keys[i]]) || 0)
|
||||
sum += v
|
||||
if (v > 0) nActive++
|
||||
}
|
||||
const penS = Math.min(28, sum * 3)
|
||||
if (penS > 0) {
|
||||
score -= penS
|
||||
parts.push('inflight_sum_' + sum + '=-' + penS)
|
||||
const penK = Math.min(10, nActive * 2)
|
||||
const penS = Math.min(20, sum * 2)
|
||||
const inflightCap = 12
|
||||
const inflightTotal = Math.min(inflightCap, penK + penS)
|
||||
if (inflightTotal > 0) {
|
||||
score -= inflightTotal
|
||||
parts.push(
|
||||
'inflight_-' +
|
||||
inflightTotal +
|
||||
'(activeKinds=' +
|
||||
nActive +
|
||||
',sum=' +
|
||||
sum +
|
||||
')'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,21 +262,31 @@ function bareTopHealthScoreDetail(m, peers, readErr) {
|
||||
live.replicationLive && typeof live.replicationLive === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (live.replicationLive)
|
||||
: null
|
||||
const stallExplicit =
|
||||
rep && rep.replicationStall === true ? true : false
|
||||
const hintRaw = rep && rep.stallHint
|
||||
if (hintRaw != null) {
|
||||
const benignStallHints = new Set([
|
||||
'',
|
||||
'ok',
|
||||
'no_peers',
|
||||
'length_unavailable',
|
||||
'0',
|
||||
'false',
|
||||
'none',
|
||||
'off',
|
||||
'null',
|
||||
'undefined'
|
||||
])
|
||||
if (stallExplicit) {
|
||||
const pen = 20
|
||||
score -= pen
|
||||
parts.push('replication_stall_flag=-' + pen)
|
||||
} else if (hintRaw != null) {
|
||||
const hs = String(hintRaw).trim().toLowerCase()
|
||||
if (
|
||||
hs &&
|
||||
hs !== '0' &&
|
||||
hs !== 'false' &&
|
||||
hs !== 'none' &&
|
||||
hs !== 'off' &&
|
||||
hs !== 'null' &&
|
||||
hs !== 'undefined'
|
||||
) {
|
||||
if (hs && !benignStallHints.has(hs)) {
|
||||
const pen = 20
|
||||
score -= pen
|
||||
parts.push('stall=-' + pen)
|
||||
parts.push('stall_hint=-' + pen + '(' + hs + ')')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +330,14 @@ function bareTopHealthScoreDetail(m, peers, readErr) {
|
||||
}
|
||||
}
|
||||
|
||||
if (rep && typeof rep.peerCount === 'number' && Number.isFinite(peers)) {
|
||||
if (
|
||||
rep &&
|
||||
typeof rep.peerCount === 'number' &&
|
||||
Number.isFinite(rep.peerCount) &&
|
||||
rep.peerCount >= 0 &&
|
||||
Number.isFinite(peers) &&
|
||||
peers >= 0
|
||||
) {
|
||||
if (Math.abs(rep.peerCount - peers) > 2) {
|
||||
score -= 8
|
||||
parts.push('peer_mismatch=-8')
|
||||
@@ -341,8 +394,10 @@ async function bareTopFetchSnapshot(ctx) {
|
||||
ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string>} */ (ctx.env)
|
||||
: {}
|
||||
const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '6', 10)
|
||||
const concurrency = Number.isFinite(concRaw) ? concRaw : 6
|
||||
const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '8', 10)
|
||||
const concurrency = Number.isFinite(concRaw) ? concRaw : 8
|
||||
const fetchEwmaOn =
|
||||
env.BARE_TOP_FETCH_EWMA === '1' || env.BARE_TOP_FETCH_EWMA === 'true'
|
||||
const snapshotLite =
|
||||
env.BARE_TOP_SNAPSHOT_LITE === '1' ||
|
||||
env.BARE_TOP_SNAPSHOT_LITE === 'true'
|
||||
@@ -353,6 +408,8 @@ async function bareTopFetchSnapshot(ctx) {
|
||||
/** @type {Record<string, string>} */
|
||||
let fileTexts = {}
|
||||
let fastAtMs = 0
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let metricsLiveFromSnap = null
|
||||
|
||||
try {
|
||||
if (typeof ctx.bareOsReadBareTopSnapshot === 'function') {
|
||||
@@ -370,20 +427,41 @@ async function bareTopFetchSnapshot(ctx) {
|
||||
}
|
||||
const am = o.atMs
|
||||
if (typeof am === 'number' && Number.isFinite(am)) fastAtMs = am
|
||||
const mlRaw = o.metricsLiveText
|
||||
if (typeof mlRaw === 'string' && mlRaw.trim()) {
|
||||
const mp = bareTopJsonParse(mlRaw)
|
||||
if (mp && typeof mp === 'object') {
|
||||
metricsLiveFromSnap = /** @type {Record<string, unknown>} */ (mp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const readErrPaths = []
|
||||
if (Object.keys(fileTexts).length === 0) {
|
||||
fileTexts = await bareTopReadProcBatch(ctx, procEntryList, concurrency)
|
||||
let useConc = concurrency
|
||||
if (fetchEwmaOn && bareTopFetchEwmaMs >= 0) {
|
||||
const e = bareTopFetchEwmaMs
|
||||
if (e > 900) useConc = Math.max(2, concurrency - 3)
|
||||
else if (e > 500) useConc = Math.max(2, concurrency - 2)
|
||||
else if (e > 280) useConc = Math.max(2, concurrency - 1)
|
||||
}
|
||||
fileTexts = await bareTopReadProcBatch(
|
||||
ctx,
|
||||
procEntryList,
|
||||
useConc,
|
||||
readErrPaths
|
||||
)
|
||||
}
|
||||
|
||||
const atMs = fastAtMs || Date.now()
|
||||
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let metricsLive = null
|
||||
let metricsLive = metricsLiveFromSnap
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let resources = null
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
@@ -484,14 +562,31 @@ async function bareTopFetchSnapshot(ctx) {
|
||||
const loadavgLine = (await bareTopReadProc(ctx, '/proc/loadavg')).split('\n')[0]?.trim() || ''
|
||||
|
||||
let cpuLine = ''
|
||||
let cpuCoreCount = 0
|
||||
const cpuRaw = await bareTopReadProc(ctx, '/proc/cpuinfo')
|
||||
for (const line of cpuRaw.split('\n')) {
|
||||
if (line.startsWith('model name') || line.startsWith('Model')) {
|
||||
cpuLine = line.replace(/^[^:]+:\s*/, '').trim().slice(0, 72)
|
||||
break
|
||||
}
|
||||
const L = line.trim().toLowerCase()
|
||||
if (L.startsWith('processor') && L.includes(':')) cpuCoreCount++
|
||||
}
|
||||
if (!cpuCoreCount) {
|
||||
const m = cpuRaw.match(/^processor\s*:/gim)
|
||||
if (m) cpuCoreCount = m.length
|
||||
}
|
||||
|
||||
let swapinfoLine = ''
|
||||
for (const line of memRaw.split('\n')) {
|
||||
const L = line.trim()
|
||||
if (L.startsWith('SwapTotal:') || L.startsWith('SwapFree:')) {
|
||||
swapinfoLine = swapinfoLine ? swapinfoLine + ' ' + L : L
|
||||
}
|
||||
}
|
||||
|
||||
const diskstatsRaw = await bareTopReadProc(ctx, '/proc/diskstats')
|
||||
const diskstatsLine = diskstatsRaw.split('\n')[0]?.trim() || ''
|
||||
|
||||
const hostOs = bareTopParsedFile(fileTexts, 'hostOs')
|
||||
const metaAt =
|
||||
metricsLive && typeof metricsLive.atMs === 'number' ? metricsLive.atMs : atMs
|
||||
@@ -514,6 +609,10 @@ async function bareTopFetchSnapshot(ctx) {
|
||||
}
|
||||
|
||||
const fetchWallMs = Date.now() - fetchStart
|
||||
bareTopFetchEwmaMs =
|
||||
bareTopFetchEwmaMs < 0
|
||||
? fetchWallMs
|
||||
: bareTopFetchEwmaMs * 0.82 + fetchWallMs * 0.18
|
||||
const peersN = Number(metricsLive && metricsLive.peers) || 0
|
||||
const healthD = bareTopHealthScoreDetail(metricsLive, peersN, readErr)
|
||||
|
||||
@@ -524,12 +623,16 @@ async function bareTopFetchSnapshot(ctx) {
|
||||
initdGraph,
|
||||
netSummary,
|
||||
meminfoLine,
|
||||
swapinfoLine,
|
||||
loadavgLine,
|
||||
cpuLine,
|
||||
cpuCoreCount,
|
||||
diskstatsLine,
|
||||
hostOs,
|
||||
atMs,
|
||||
metaAtMs: metaAt,
|
||||
readErr,
|
||||
readErrPaths,
|
||||
fileTexts,
|
||||
extra,
|
||||
fairnessSnapshot,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,17 +5,25 @@
|
||||
"synopsis": [
|
||||
"baretop [OPTION]... [OPERAND]..."
|
||||
],
|
||||
"description": "Full-screen terminal dashboard (htop-inspired) over /proc/bare_os/*, metrics_live, and optional ctx fast paths. Logical process PIDs from process_table.json — not host OS PIDs. Overview scrolls with a pinned HUD (sparklines, delegates). BARE_TOP_OVERVIEW_COMPACT and BARE_TOP_OVERVIEW_SECTIONS tune sections; BARE_TOP_LAYOUT=even enables a wide split. Processes: sort pid|name|state|time (BARE_TOP_PROC_SORT, F6/S, </>), tree (V), filter (/), optional BARE_TOP_MOUSE. See src/baretop.js --help for the full environment list (BARE_TOP_SNAPSHOT_LITE, BARE_TOP_EXPORT_REDACT, BARE_TOP_HEALTH_DETAIL, …).",
|
||||
"description": "Full-screen terminal dashboard (htop-inspired) over /proc/bare_os/*, metrics_live, and optional ctx fast paths. Logical process PIDs from process_table.json — not host OS PIDs. Overview scrolls with a pinned HUD (sparklines, delegates) and extra sections (network summary line, diskstats, syscalls summary, security_posture, boot_budget_summary, peer health, capabilities diff, extensions, git deltas in delta mode). BARE_TOP_OVERVIEW_COMPACT and BARE_TOP_OVERVIEW_SECTIONS tune sections. Network tab formats nested net_summary objects (replicationQueue, peerFirewallStats, bsdSocketGuestBridge). BARE_TOP_INCREMENTAL=2 builds a full text frame each tick but patches only changed terminal lines. See src/baretop.js --help for the full environment list.",
|
||||
"options": [],
|
||||
"environment": [
|
||||
"BARE_TOP_INTERVAL_MS — refresh period (ms).",
|
||||
"BARE_TOP_SNAPSHOT_LITE=1 — smaller bareOsReadBareTopSnapshot batch.",
|
||||
"BARE_TOP_OVERVIEW_COMPACT=1 — hide low-priority overview sections.",
|
||||
"BARE_TOP_OVERVIEW_SECTIONS — comma list of section tokens.",
|
||||
"BARE_TOP_PROC_SORT — pid | name | state | time.",
|
||||
"BARE_TOP_MOUSE=1 — SGR mouse (wheel; row click on processes tab).",
|
||||
"BARE_TOP_PROC_SORT — pid | name | state | time | nice | pri | cpu.",
|
||||
"BARE_TOP_NET_FILTER=1 — / filter applies on network tab.",
|
||||
"BARE_TOP_CMD_ELLIPSIS_MIDDLE=1 — middle-ellipsis long process names.",
|
||||
"BARE_TOP_MOUSE=1 — SGR mouse (wheel; row click processes; click-scroll other list tabs).",
|
||||
"BARE_TOP_EXPORT_REDACT=1 — omit fileTexts from JSON export.",
|
||||
"BARE_TOP_HEALTH_DETAIL=1 — show health score penalty breakdown."
|
||||
"BARE_TOP_HEALTH_DETAIL=1 — show health score penalty breakdown.",
|
||||
"BARE_TOP_INCREMENTAL=2 — line-diff terminal output (layout still computed each tick).",
|
||||
"BARE_TOP_DEBUG_TIMINGS=1 — throttled fetch/draw timing on stderr.",
|
||||
"BARE_TOP_FETCH_EWMA=1 — reduce parallel /proc batch reads when recent fetches are slow.",
|
||||
"BARE_TOP_SCROLL_REGION=1 — DECSTBM scroll region around the process list (experimental).",
|
||||
"BARE_TOP_LAYOUT_AUTO=1 — use even layout when terminal is wide.",
|
||||
"bareOsReadBareTopSnapshot — may include metricsLiveText to skip a separate metrics_live read."
|
||||
],
|
||||
"keywords": [
|
||||
"baretop",
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
|
||||
"scripts": {
|
||||
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
|
||||
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs"
|
||||
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Manual QA checklist for baretop health score (run inside a Bare OS TTY session).
|
||||
* 1. Run: baretop
|
||||
* 2. With zero peers and idle delegates, health should read 100 (not stuck at 73).
|
||||
* 3. Run with: BARE_TOP_HEALTH_DETAIL=1 baretop — overview shows penalty breakdown.
|
||||
* 4. Optional: BARE_TOP_INCREMENTAL=2 baretop — stderr with BARE_TOP_DEBUG_TIMINGS=1 may show patchLen < outLen when idle.
|
||||
* 5. Network tab (tab 4): nested net_summary fields render as lines/JSON — no literal “[object Object]”.
|
||||
* 6. Optional: BARE_TOP_NET_FILTER=1 then / on network tab filters lines.
|
||||
*/
|
||||
console.log(`baretop health QA:
|
||||
- Expect health 100 on single-node idle (operational stallHint no longer penalized).
|
||||
- Use BARE_TOP_HEALTH_DETAIL=1 to verify breakdown text.
|
||||
- Use BARE_TOP_DEBUG_TIMINGS=1 to inspect fetchMs / outLen / patchLen.
|
||||
- Network tab: replicationQueue / peerFirewallStats / bsdSocketGuestBridge should show structured text.
|
||||
- BARE_TOP_NET_FILTER=1 enables / filter on the network tab.`)
|
||||
@@ -11,15 +11,19 @@ async function run(ctx, argv) {
|
||||
'Requires a TTY.\n' +
|
||||
'\n' +
|
||||
'Tabs: 1 overview 2 processes 3 initd 4 network 5 features 6 diagnostics\n' +
|
||||
' 7 operator 8 pear 9 catalog [ ] host 0 keys (F1 help, F5 refresh, F9 signal)\n' +
|
||||
' 7 operator 8 pear 9 catalog [ ] host 0 keys\n' +
|
||||
' F1 help F2 setup F3/F4 proc row F5 refresh F9 signal F10 quit\n' +
|
||||
'\n' +
|
||||
'Environment:\n' +
|
||||
' BARE_TOP_INTERVAL_MS Refresh period (default 1000; clamped 250–10000)\n' +
|
||||
' BARE_TOP_UI_MIN_MS Minimum ms between full redraws (0 = off)\n' +
|
||||
' BARE_TOP_FETCH_CONCURRENCY Parallel /proc reads per batch (default 6)\n' +
|
||||
' BARE_TOP_SNAPSHOT_LITE Set to 1 for smaller bareOsReadBareTopSnapshot batch\n' +
|
||||
' BARE_TOP_FETCH_CONCURRENCY Parallel /proc reads per batch (default 8)\n' +
|
||||
' BARE_TOP_FETCH_EWMA Set to 1 to reduce batch concurrency when recent fetches are slow\n' +
|
||||
' BARE_TOP_SNAPSHOT_LITE Set to 1 for smaller bareOsReadBareTopSnapshot batch (net_summary still read)\n' +
|
||||
' BARE_TOP_NET_FILTER Set to 1 so / filter applies on network tab\n' +
|
||||
' BARE_TOP_CMD_ELLIPSIS_MIDDLE Set to 1 for middle-ellipsis on long process names\n' +
|
||||
' BARE_TOP_NO_ALTSCREEN Any non-empty value skips alternate-screen mode\n' +
|
||||
' BARE_TOP_INCREMENTAL Set to 1 to skip full 2J clear (may flicker)\n' +
|
||||
' BARE_TOP_INCREMENTAL 1 = soft home (no 2J) 2 = line-diff patches (full frame each tick; terminal patched)\n' +
|
||||
' BARE_TOP_NO_2J_AFTER_FIRST Set to 1 to use soft clear after first full paint\n' +
|
||||
' BARE_TOP_ASCII_GRAPH Set to 1 for ASCII sparklines\n' +
|
||||
' BARE_TOP_ASCII_UI Set to 1 for ASCII box/spark fallbacks\n' +
|
||||
@@ -32,6 +36,7 @@ async function run(ctx, argv) {
|
||||
' BARE_TOP_COLOR256 Set to 1 to force 256-color bar hints\n' +
|
||||
' BARE_TOP_VI_KEYS Set to 1 for hjkl focus in vi-style mode\n' +
|
||||
' BARE_TOP_LAYOUT stacked | even (wide overview split + mini jobs)\n' +
|
||||
' BARE_TOP_LAYOUT_AUTO Set to 1 to pick even layout when cols >= 100\n' +
|
||||
' BARE_TOP_FRAMES Set to 1 for left rule on scroll regions\n' +
|
||||
' BARE_TOP_FULLSCREEN_HIDE_TABS Set to 1 to hide tab strip in fullscreen (f)\n' +
|
||||
' BARE_TOP_OVERVIEW_COMPACT Set to 1 to hide low-priority overview sections\n' +
|
||||
@@ -50,11 +55,11 @@ async function run(ctx, argv) {
|
||||
' BARE_TOP_SCROLL_REGION Set to 1 only with compatible terminals (see help)\n' +
|
||||
' NO_COLOR Disable ANSI colors\n' +
|
||||
'\n' +
|
||||
'Keys: q quit r F5 refresh+burst sp pause . step d delta e export f fullscreen\n' +
|
||||
' t UTC [ ] tabs arrows scroll / move selection g G top/bottom h F1 help\n' +
|
||||
'Keys: q F10 quit r F5 refresh+burst sp pause . step d delta e export f fullscreen\n' +
|
||||
' t UTC [ ] tabs arrows scroll / move selection g G top/bottom h F1 help F2 setup\n' +
|
||||
' / filter (overview sections, initd, or processes) V tree (processes)\n' +
|
||||
' F6 S sort menu (processes) <> cycle sort Enter detail k F9 signal\n' +
|
||||
' Esc closes help or filter\n' +
|
||||
' F3 F4 next/prev process row F6 S sort menu (processes) <> cycle sort\n' +
|
||||
' Enter detail k F9 signal Esc closes help, setup, or filter\n' +
|
||||
'\n' +
|
||||
'Contributors: /bin bundles cannot use node:crypto or node:module; use ctx.vfs and\n' +
|
||||
'JSON.parse only. See holepunch-repos/docs/05-BARE-RUNTIME.md (Bare module stack).'
|
||||
|
||||
@@ -17,6 +17,10 @@ test('baretop-tui uses alternate screen teardown pattern', async (t) => {
|
||||
(src.match(/\\x1b\[2J\\x1b\[H/g) || []).length >= 1,
|
||||
'expected full clear fallback in finally'
|
||||
)
|
||||
t.ok(
|
||||
src.includes('incrementalFrameDiff'),
|
||||
'expected incremental frame diff gate in tui source'
|
||||
)
|
||||
})
|
||||
|
||||
test('shipped kernel/bin/baretop includes snapshot + TUI', async (t) => {
|
||||
|
||||
@@ -19,6 +19,10 @@ test('fixture metrics_live-shaped JSON parses with expected keys', async (t) =>
|
||||
t.ok(typeof o.delegateInflight === 'object')
|
||||
t.ok(o.replicationLive && typeof o.replicationLive === 'object')
|
||||
t.ok(o.kernelCounters && typeof o.kernelCounters === 'object')
|
||||
t.ok(
|
||||
o.netSummaryNestedExample &&
|
||||
typeof o.netSummaryNestedExample.replicationQueue === 'object'
|
||||
)
|
||||
})
|
||||
|
||||
test('baretop-snapshot preamble lists batch proc entries', async (t) => {
|
||||
@@ -29,6 +33,7 @@ test('baretop-snapshot preamble lists batch proc entries', async (t) => {
|
||||
t.ok(src.includes('bareTopHealthScore'))
|
||||
t.ok(src.includes('BARE_TOP_SNAPSHOT_LITE_ENTRIES'))
|
||||
t.ok(src.includes('bootBudgetSummary'))
|
||||
t.ok(src.includes('security_posture.json'))
|
||||
})
|
||||
|
||||
test('bareTopHealthScore penalizes stall and inflight cardinality', async (t) => {
|
||||
@@ -49,6 +54,44 @@ test('bareTopHealthScore penalizes stall and inflight cardinality', async (t) =>
|
||||
t.is(ctx.bareTopHealthScore(good, 2), 100)
|
||||
})
|
||||
|
||||
test('bareTopHealthScore: no_peers + idle delegates is not stuck at 73', async (t) => {
|
||||
const src = await readFile(snapPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(src, ctx)
|
||||
const idle = {
|
||||
peers: 0,
|
||||
delegateInflight: {},
|
||||
replicationLive: { peerCount: 0, stallHint: 'no_peers' }
|
||||
}
|
||||
t.is(ctx.bareTopHealthScore(idle, 0), 100)
|
||||
const lightInflight = {
|
||||
peers: 0,
|
||||
delegateInflight: { http: 1, dns: 0 },
|
||||
replicationLive: { peerCount: 0, stallHint: 'no_peers' }
|
||||
}
|
||||
const s2 = ctx.bareTopHealthScore(lightInflight, 0)
|
||||
t.ok(s2 >= 90)
|
||||
t.not(s2, 73)
|
||||
})
|
||||
|
||||
test('bareTopHealthScoreDetail: benign stall hints and replicationStall flag', async (t) => {
|
||||
const src = await readFile(snapPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(src, ctx)
|
||||
const lenOk = {
|
||||
peers: 0,
|
||||
replicationLive: { stallHint: 'length_unavailable', peerCount: 0 }
|
||||
}
|
||||
t.is(ctx.bareTopHealthScore(lenOk, 0), 100)
|
||||
const explicit = {
|
||||
peers: 1,
|
||||
replicationLive: { stallHint: 'ok', peerCount: 1, replicationStall: true }
|
||||
}
|
||||
t.is(ctx.bareTopHealthScore(explicit, 1), 80)
|
||||
const d = ctx.bareTopHealthScoreDetail(explicit, 1, null)
|
||||
t.ok(String(d.breakdown).includes('replication_stall_flag'))
|
||||
})
|
||||
|
||||
test('process_table fixture maps to sorted PID rows via helpers', async (t) => {
|
||||
const raw = await readFile(processTableFixture, 'utf8')
|
||||
const pt = JSON.parse(raw)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import test from 'brittle'
|
||||
|
||||
/**
|
||||
* Mirrors baretop-tui incrementalFrameDiff patch builder (line CUP + EL + line).
|
||||
* @param {string[]} prevLines
|
||||
* @param {string[]} nextLines
|
||||
*/
|
||||
function bareTopLineDiffPatch(prevLines, nextLines) {
|
||||
let patch = '\x1b[?25l'
|
||||
let nch = 0
|
||||
for (let i = 0; i < nextLines.length; i++) {
|
||||
if (prevLines[i] !== nextLines[i]) {
|
||||
nch++
|
||||
const r = Math.max(1, Math.min(Math.floor(i + 1), 9999))
|
||||
const c = 1
|
||||
patch += '\x1b[' + r + ';' + c + 'H' + '\x1b[K' + nextLines[i] + '\r\n'
|
||||
}
|
||||
}
|
||||
patch += '\x1b[?25h'
|
||||
return { patch, nch }
|
||||
}
|
||||
|
||||
test('incremental line-diff patch is smaller than full frame when one line changes', async (t) => {
|
||||
const n = 48
|
||||
const prev = Array.from({ length: n }, (_, i) => 'row-' + i + ' ' + 'x'.repeat(40))
|
||||
const next = prev.slice()
|
||||
next[22] = 'row-22 ' + 'CHANGED'.repeat(6)
|
||||
const full = next.join('\r\n') + '\r\n'
|
||||
const { patch, nch } = bareTopLineDiffPatch(prev, next)
|
||||
t.is(nch, 1)
|
||||
t.ok(
|
||||
patch.length < full.length,
|
||||
'expected patch bytes < full frame (' +
|
||||
patch.length +
|
||||
' vs ' +
|
||||
full.length +
|
||||
')'
|
||||
)
|
||||
})
|
||||
@@ -120,6 +120,91 @@ test('bareTopOverviewLines keeps ASCII section lines within cols', async (t) =>
|
||||
}
|
||||
})
|
||||
|
||||
test('bareTopNetTabLines sorts interfaces by rx+tx', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const lines = ctx.bareTopNetTabLines(
|
||||
{
|
||||
interfaces: [
|
||||
{ name: 'lo', rxBytes: 1, txBytes: 1 },
|
||||
{ name: 'eth0', rxBytes: 100, txBytes: 50 }
|
||||
]
|
||||
},
|
||||
100
|
||||
)
|
||||
t.ok(lines.some((l) => l.includes('interfaces')))
|
||||
t.ok(lines.some((l) => l.includes('eth0')))
|
||||
})
|
||||
|
||||
test('bareTopNetTabLines expands nested objects (no [object Object])', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const raw = await readFile(
|
||||
join(__dirname, 'fixtures/baretop-net-summary-nested.json'),
|
||||
'utf8'
|
||||
)
|
||||
const net = JSON.parse(raw)
|
||||
const lines = ctx.bareTopNetTabLines(net, 100, { maxLines: 200 })
|
||||
const blob = lines.join('\n')
|
||||
t.ok(!blob.includes('[object Object]'), 'should not stringify objects blindly')
|
||||
t.ok(blob.includes('replicationQueue'), 'section for queue')
|
||||
t.ok(blob.includes('peerFirewallStats'), 'section for firewall')
|
||||
t.ok(blob.includes('bsdSocketGuestBridge'), 'section for bridge')
|
||||
t.ok(blob.includes('acceptedSessionCount') || blob.includes('10'), 'firewall scalar')
|
||||
})
|
||||
|
||||
test('bareTopNetTabLines respects maxLines cap', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const big = { a: 1 }
|
||||
for (let i = 0; i < 80; i++) big['k' + i] = i
|
||||
const lines = ctx.bareTopNetTabLines(big, 80, { maxLines: 12 })
|
||||
t.ok(lines.length <= 12)
|
||||
t.ok(lines[lines.length - 1].includes('truncated'))
|
||||
})
|
||||
|
||||
test('bareTopSortProcessRows supports nice and cpu keys', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const rows = [
|
||||
{ pid: 1, name: 'a', nice: 0 },
|
||||
{ pid: 2, name: 'b', nice: 5 }
|
||||
]
|
||||
const byNice = ctx.bareTopSortProcessRows(rows, 'nice', true)
|
||||
t.is(bareTopProcessPidLocal(byNice[0]), 1)
|
||||
const rows2 = [
|
||||
{ pid: 1, name: 'a', cpuPct: 10 },
|
||||
{ pid: 2, name: 'b', cpuPct: 99 }
|
||||
]
|
||||
const byCpu = ctx.bareTopSortProcessRows(rows2, 'cpu', false)
|
||||
t.is(bareTopProcessPidLocal(byCpu[0]), 2)
|
||||
})
|
||||
|
||||
test('bareTopLimitsMergedLines flattens quotas and rlimits', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const snap = {
|
||||
extra: { quotas: { pipe: 8 }, rlimits: { nofile: 1024 } }
|
||||
}
|
||||
const lines = ctx.bareTopLimitsMergedLines(snap)
|
||||
t.ok(lines.some((l) => l.includes('pipe=8')))
|
||||
t.ok(lines.some((l) => l.includes('nofile=1024')))
|
||||
})
|
||||
|
||||
test('bareTopDelegateInflightTableLines right-aligns counts', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const lines = ctx.bareTopDelegateInflightTableLines({ a: 3, b: 12 }, 5, 8)
|
||||
t.ok(lines[0].includes('b') && lines[0].includes('12'))
|
||||
t.ok(lines[1].includes('a') && lines[1].includes('3'))
|
||||
})
|
||||
|
||||
test('bareTopParseMeminfoMetrics derives MemTotal and MemAvailable', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"schemaVersion": 2,
|
||||
"topicHex": "deadbeef",
|
||||
"peerCount": 3,
|
||||
"seedHandshakeError": null,
|
||||
"replicationQueueDepth": 2,
|
||||
"replicationQueue": {
|
||||
"depth": 2,
|
||||
"pending": 1,
|
||||
"items": [{ "id": "a" }]
|
||||
},
|
||||
"peerFirewallStats": {
|
||||
"acceptedSessionCount": 10,
|
||||
"rejectedSessionCount": 1,
|
||||
"inboundSessionCount": 4,
|
||||
"outboundSessionCount": 6,
|
||||
"wave9": { "x": 1 }
|
||||
},
|
||||
"bsdSocketGuestBridge": {
|
||||
"schema": 1,
|
||||
"policyEnv": "BARE_OS_BSD_SOCKET_POLICY",
|
||||
"note": "test bridge",
|
||||
"activeBridgedFds": 0
|
||||
},
|
||||
"atMs": 1700000000000
|
||||
}
|
||||
@@ -28,5 +28,27 @@
|
||||
"swarmLifecycle": { "phase": "steady" },
|
||||
"initdReadiness": { "failed": 0, "starting": 1, "active": 4 },
|
||||
"workerBudgetHints": { "wallMsMax": 60000 },
|
||||
"processTable": { "schema": 7, "rows": [] }
|
||||
"processTable": { "schema": 7, "rows": [] },
|
||||
"netSummaryNestedExample": {
|
||||
"schemaVersion": 2,
|
||||
"topicHex": "deadbeef",
|
||||
"peerCount": 2,
|
||||
"seedHandshakeError": "",
|
||||
"replicationQueueDepth": 4,
|
||||
"peerFirewallBlockedSessions": 0,
|
||||
"peerFirewallAllowedSessions": 12,
|
||||
"replicationQueue": {
|
||||
"depth": 4,
|
||||
"lastError": null,
|
||||
"batches": [{ "id": 1, "state": "pending" }]
|
||||
},
|
||||
"peerFirewallStats": {
|
||||
"sessions": { "blocked": 0, "allowed": 12 }
|
||||
},
|
||||
"bsdSocketGuestBridge": {
|
||||
"schema": 1,
|
||||
"policyEnv": "default",
|
||||
"note": "fixture"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user