Updates for btop
This commit is contained in:
+1305
-86
File diff suppressed because it is too large
Load Diff
+1305
-86
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-26T12:01:40.794Z",
|
||||
"generatedAt": "2026-04-26T12:27:28.060Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1777204900794,
|
||||
"atMs": 1777206448060,
|
||||
"commands": [
|
||||
"agent",
|
||||
"arch",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8630,19 +8630,21 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
return { ok: true }
|
||||
},
|
||||
/**
|
||||
* Batch-read `/proc/bare_os/*` mirrors for baretop(1). Keys must stay aligned with
|
||||
* Batch-read `/proc/bare_os/*` mirrors for baretop(1). Keys should stay aligned with
|
||||
* `BARE_TOP_SNAPSHOT_PROC_ENTRIES` in packages/bare-os-coreutils/lib/baretop-snapshot.js.
|
||||
* @returns {Promise<{ atMs: number, files: Record<string, string>, metricsLiveText?: string }>}
|
||||
* Supports profile and requested-key hints for smoother tab-aware snapshots.
|
||||
* @returns {Promise<{ atMs: number, files: Record<string, string>, metricsLiveText?: string, snapshotBytes?: number, profile?: string, truncated?: boolean }>}
|
||||
*/
|
||||
async bareOsReadBareTopSnapshot(opts) {
|
||||
const atMs = Date.now()
|
||||
/** @type {Record<string, string>} */
|
||||
const files = Object.create(null)
|
||||
const lite =
|
||||
opts &&
|
||||
typeof opts === 'object' &&
|
||||
/** @type {{ lite?: boolean }} */ (opts).lite === true
|
||||
const fullEntries = [
|
||||
const opt =
|
||||
opts && typeof opts === 'object'
|
||||
? /** @type {{ lite?: boolean, profile?: string, requestedKeys?: string[], maxBytes?: number }} */ (opts)
|
||||
: {}
|
||||
const lite = opt.lite === true
|
||||
const standardEntries = [
|
||||
['index', '/proc/bare_os/index.json'],
|
||||
['version', '/proc/bare_os/version'],
|
||||
['hostOs', '/proc/bare_os/host_os.json'],
|
||||
@@ -8688,45 +8690,82 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
['processTable', '/proc/bare_os/process_table.json'],
|
||||
['syscalls', '/proc/bare_os/syscalls.json'],
|
||||
['metricsProm', '/proc/bare_os/metrics.prom'],
|
||||
['meshdrop', '/proc/bare_os/meshdrop.json'],
|
||||
['peerDetails', '/proc/bare_os/peer_details.json'],
|
||||
['dhtScan', '/proc/bare_os/dht_scan.json'],
|
||||
['swarmDoctor', '/proc/bare_os/swarm_doctor.json'],
|
||||
['routeSummary', '/proc/bare_os/route_summary.json'],
|
||||
['holepunchSummary', '/proc/bare_os/holepunch_summary.json'],
|
||||
['protomuxWire', '/proc/bare_os/protomux.json'],
|
||||
['securityPosture', '/proc/bare_os/security_posture.json'],
|
||||
['processIo', '/proc/bare_os/process_io.json'],
|
||||
['processThreads', '/proc/bare_os/process_threads.json'],
|
||||
['processMaps', '/proc/bare_os/process_maps.json']
|
||||
]
|
||||
const liteEntries = [
|
||||
const minimalEntries = [
|
||||
['index', '/proc/bare_os/index.json'],
|
||||
['version', '/proc/bare_os/version'],
|
||||
['hostOs', '/proc/bare_os/host_os.json'],
|
||||
['replication', '/proc/bare_os/replication'],
|
||||
['swarm', '/proc/bare_os/swarm'],
|
||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||
['processTable', '/proc/bare_os/process_table.json'],
|
||||
['snapshotHints', '/proc/bare_os/snapshot_hints.json']
|
||||
]
|
||||
const extendedEntries = standardEntries.concat([
|
||||
['meshdrop', '/proc/bare_os/meshdrop.json'],
|
||||
['peerDetails', '/proc/bare_os/peer_details.json'],
|
||||
['dhtScan', '/proc/bare_os/dht_scan.json'],
|
||||
['swarmDoctor', '/proc/bare_os/swarm_doctor.json'],
|
||||
['routeSummary', '/proc/bare_os/route_summary.json'],
|
||||
['holepunchSummary', '/proc/bare_os/holepunch_summary.json'],
|
||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||
['processTable', '/proc/bare_os/process_table.json'],
|
||||
['snapshotHints', '/proc/bare_os/snapshot_hints.json']
|
||||
]
|
||||
const entries = lite ? liteEntries : fullEntries
|
||||
['holepunchSummary', '/proc/bare_os/holepunch_summary.json']
|
||||
])
|
||||
const profileReq = String(opt.profile || '').trim().toLowerCase()
|
||||
const profile =
|
||||
lite || profileReq === 'minimal'
|
||||
? 'minimal'
|
||||
: profileReq === 'extended'
|
||||
? 'extended'
|
||||
: 'standard'
|
||||
const byProfile = {
|
||||
minimal: minimalEntries,
|
||||
standard: standardEntries,
|
||||
extended: extendedEntries
|
||||
}
|
||||
let entries = byProfile[profile] || standardEntries
|
||||
const requestedKeySet =
|
||||
Array.isArray(opt.requestedKeys) && opt.requestedKeys.length
|
||||
? new Set(
|
||||
opt.requestedKeys
|
||||
.map((k) => String(k || '').trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
: null
|
||||
if (requestedKeySet && requestedKeySet.size) {
|
||||
entries = entries.filter(([k]) => requestedKeySet.has(k))
|
||||
}
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return { atMs, files }
|
||||
const conc = 6
|
||||
const maxBytesRaw = Number(opt.maxBytes)
|
||||
const maxBytes =
|
||||
Number.isFinite(maxBytesRaw) && maxBytesRaw > 1024
|
||||
? Math.min(8 * 1024 * 1024, Math.floor(maxBytesRaw))
|
||||
: 0
|
||||
let truncated = false
|
||||
for (let i = 0; i < entries.length; i += conc) {
|
||||
const slice = entries.slice(i, i + conc)
|
||||
await Promise.all(
|
||||
slice.map(async ([key, path]) => {
|
||||
if (truncated && maxBytes > 0) {
|
||||
files[key] = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
const buf = await vfs.readFile(path)
|
||||
files[key] =
|
||||
typeof buf === 'string' ? buf : b4a.toString(buf)
|
||||
const txt = typeof buf === 'string' ? buf : b4a.toString(buf)
|
||||
files[key] = txt
|
||||
if (maxBytes > 0) {
|
||||
let running = 0
|
||||
for (const fk of Object.keys(files)) {
|
||||
const fv = files[fk]
|
||||
if (typeof fv === 'string') running += fv.length
|
||||
}
|
||||
if (running > maxBytes) truncated = true
|
||||
}
|
||||
} catch {
|
||||
files[key] = ''
|
||||
}
|
||||
@@ -8741,7 +8780,13 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return { atMs, files, metricsLiveText }
|
||||
let snapshotBytes = 0
|
||||
for (const k of Object.keys(files)) {
|
||||
const v = files[k]
|
||||
if (typeof v === 'string') snapshotBytes += v.length
|
||||
}
|
||||
if (typeof metricsLiveText === 'string') snapshotBytes += metricsLiveText.length
|
||||
return { atMs, files, metricsLiveText, snapshotBytes, profile, truncated }
|
||||
},
|
||||
/**
|
||||
* Register `/run/bare-os/virtual/<name>` reader (cap-gated: `virtualRegisterFiles`).
|
||||
|
||||
@@ -41,6 +41,35 @@ Or `node packages/bare-os-coreutils/build.mjs`.
|
||||
|
||||
**`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`**.
|
||||
|
||||
### Baretop performance baseline
|
||||
|
||||
Run the repeatable baseline from repo root:
|
||||
|
||||
```bash
|
||||
npm run perf:baretop -w bare-os-coreutils
|
||||
```
|
||||
|
||||
or inside the package:
|
||||
|
||||
```bash
|
||||
node ./test/baretop-perf-baseline.test.mjs
|
||||
```
|
||||
|
||||
KPI envelope tracked in Wave 1:
|
||||
|
||||
- `fetchMs` (snapshot fetch wall time): rolling `p50` / `p95`
|
||||
- `composeMs` (frame compose cost): rolling `p50` / `p95`
|
||||
- `emitMs` (terminal emit cost): rolling `p50` / `p95`
|
||||
- `emitBytes` (bytes written/frame): rolling `p50` / `p95`
|
||||
- `droppedRefreshPct` (draws skipped by UI throttling): rolling `p50` / `p95`
|
||||
- startup-to-first-frame latency (`start=...ms` in profile footer)
|
||||
|
||||
Expected development-range targets (local machine, non-SSH):
|
||||
|
||||
- `composeMs p95 < 40ms` on fixture runs
|
||||
- `emitMs p95 < 10ms` in synthetic tests
|
||||
- `droppedRefreshPct p50 < 10%` with default `BARE_TOP_UI_MIN_MS=0`
|
||||
|
||||
**`agent`** is the OpenAI-compatible **HTTPS** assistant (**ReAct**-style tools, TTY streaming). Preamble pulls in **`lib/agent-*.js`**, **`agent-workspace.js`** (**`~/.agent/workspace/*.md`** loader), **`agent-skills.js`** (compact skill index + **`read_skill`**), **`agent-web-fetch.js`**, **`agent-tools.js`**, **`agent-tui.js`** (see **`build.mjs`** **`preamble.agent`**). Config and secrets live under `**~/.agent/**` on the **personal** drive (**`man agent`**). **Markdown soul files** default from [`share/agent-workspace/`](share/agent-workspace/) (also staged to **`kernel/share/agent-workspace/`** on build). Outbound HTTPS uses **`ctx.httpFetch`** (same **`BARE_OS_HTTP_ALLOWLIST`** / **`BARE_OS_HTTP_DENYLIST`** as delegated **`curl`** / **`wget`**); the **`web_fetch`** tool needs every target host allowlisted alongside your API origin. Maintainer smoke under the Bare runtime: from repo root **`npm run smoke:agent-web-fetch:bare`** ([`scripts/smoke-agent-web-fetch-bare.mjs`](../../scripts/smoke-agent-web-fetch-bare.mjs)). **`chat`** is separate: swarm / Protomux text chat (**`lib/chat-tui.js`** preamble; **`man chat`**).
|
||||
|
||||
**`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)).
|
||||
|
||||
@@ -13,6 +13,9 @@ function bareTopSafeWrite(ctx, stdout, s) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Hysteresis latch to avoid patch/full oscillation bursts. */
|
||||
var bareTopPatchHysteresis = { preferFullUntil: 0 }
|
||||
|
||||
/**
|
||||
* FNV-1a 32-bit for line dirty detection (item 4).
|
||||
* @param {string} s
|
||||
@@ -86,37 +89,74 @@ function bareTopBuildLinePatch(prevLines, nextLines, cup, opts) {
|
||||
* cols: number,
|
||||
* incrementalFrameDiff: boolean,
|
||||
* useLineHash: boolean,
|
||||
* patchThresholdPct?: number,
|
||||
* fullscreenPanel: boolean,
|
||||
* cup: (r: number, c: number) => string
|
||||
* }} o
|
||||
* @returns {{ nextLines: string[], wrotePatch: boolean, patchLen: number }}
|
||||
* @returns {{ nextLines: string[], wrotePatch: boolean, patchLen: number, consideredPatch: boolean, fallbackReason: string }}
|
||||
*/
|
||||
function bareTopEmitFrame(ctx, stdout, fullOut, o) {
|
||||
const nextLines = bareTopFrameToLines(fullOut)
|
||||
let wrotePatch = false
|
||||
let patchLen = 0
|
||||
let consideredPatch = false
|
||||
let fallbackReason = 'full_frame_initial'
|
||||
const pctThreshold = Math.max(
|
||||
0.5,
|
||||
Math.min(0.99, Number(o.patchThresholdPct) || 0.92)
|
||||
)
|
||||
const now = Date.now()
|
||||
const inHysteresisFull = bareTopPatchHysteresis.preferFullUntil > now
|
||||
if (
|
||||
o.incrementalFrameDiff &&
|
||||
o.prevLines &&
|
||||
o.prevLines.length === nextLines.length &&
|
||||
nextLines.length > 2 &&
|
||||
!o.fullscreenPanel
|
||||
!o.fullscreenPanel &&
|
||||
!inHysteresisFull
|
||||
) {
|
||||
consideredPatch = true
|
||||
const { patch, nch } = bareTopBuildLinePatch(
|
||||
o.prevLines,
|
||||
nextLines,
|
||||
o.cup,
|
||||
{ useLineHash: o.useLineHash, lineHashMinLen: 200 }
|
||||
{
|
||||
// On tiny frames, hash setup is usually slower than direct compare.
|
||||
useLineHash: o.useLineHash && nextLines.length > 8,
|
||||
lineHashMinLen: 200
|
||||
}
|
||||
)
|
||||
if (nch > 0 && nch < nextLines.length * 0.92) {
|
||||
if (nch > 0 && nch < nextLines.length * pctThreshold) {
|
||||
patchLen = patch.length
|
||||
bareTopSafeWrite(/** @type {Record<string, unknown>} */ (ctx), stdout, patch)
|
||||
wrotePatch = true
|
||||
return { nextLines, wrotePatch, patchLen }
|
||||
return {
|
||||
nextLines,
|
||||
wrotePatch,
|
||||
patchLen,
|
||||
consideredPatch,
|
||||
fallbackReason: 'patch_ok'
|
||||
}
|
||||
}
|
||||
fallbackReason = nch === 0 ? 'patch_no_changes' : 'patch_change_ratio_high'
|
||||
if (nch >= nextLines.length * 0.96) {
|
||||
bareTopPatchHysteresis.preferFullUntil = now + 1200
|
||||
}
|
||||
} else if (!o.incrementalFrameDiff) {
|
||||
fallbackReason = 'incremental_disabled'
|
||||
} else if (inHysteresisFull) {
|
||||
fallbackReason = 'patch_hysteresis_full'
|
||||
} else if (!o.prevLines) {
|
||||
fallbackReason = 'no_prev_frame'
|
||||
} else if (o.prevLines.length !== nextLines.length) {
|
||||
fallbackReason = 'line_count_changed'
|
||||
} else if (nextLines.length <= 2) {
|
||||
fallbackReason = 'frame_too_small'
|
||||
} else if (o.fullscreenPanel) {
|
||||
fallbackReason = 'fullscreen_panel'
|
||||
}
|
||||
bareTopSafeWrite(/** @type {Record<string, unknown>} */ (ctx), stdout, fullOut)
|
||||
return { nextLines, wrotePatch, patchLen }
|
||||
return { nextLines, wrotePatch, patchLen, consideredPatch, fallbackReason }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
/** EWMA of last fetch wall time (ms); used when `BARE_TOP_FETCH_EWMA=1`. */
|
||||
var bareTopFetchEwmaMs = -1
|
||||
/** Last fallback proc batch concurrency (for jitter smoothing). */
|
||||
var bareTopLastFallbackConc = -1
|
||||
/** Soft cache for secondary proc/ctx reads (stale-while-refresh). */
|
||||
var bareTopSecondaryCache = Object.create(null)
|
||||
|
||||
/**
|
||||
* Keep in sync with `bareOsReadBareTopSnapshot` path list in packages/bare-os-booter/index.js
|
||||
@@ -72,6 +76,15 @@ var BARE_TOP_SNAPSHOT_LITE_ENTRIES = [
|
||||
['snapshotHints', '/proc/bare_os/snapshot_hints.json']
|
||||
]
|
||||
|
||||
var BARE_TOP_SNAPSHOT_EXTENDED_HINT_KEYS = [
|
||||
'meshdrop',
|
||||
'peerDetails',
|
||||
'dhtScan',
|
||||
'swarmDoctor',
|
||||
'routeSummary',
|
||||
'holepunchSummary'
|
||||
]
|
||||
|
||||
/** @param {unknown} buf @param {unknown} b4a @returns {string} */
|
||||
function bareTopBufToString(buf, b4a) {
|
||||
if (buf == null) return ''
|
||||
@@ -155,6 +168,31 @@ async function bareTopReadProcBatch(ctx, entries, concurrency, errPaths) {
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-through stale cache helper for secondary fields.
|
||||
* @param {string} key
|
||||
* @param {number} ttlMs
|
||||
* @param {() => Promise<unknown>} producer
|
||||
* @returns {Promise<{ value: unknown, stale: boolean, freshAtMs: number, ttlMs: number }>}
|
||||
*/
|
||||
async function bareTopCachedSecondary(key, ttlMs, producer) {
|
||||
const now = Date.now()
|
||||
const cur = bareTopSecondaryCache[key]
|
||||
if (cur && typeof cur === 'object' && now - cur.atMs <= ttlMs) {
|
||||
return { value: cur.value, stale: false, freshAtMs: cur.atMs, ttlMs }
|
||||
}
|
||||
try {
|
||||
const value = await producer()
|
||||
bareTopSecondaryCache[key] = { value, atMs: now }
|
||||
return { value, stale: false, freshAtMs: now, ttlMs }
|
||||
} catch {
|
||||
if (cur && typeof cur === 'object') {
|
||||
return { value: cur.value, stale: true, freshAtMs: cur.atMs, ttlMs }
|
||||
}
|
||||
return { value: null, stale: true, freshAtMs: 0, ttlMs }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} o
|
||||
* @param {number} maxChars
|
||||
@@ -400,7 +438,7 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
: {}
|
||||
const fo =
|
||||
fetchOpts && typeof fetchOpts === 'object'
|
||||
? /** @type {{ forceLite?: boolean }} */ (fetchOpts)
|
||||
? /** @type {{ forceLite?: boolean, activeTab?: string }} */ (fetchOpts)
|
||||
: null
|
||||
const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '8', 10)
|
||||
const concurrency = Number.isFinite(concRaw) ? concRaw : 8
|
||||
@@ -411,20 +449,77 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
? true
|
||||
: env.BARE_TOP_SNAPSHOT_LITE === '1' ||
|
||||
env.BARE_TOP_SNAPSHOT_LITE === 'true'
|
||||
const snapshotExtendedOn =
|
||||
env.BARE_TOP_SNAPSHOT_EXTENDED === '1' ||
|
||||
env.BARE_TOP_SNAPSHOT_EXTENDED === 'true'
|
||||
const missingSignalsOn =
|
||||
env.BARE_TOP_MISSING_SIGNALS === '1' ||
|
||||
env.BARE_TOP_MISSING_SIGNALS === 'true'
|
||||
const procEntryList = snapshotLite
|
||||
? BARE_TOP_SNAPSHOT_LITE_ENTRIES
|
||||
: BARE_TOP_SNAPSHOT_PROC_ENTRIES
|
||||
const activeTab = String((fo && fo.activeTab) || 'overview').toLowerCase()
|
||||
const requestedKeys = (() => {
|
||||
if (snapshotLite) return BARE_TOP_SNAPSHOT_LITE_ENTRIES.map(([k]) => k)
|
||||
if (activeTab === 'processes')
|
||||
return [
|
||||
'index',
|
||||
'version',
|
||||
'hostOs',
|
||||
'processTable',
|
||||
'sessionStats',
|
||||
'swarm',
|
||||
'replication',
|
||||
'snapshotHints'
|
||||
]
|
||||
if (activeTab === 'network')
|
||||
return [
|
||||
'index',
|
||||
'version',
|
||||
'hostOs',
|
||||
'swarm',
|
||||
'replication',
|
||||
'dhtStatus',
|
||||
'udxExtended',
|
||||
'snapshotHints'
|
||||
]
|
||||
if (
|
||||
missingSignalsOn &&
|
||||
(activeTab === 'network' || activeTab === 'operator' || activeTab === 'diagnostics')
|
||||
) {
|
||||
return BARE_TOP_SNAPSHOT_PROC_ENTRIES.map(([k]) => k).concat(
|
||||
BARE_TOP_SNAPSHOT_EXTENDED_HINT_KEYS
|
||||
)
|
||||
}
|
||||
return BARE_TOP_SNAPSHOT_PROC_ENTRIES.map(([k]) => k)
|
||||
})()
|
||||
const snapshotProfile =
|
||||
snapshotLite || activeTab === 'processes' || activeTab === 'network'
|
||||
? 'minimal'
|
||||
: snapshotExtendedOn ||
|
||||
(missingSignalsOn &&
|
||||
(activeTab === 'network' ||
|
||||
activeTab === 'operator' ||
|
||||
activeTab === 'diagnostics'))
|
||||
? 'extended'
|
||||
: 'standard'
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
let fileTexts = {}
|
||||
let fastAtMs = 0
|
||||
let snapshotBatchBytes = 0
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let metricsLiveFromSnap = null
|
||||
|
||||
try {
|
||||
if (typeof ctx.bareOsReadBareTopSnapshot === 'function') {
|
||||
const r = await ctx.bareOsReadBareTopSnapshot(
|
||||
snapshotLite ? { lite: true } : undefined
|
||||
snapshotLite
|
||||
? { lite: true, requestedKeys }
|
||||
: {
|
||||
profile: snapshotProfile,
|
||||
requestedKeys
|
||||
}
|
||||
)
|
||||
if (r && typeof r === 'object') {
|
||||
const o = /** @type {Record<string, unknown>} */ (r)
|
||||
@@ -437,6 +532,13 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
}
|
||||
const am = o.atMs
|
||||
if (typeof am === 'number' && Number.isFinite(am)) fastAtMs = am
|
||||
if (
|
||||
typeof o.snapshotBytes === 'number' &&
|
||||
Number.isFinite(o.snapshotBytes) &&
|
||||
o.snapshotBytes >= 0
|
||||
) {
|
||||
snapshotBatchBytes = o.snapshotBytes
|
||||
}
|
||||
const mlRaw = o.metricsLiveText
|
||||
if (typeof mlRaw === 'string' && mlRaw.trim()) {
|
||||
const mp = bareTopJsonParse(mlRaw)
|
||||
@@ -460,6 +562,11 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
else if (e > 500) useConc = Math.max(2, concurrency - 2)
|
||||
else if (e > 280) useConc = Math.max(2, concurrency - 1)
|
||||
}
|
||||
if (bareTopLastFallbackConc > 0) {
|
||||
if (useConc > bareTopLastFallbackConc + 1) useConc = bareTopLastFallbackConc + 1
|
||||
if (useConc < bareTopLastFallbackConc - 1) useConc = bareTopLastFallbackConc - 1
|
||||
}
|
||||
bareTopLastFallbackConc = useConc
|
||||
fileTexts = await bareTopReadProcBatch(
|
||||
ctx,
|
||||
procEntryList,
|
||||
@@ -486,6 +593,22 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
let subprocessBridge = null
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
let hostStats = null
|
||||
const ttl = {
|
||||
metricsLive: 300,
|
||||
resources: activeTab === 'diagnostics' ? 400 : 1500,
|
||||
features: activeTab === 'features' ? 500 : 2000,
|
||||
netSummary: activeTab === 'network' ? 300 : 1500,
|
||||
initdGraph: activeTab === 'initd' ? 400 : 2000,
|
||||
fairness: activeTab === 'operator' ? 500 : 2000,
|
||||
subprocess: activeTab === 'diagnostics' ? 600 : 2500,
|
||||
hostStats: activeTab === 'host' ? 500 : 2000,
|
||||
memRaw: activeTab === 'mem' ? 350 : 1400,
|
||||
loadavg: activeTab === 'cpu' ? 350 : 1200,
|
||||
cpuRaw: activeTab === 'cpu' ? 450 : 1800,
|
||||
diskstats: activeTab === 'disk' ? 450 : 1800
|
||||
}
|
||||
/** @type {Record<string, { stale: boolean, freshAtMs: number, ttlMs: number }>} */
|
||||
const sectionTtl = {}
|
||||
|
||||
try {
|
||||
if (typeof ctx.bareOsReadProcMetricsLive === 'function') {
|
||||
@@ -498,9 +621,22 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
}
|
||||
|
||||
if (!metricsLive) {
|
||||
const ml = await bareTopCachedSecondary(
|
||||
'metrics_live',
|
||||
ttl.metricsLive,
|
||||
async () => {
|
||||
const t = await bareTopReadProc(ctx, '/proc/bare_os/metrics_live.json')
|
||||
const p = bareTopJsonParse(t)
|
||||
if (p && typeof p === 'object') metricsLive = /** @type {Record<string, unknown>} */ (p)
|
||||
return bareTopJsonParse(t)
|
||||
}
|
||||
)
|
||||
sectionTtl.metricsLive = {
|
||||
stale: ml.stale,
|
||||
freshAtMs: ml.freshAtMs,
|
||||
ttlMs: ml.ttlMs
|
||||
}
|
||||
if (ml.value && typeof ml.value === 'object') {
|
||||
metricsLive = /** @type {Record<string, unknown>} */ (ml.value)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -513,52 +649,119 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
}
|
||||
|
||||
if (!resources) {
|
||||
const rs = await bareTopCachedSecondary(
|
||||
'resources',
|
||||
ttl.resources,
|
||||
async () => {
|
||||
const t = await bareTopReadProc(ctx, '/proc/bare_os_resources')
|
||||
const p = bareTopJsonParse(t)
|
||||
if (p && typeof p === 'object') resources = /** @type {Record<string, unknown>} */ (p)
|
||||
return bareTopJsonParse(t)
|
||||
}
|
||||
)
|
||||
sectionTtl.resources = { stale: rs.stale, freshAtMs: rs.freshAtMs, ttlMs: rs.ttlMs }
|
||||
if (rs.value && typeof rs.value === 'object') {
|
||||
resources = /** @type {Record<string, unknown>} */ (rs.value)
|
||||
}
|
||||
}
|
||||
|
||||
const fs = await bareTopCachedSecondary(
|
||||
'features',
|
||||
ttl.features,
|
||||
async () => {
|
||||
let fp = bareTopJsonParse(await bareTopReadProc(ctx, '/proc/bare_os/features'))
|
||||
if (!fp) fp = bareTopJsonParse(await bareTopReadProc(ctx, '/proc/bare_os_features'))
|
||||
if (fp && typeof fp === 'object') features = /** @type {Record<string, unknown>} */ (fp)
|
||||
return fp
|
||||
}
|
||||
)
|
||||
sectionTtl.features = { stale: fs.stale, freshAtMs: fs.freshAtMs, ttlMs: fs.ttlMs }
|
||||
if (fs.value && typeof fs.value === 'object') {
|
||||
features = /** @type {Record<string, unknown>} */ (fs.value)
|
||||
}
|
||||
|
||||
const ns = await bareTopCachedSecondary(
|
||||
'net_summary',
|
||||
ttl.netSummary,
|
||||
async () => {
|
||||
const netT = await bareTopReadProc(ctx, '/proc/bare_os/net_summary.json')
|
||||
const np = bareTopJsonParse(netT)
|
||||
if (np && typeof np === 'object') netSummary = /** @type {Record<string, unknown>} */ (np)
|
||||
return bareTopJsonParse(netT)
|
||||
}
|
||||
)
|
||||
sectionTtl.netSummary = {
|
||||
stale: ns.stale,
|
||||
freshAtMs: ns.freshAtMs,
|
||||
ttlMs: ns.ttlMs
|
||||
}
|
||||
if (ns.value && typeof ns.value === 'object') {
|
||||
netSummary = /** @type {Record<string, unknown>} */ (ns.value)
|
||||
}
|
||||
|
||||
const ig = await bareTopCachedSecondary(
|
||||
'initd_graph',
|
||||
ttl.initdGraph,
|
||||
async () => {
|
||||
const initT = await bareTopReadProc(ctx, '/proc/bare_os/initd_graph.json')
|
||||
initdGraph = bareTopJsonParse(initT)
|
||||
return bareTopJsonParse(initT)
|
||||
}
|
||||
)
|
||||
sectionTtl.initdGraph = {
|
||||
stale: ig.stale,
|
||||
freshAtMs: ig.freshAtMs,
|
||||
ttlMs: ig.ttlMs
|
||||
}
|
||||
initdGraph = ig.value
|
||||
if (initdGraph === null) {
|
||||
const alt = bareTopJsonParse(fileTexts.initdDag || '')
|
||||
initdGraph = alt
|
||||
}
|
||||
|
||||
try {
|
||||
const fa = await bareTopCachedSecondary('fairness_snapshot', ttl.fairness, async () => {
|
||||
if (typeof ctx.bareOsReadDelegateFairnessSnapshot === 'function') {
|
||||
const o = ctx.bareOsReadDelegateFairnessSnapshot()
|
||||
if (o && typeof o === 'object')
|
||||
fairnessSnapshot = /** @type {Record<string, unknown>} */ (o)
|
||||
return ctx.bareOsReadDelegateFairnessSnapshot()
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
return null
|
||||
})
|
||||
sectionTtl.fairnessSnapshot = {
|
||||
stale: fa.stale,
|
||||
freshAtMs: fa.freshAtMs,
|
||||
ttlMs: fa.ttlMs
|
||||
}
|
||||
if (fa.value && typeof fa.value === 'object') {
|
||||
fairnessSnapshot = /** @type {Record<string, unknown>} */ (fa.value)
|
||||
}
|
||||
|
||||
try {
|
||||
const sb = await bareTopCachedSecondary(
|
||||
'subprocess_bridge',
|
||||
ttl.subprocess,
|
||||
async () => {
|
||||
if (typeof ctx.bareOsReadSubprocessBridgeSnapshot === 'function') {
|
||||
subprocessBridge = ctx.bareOsReadSubprocessBridgeSnapshot()
|
||||
return ctx.bareOsReadSubprocessBridgeSnapshot()
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
return null
|
||||
}
|
||||
)
|
||||
sectionTtl.subprocessBridge = {
|
||||
stale: sb.stale,
|
||||
freshAtMs: sb.freshAtMs,
|
||||
ttlMs: sb.ttlMs
|
||||
}
|
||||
subprocessBridge = sb.value
|
||||
|
||||
const hs = await bareTopCachedSecondary('host_stats', ttl.hostStats, async () => {
|
||||
return ctx.bareOsHostStats || null
|
||||
})
|
||||
sectionTtl.hostStats = { stale: hs.stale, freshAtMs: hs.freshAtMs, ttlMs: hs.ttlMs }
|
||||
if (hs.value && typeof hs.value === 'object') {
|
||||
hostStats = /** @type {Record<string, unknown>} */ (hs.value)
|
||||
}
|
||||
|
||||
try {
|
||||
const hs = ctx.bareOsHostStats
|
||||
if (hs && typeof hs === 'object') hostStats = /** @type {Record<string, unknown>} */ (hs)
|
||||
} catch {
|
||||
/* ignore */
|
||||
const memC = await bareTopCachedSecondary('proc_meminfo', ttl.memRaw, async () =>
|
||||
bareTopReadProc(ctx, '/proc/meminfo')
|
||||
)
|
||||
sectionTtl.meminfo = {
|
||||
stale: memC.stale,
|
||||
freshAtMs: memC.freshAtMs,
|
||||
ttlMs: memC.ttlMs
|
||||
}
|
||||
|
||||
const memRaw = await bareTopReadProc(ctx, '/proc/meminfo')
|
||||
const memRaw = typeof memC.value === 'string' ? memC.value : ''
|
||||
let meminfoLine = ''
|
||||
for (const line of memRaw.split('\n')) {
|
||||
const L = line.trim()
|
||||
@@ -569,11 +772,27 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
}
|
||||
if (!meminfoLine) meminfoLine = memRaw.split('\n')[0]?.trim() || ''
|
||||
|
||||
const loadavgLine = (await bareTopReadProc(ctx, '/proc/loadavg')).split('\n')[0]?.trim() || ''
|
||||
const loadC = await bareTopCachedSecondary('proc_loadavg', ttl.loadavg, async () =>
|
||||
bareTopReadProc(ctx, '/proc/loadavg')
|
||||
)
|
||||
sectionTtl.loadavg = {
|
||||
stale: loadC.stale,
|
||||
freshAtMs: loadC.freshAtMs,
|
||||
ttlMs: loadC.ttlMs
|
||||
}
|
||||
const loadavgLine = String(loadC.value || '').split('\n')[0]?.trim() || ''
|
||||
|
||||
let cpuLine = ''
|
||||
let cpuCoreCount = 0
|
||||
const cpuRaw = await bareTopReadProc(ctx, '/proc/cpuinfo')
|
||||
const cpuC = await bareTopCachedSecondary('proc_cpuinfo', ttl.cpuRaw, async () =>
|
||||
bareTopReadProc(ctx, '/proc/cpuinfo')
|
||||
)
|
||||
sectionTtl.cpuinfo = {
|
||||
stale: cpuC.stale,
|
||||
freshAtMs: cpuC.freshAtMs,
|
||||
ttlMs: cpuC.ttlMs
|
||||
}
|
||||
const cpuRaw = typeof cpuC.value === 'string' ? cpuC.value : ''
|
||||
for (const line of cpuRaw.split('\n')) {
|
||||
if (line.startsWith('model name') || line.startsWith('Model')) {
|
||||
cpuLine = line.replace(/^[^:]+:\s*/, '').trim().slice(0, 72)
|
||||
@@ -594,10 +813,45 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
const diskstatsRaw = await bareTopReadProc(ctx, '/proc/diskstats')
|
||||
const dsC = await bareTopCachedSecondary(
|
||||
'proc_diskstats',
|
||||
ttl.diskstats,
|
||||
async () => bareTopReadProc(ctx, '/proc/diskstats')
|
||||
)
|
||||
sectionTtl.diskstats = {
|
||||
stale: dsC.stale,
|
||||
freshAtMs: dsC.freshAtMs,
|
||||
ttlMs: dsC.ttlMs
|
||||
}
|
||||
const diskstatsRaw = typeof dsC.value === 'string' ? dsC.value : ''
|
||||
const diskstatsLine = diskstatsRaw.split('\n')[0]?.trim() || ''
|
||||
|
||||
const hostOs = bareTopParsedFile(fileTexts, 'hostOs')
|
||||
const procIndex =
|
||||
bareTopParsedFile(fileTexts, 'index') || /** @type {Record<string, unknown>} */ ({})
|
||||
const procIndexAvailability = (() => {
|
||||
/** @type {Record<string, boolean>} */
|
||||
const out = Object.create(null)
|
||||
const nodes =
|
||||
procIndex && Array.isArray(procIndex.nodes) ? procIndex.nodes : []
|
||||
const nodeSet = new Set()
|
||||
for (const n of nodes) {
|
||||
if (!n || typeof n !== 'object') continue
|
||||
const no = /** @type {Record<string, unknown>} */ (n)
|
||||
if (typeof no.name === 'string') nodeSet.add(no.name)
|
||||
}
|
||||
for (const [k, p] of BARE_TOP_SNAPSHOT_PROC_ENTRIES) {
|
||||
const base = p.split('/').pop() || ''
|
||||
out[k] =
|
||||
nodeSet.has(base) ||
|
||||
nodeSet.has(base.replace(/\.json$/i, '')) ||
|
||||
(typeof fileTexts[k] === 'string' && fileTexts[k].length > 0)
|
||||
}
|
||||
for (const k of BARE_TOP_SNAPSHOT_EXTENDED_HINT_KEYS) {
|
||||
out[k] = out[k] === true || (typeof fileTexts[k] === 'string' && fileTexts[k].length > 0)
|
||||
}
|
||||
return out
|
||||
})()
|
||||
const metaAt =
|
||||
metricsLive && typeof metricsLive.atMs === 'number' ? metricsLive.atMs : atMs
|
||||
|
||||
@@ -617,6 +871,17 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
}
|
||||
extra[key] = bareTopParsedFile(fileTexts, key)
|
||||
}
|
||||
for (const key of BARE_TOP_SNAPSHOT_EXTENDED_HINT_KEYS) {
|
||||
if (extra[key] == null) extra[key] = bareTopParsedFile(fileTexts, key)
|
||||
}
|
||||
extra.procIndex = procIndex
|
||||
extra.procIndexAvailability = procIndexAvailability
|
||||
/** @type {Record<string, number>} */
|
||||
const snapshotBytesByKey = Object.create(null)
|
||||
for (const k of Object.keys(fileTexts)) {
|
||||
const v = fileTexts[k]
|
||||
snapshotBytesByKey[k] = typeof v === 'string' ? v.length : 0
|
||||
}
|
||||
|
||||
const fetchWallMs = Date.now() - fetchStart
|
||||
bareTopFetchEwmaMs =
|
||||
@@ -650,6 +915,11 @@ async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||||
subprocessBridge,
|
||||
hostStats,
|
||||
fetchWallMs,
|
||||
snapshotBatchBytes,
|
||||
snapshotBytesByKey,
|
||||
sectionTtl,
|
||||
snapshotProfile,
|
||||
snapshotRequestedKeys: requestedKeys,
|
||||
healthScore: healthD.score,
|
||||
healthBreakdown: healthD.breakdown,
|
||||
snapshotLite
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,24 @@
|
||||
/** Pure UI helpers for /bin/baretop (preamble; no import). */
|
||||
|
||||
/** @type {Map<string, unknown>} */
|
||||
var bareTopUiMemo = new Map()
|
||||
var BARE_TOP_UI_MEMO_MAX = 256
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {() => unknown} build
|
||||
*/
|
||||
function bareTopMemoGet(key, build) {
|
||||
if (bareTopUiMemo.has(key)) return bareTopUiMemo.get(key)
|
||||
const v = build()
|
||||
bareTopUiMemo.set(key, v)
|
||||
if (bareTopUiMemo.size > BARE_TOP_UI_MEMO_MAX) {
|
||||
const it = bareTopUiMemo.keys().next()
|
||||
if (!it.done) bareTopUiMemo.delete(it.value)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @returns {{ label: string, value: string }[]}
|
||||
@@ -235,6 +254,36 @@ function bareTopSortProcessRows(rows, sortKey, asc, opts) {
|
||||
const tertiaryPpid = o.tertiaryPpid === true
|
||||
const wallMs = Number(o.sortWallMs) || 0
|
||||
const dir = asc ? 1 : -1
|
||||
const memoKey =
|
||||
'sort:' +
|
||||
sortKey +
|
||||
':' +
|
||||
(asc ? '1' : '0') +
|
||||
':' +
|
||||
(tertiaryPpid ? '1' : '0') +
|
||||
':' +
|
||||
wallMs +
|
||||
':' +
|
||||
rows
|
||||
.map((r) => {
|
||||
const rr = /** @type {Record<string, unknown>} */ (r)
|
||||
return (
|
||||
String(rr.pid ?? '') +
|
||||
'/' +
|
||||
String(rr.ppid ?? '') +
|
||||
'/' +
|
||||
String(rr.name ?? '') +
|
||||
'/' +
|
||||
String(rr.state ?? '') +
|
||||
'/' +
|
||||
String(rr.startedAtMs ?? '') +
|
||||
'/' +
|
||||
String(rr.cpuPct ?? rr.cpu ?? '')
|
||||
)
|
||||
})
|
||||
.join('|')
|
||||
return /** @type {Record<string, unknown>[]} */ (
|
||||
bareTopMemoGet(memoKey, () => {
|
||||
const out = rows.slice()
|
||||
out.sort((a, b) => {
|
||||
const ka = bareTopProcessSortKey(a, sortKey, wallMs)
|
||||
@@ -251,6 +300,8 @@ function bareTopSortProcessRows(rows, sortKey, asc, opts) {
|
||||
return bareTopProcessPid(a) - bareTopProcessPid(b)
|
||||
})
|
||||
return out
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,6 +650,27 @@ function bareTopUiFlatten(prefix, v, depth, maxD, lines, maxKeys) {
|
||||
* @param {number} maxLines
|
||||
*/
|
||||
function bareTopUiFlattenLimited(prefix, v, depth, maxD, maxKeys, maxLines) {
|
||||
let sig = ''
|
||||
try {
|
||||
sig = JSON.stringify(v)
|
||||
} catch {
|
||||
sig = String(v)
|
||||
}
|
||||
const key =
|
||||
'flatten:' +
|
||||
prefix +
|
||||
':' +
|
||||
depth +
|
||||
':' +
|
||||
maxD +
|
||||
':' +
|
||||
maxKeys +
|
||||
':' +
|
||||
maxLines +
|
||||
':' +
|
||||
sig
|
||||
return /** @type {string[]} */ (
|
||||
bareTopMemoGet(key, () => {
|
||||
const lines = []
|
||||
bareTopUiFlatten(prefix, v, depth, maxD, lines, maxKeys)
|
||||
const cap = Math.max(4, maxLines | 0)
|
||||
@@ -606,6 +678,8 @@ function bareTopUiFlattenLimited(prefix, v, depth, maxD, maxKeys, maxLines) {
|
||||
return lines.slice(0, cap - 1).concat([' … (truncated)'])
|
||||
}
|
||||
return lines
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -973,7 +1047,8 @@ function bareTopP2PStackLines(snap) {
|
||||
*/
|
||||
function bareTopNetTabLines(net, cols, opts) {
|
||||
const oopts = opts && typeof opts === 'object' ? opts : {}
|
||||
const maxTotal = Math.max(8, (oopts.maxLines | 0) || 512)
|
||||
const adaptiveMax = cols < 90 ? 256 : cols < 120 ? 384 : 512
|
||||
const maxTotal = Math.max(8, (oopts.maxLines | 0) || adaptiveMax)
|
||||
const filt = String(oopts.filter || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@@ -1050,6 +1125,24 @@ function bareTopNetTabLines(net, cols, opts) {
|
||||
bareTopTruncateCell(extra, w - 24, false)
|
||||
)
|
||||
}
|
||||
if (arr.length > 32) {
|
||||
let remRx = 0
|
||||
let remTx = 0
|
||||
for (const iface of arr.slice(32)) {
|
||||
if (!iface || typeof iface !== 'object') continue
|
||||
const i = /** @type {Record<string, unknown>} */ (iface)
|
||||
remRx += Number(i.rxBytes ?? i.rx) || 0
|
||||
remTx += Number(i.txBytes ?? i.tx) || 0
|
||||
}
|
||||
pushFiltered(
|
||||
' (+ ' +
|
||||
(arr.length - 32) +
|
||||
' more) rx=' +
|
||||
bareTopNetFormatBytes(remRx) +
|
||||
' tx=' +
|
||||
bareTopNetFormatBytes(remTx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sectionKeys = new Set([
|
||||
@@ -1155,6 +1248,137 @@ function bareTopNetTabLines(net, cols, opts) {
|
||||
return lines.length ? lines : [' (empty net summary)']
|
||||
}
|
||||
|
||||
function bareTopNetworkDeepDiveLines(extra, cols) {
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
const rs =
|
||||
extra && extra.routeSummary && typeof extra.routeSummary === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (extra.routeSummary)
|
||||
: null
|
||||
if (rs) {
|
||||
const direct = Number(rs.directCount ?? rs.direct ?? 0) || 0
|
||||
const relay = Number(rs.relayCount ?? rs.relay ?? 0) || 0
|
||||
const total = direct + relay
|
||||
const ratio = total > 0 ? Math.round((direct / total) * 100) : 0
|
||||
out.push(
|
||||
' route direct=' +
|
||||
direct +
|
||||
' relay=' +
|
||||
relay +
|
||||
' directRatio=' +
|
||||
ratio +
|
||||
'%'
|
||||
)
|
||||
}
|
||||
const hs =
|
||||
extra && extra.holepunchSummary && typeof extra.holepunchSummary === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (extra.holepunchSummary)
|
||||
: null
|
||||
if (hs) {
|
||||
const keys = Object.keys(hs).slice(0, 4)
|
||||
const bits = keys.map((k) => k + '=' + bareTopFormatScalarTerminal(hs[k], 18))
|
||||
if (bits.length) out.push(' holepunch ' + bits.join(' '))
|
||||
}
|
||||
const sd =
|
||||
extra && extra.swarmDoctor && typeof extra.swarmDoctor === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (extra.swarmDoctor)
|
||||
: null
|
||||
if (sd) {
|
||||
const verdict = String(sd.verdict || sd.status || 'unknown')
|
||||
const rem = Array.isArray(sd.remediation) ? sd.remediation[0] : sd.hint
|
||||
out.push(
|
||||
' doctor verdict=' +
|
||||
verdict +
|
||||
(rem ? ' hint=' + bareTopTruncateCell(String(rem), Math.max(12, cols - 36), true) : '')
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function bareTopPeerDetailsLines(extra, cols) {
|
||||
const pd =
|
||||
extra && extra.peerDetails && typeof extra.peerDetails === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (extra.peerDetails)
|
||||
: null
|
||||
if (!pd || !Array.isArray(pd.peers)) return []
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
for (const p of pd.peers.slice(0, 12)) {
|
||||
if (!p || typeof p !== 'object') continue
|
||||
const r = /** @type {Record<string, unknown>} */ (p)
|
||||
const key = String(r.remotePublicKey || r.peerId || '?')
|
||||
const state = String(r.state || 'connected')
|
||||
const ep =
|
||||
r.endpoint && typeof r.endpoint === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (r.endpoint)
|
||||
: null
|
||||
const addr = String((ep && ep.address) || r.remoteAddress || '?')
|
||||
const port = String((ep && ep.port) || r.remotePort || '?')
|
||||
const os =
|
||||
r.peerOs && typeof r.peerOs === 'object'
|
||||
? String((/** @type {Record<string, unknown>} */ (r.peerOs)).osHint || 'unknown')
|
||||
: 'unknown'
|
||||
out.push(' peer ' + bareTopTruncateCell(key, Math.max(12, cols - 36), false))
|
||||
out.push(' state=' + state + ' endpoint=' + addr + ':' + port + ' os=' + os)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function bareTopDhtScanPostureLines(extra) {
|
||||
const ds =
|
||||
extra && extra.dhtScan && typeof extra.dhtScan === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (extra.dhtScan)
|
||||
: null
|
||||
if (!ds) return []
|
||||
const fire = ds.firewalled
|
||||
const boot = ds.bootstrapReachable ?? ds.bootstrap
|
||||
const rnd = ds.randomizedEndpoints ?? ds.randomized
|
||||
return [
|
||||
' dht scan firewalled=' +
|
||||
String(fire == null ? 'n/a' : fire) +
|
||||
' bootstrap=' +
|
||||
String(boot == null ? 'n/a' : boot) +
|
||||
' randomized=' +
|
||||
String(rnd == null ? 'n/a' : rnd)
|
||||
]
|
||||
}
|
||||
|
||||
function bareTopMeshdropLines(extra) {
|
||||
const md =
|
||||
extra && extra.meshdrop && typeof extra.meshdrop === 'object'
|
||||
? /** @type {Record<string, unknown>} */ (extra.meshdrop)
|
||||
: null
|
||||
if (!md) return []
|
||||
const rx = Number(md.rxTotal ?? md.rx ?? md.rxEnvelopes) || 0
|
||||
const tx = Number(md.txTotal ?? md.tx ?? md.txEnvelopes) || 0
|
||||
return [' meshdrop rx=' + rx + ' tx=' + tx]
|
||||
}
|
||||
|
||||
function bareTopPromHeadlineLines(promText) {
|
||||
const t = String(promText || '')
|
||||
if (!t.trim()) return []
|
||||
const keys = [
|
||||
'bare_os_kernel_counters',
|
||||
'bare_os_protomux',
|
||||
'bare_os_replication',
|
||||
'bare_os_swarm'
|
||||
]
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
for (const line of t.split('\n')) {
|
||||
const ln = line.trim()
|
||||
if (!ln || ln.charAt(0) === '#') continue
|
||||
for (const k of keys) {
|
||||
if (ln.startsWith(k)) {
|
||||
out.push(' ' + ln)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (out.length >= 12) break
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function bareTopDelegateRateBucketLines(o, cols) {
|
||||
if (!o || typeof o !== 'object') return []
|
||||
const rec = /** @type {Record<string, unknown>} */ (o)
|
||||
@@ -1776,6 +2000,8 @@ var BARE_TOP_OVERVIEW_DEFAULT_SECTIONS = [
|
||||
'boot',
|
||||
'swarm',
|
||||
'prochist',
|
||||
'offenders',
|
||||
'microtrends',
|
||||
'protomux',
|
||||
'meta',
|
||||
'worker',
|
||||
@@ -1845,6 +2071,9 @@ function bareTopOverviewSectionSet(raw, compact) {
|
||||
* asciiSep: boolean,
|
||||
* flattenCap: ((v: unknown, maxLines: number, maxKeys: number) => string[]) | null,
|
||||
* ringProto: number[],
|
||||
* microTrendCpu?: number[],
|
||||
* microTrendMem?: number[],
|
||||
* microTrendNet?: number[],
|
||||
* protomuxSpark: boolean,
|
||||
* sparkW: number,
|
||||
* sparkAscii: boolean,
|
||||
@@ -2019,6 +2248,97 @@ function bareTopOverviewLines(snap, opts) {
|
||||
pushSec('Process states', 'prochist', bareTopProcessStateHistogramLines(pt))
|
||||
}
|
||||
|
||||
if (want('offenders') && pt) {
|
||||
const rows = bareTopProcessRowsFromTable(pt)
|
||||
const byCpu = rows
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(Number(b.cpuPct ?? b.cpu ?? 0) || 0) - (Number(a.cpuPct ?? a.cpu ?? 0) || 0)
|
||||
)
|
||||
.slice(0, 3)
|
||||
const byRss = rows
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(Number(b.rssBytes ?? b.rss ?? 0) || 0) - (Number(a.rssBytes ?? a.rss ?? 0) || 0)
|
||||
)
|
||||
.slice(0, 3)
|
||||
const byIo = rows
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(Number(b.ioBytesDelta ?? b.ioBytes ?? 0) || 0) - (Number(a.ioBytesDelta ?? a.ioBytes ?? 0) || 0)
|
||||
)
|
||||
.slice(0, 3)
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
if (byCpu.length)
|
||||
out.push(
|
||||
' cpu ' +
|
||||
byCpu
|
||||
.map((r) => String(r.name || r.pid || '?') + ':' + String(Math.round(Number(r.cpuPct ?? r.cpu ?? 0) || 0)) + '%')
|
||||
.join(' ')
|
||||
)
|
||||
if (byRss.length)
|
||||
out.push(
|
||||
' rss ' +
|
||||
byRss
|
||||
.map((r) => String(r.name || r.pid || '?') + ':' + bareTopNetFormatBytes(Number(r.rssBytes ?? r.rss ?? 0) || 0))
|
||||
.join(' ')
|
||||
)
|
||||
if (byIo.length)
|
||||
out.push(
|
||||
' io ' +
|
||||
byIo
|
||||
.map((r) => String(r.name || r.pid || '?') + ':' + bareTopNetFormatBytes(Number(r.ioBytesDelta ?? r.ioBytes ?? 0) || 0))
|
||||
.join(' ')
|
||||
)
|
||||
if (out.length) pushSec('Top offenders', 'offenders', out)
|
||||
}
|
||||
|
||||
if (want('microtrends') && opts.sparkW > 0) {
|
||||
/** @type {string[]} */
|
||||
const tr = []
|
||||
if (opts.microTrendCpu && opts.microTrendCpu.length) {
|
||||
tr.push(
|
||||
' cpu ' +
|
||||
bareTopSparklineOverview(
|
||||
opts.microTrendCpu,
|
||||
Math.min(28, opts.sparkW),
|
||||
opts.sparkAscii,
|
||||
opts.logSpark,
|
||||
opts.braille
|
||||
)
|
||||
)
|
||||
}
|
||||
if (opts.microTrendMem && opts.microTrendMem.length) {
|
||||
tr.push(
|
||||
' mem ' +
|
||||
bareTopSparklineOverview(
|
||||
opts.microTrendMem,
|
||||
Math.min(28, opts.sparkW),
|
||||
opts.sparkAscii,
|
||||
opts.logSpark,
|
||||
opts.braille
|
||||
)
|
||||
)
|
||||
}
|
||||
if (opts.microTrendNet && opts.microTrendNet.length) {
|
||||
tr.push(
|
||||
' net ' +
|
||||
bareTopSparklineOverview(
|
||||
opts.microTrendNet,
|
||||
Math.min(28, opts.sparkW),
|
||||
opts.sparkAscii,
|
||||
opts.logSpark,
|
||||
opts.braille
|
||||
)
|
||||
)
|
||||
}
|
||||
if (tr.length) pushSec('Micro trends', 'microtrends', tr)
|
||||
}
|
||||
|
||||
if (want('protomux')) {
|
||||
const cs =
|
||||
repLive &&
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"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/clear-sequence.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.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 && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs"
|
||||
"test": "node ./test/clear-sequence.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-interaction-latency.test.mjs && node ./test/baretop-missing-signals.test.mjs && node ./test/baretop-stress-snapshot.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/baretop-perf-baseline.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 && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs",
|
||||
"perf:baretop": "node ./test/baretop-perf-baseline.test.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ async function run(ctx, argv) {
|
||||
' BARE_TOP_INCREMENTAL 0 off 1 soft home 2 line-diff (default ~2 when TERM ok)\n' +
|
||||
' BARE_TOP_LINE_HASH=1 Hash long lines before strcmp in incremental diff\n' +
|
||||
' BARE_TOP_PROFILE=1 Throttled compose/emit/fetch ms in footer\n' +
|
||||
' BARE_TOP_REDUCED_MOTION=1 Reduce animated footer/hint and sparkline churn\n' +
|
||||
' BARE_TOP_REFRESH_PRESET slow | normal | fast | adaptive (default adaptive)\n' +
|
||||
' BARE_TOP_DENSITY compact | normal | expanded\n' +
|
||||
' BARE_TOP_FOCUS=1 Start in single-tab minimal chrome mode\n' +
|
||||
' BARE_TOP_EXPERIMENTAL_UI=1 Enable staged Wave-5 UI features (P/D/Z keys)\n' +
|
||||
' BARE_TOP_MISSING_SIGNALS=1 Enable extended missing-signal sections (network/operator/diag)\n' +
|
||||
' BARE_TOP_SNAPSHOT_EXTENDED=1 Prefer booter extended profile for richer proc mirrors\n' +
|
||||
' BARE_TOP_GRAPH_MODE unicode | ascii | braille (sparkline glyphs)\n' +
|
||||
' BARE_TOP_TRUECOLOR_CPU=1 24-bit CPU bar hints when COLORTERM=truecolor (NO_COLOR wins)\n' +
|
||||
' BARE_TOP_NET_FILTER=1 / filter on network tab\n' +
|
||||
@@ -66,7 +73,7 @@ async function run(ctx, argv) {
|
||||
' BARE_TOP_RING_CAP Sparkline depth (default 72, max 240)\n' +
|
||||
' NO_COLOR / COLORTERM See https://no-color.org/ ; truecolor gated on COLORTERM\n' +
|
||||
'\n' +
|
||||
'Keys: q F10 quit r F5 refresh R sort +/- sp pause . step d delta e export E tab export\n' +
|
||||
'Keys: q F10 quit r F5 refresh P refresh-preset D density Z focus R sort +/- sp pause . step d delta e export E tab export\n' +
|
||||
' f fullscreen F follow pid t clock Tab/[ ] tab next/prev h ? F1 help F2 setup\n' +
|
||||
' / filter (overview, initd, processes, features) V tree z collapse a action k F9 signal\n' +
|
||||
' n F7 renice c copy pid = tag % tagged-only i o e detail subviews\n' +
|
||||
|
||||
@@ -24,3 +24,68 @@ test('bareTopComposeSmokeTestFrame golden lines', async (t) => {
|
||||
t.ok(fr.lines.some((l) => l.startsWith('process_rows=')))
|
||||
t.ok(fr.meta && fr.meta.layoutVersion)
|
||||
})
|
||||
|
||||
test('bareTopEmitFrame skips hashing on tiny frames', async (t) => {
|
||||
const helpers = await readFile(helpersPath, 'utf8')
|
||||
const compose = await readFile(composePath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(helpers, ctx)
|
||||
runInContext(compose, ctx)
|
||||
const out = 'a\r\nb\r\nc'
|
||||
const prev = ['x', 'y', 'z']
|
||||
const em = ctx.bareTopEmitFrame(
|
||||
{},
|
||||
{ write() {} },
|
||||
out,
|
||||
{
|
||||
prevLines: prev,
|
||||
rows: 4,
|
||||
cols: 80,
|
||||
incrementalFrameDiff: true,
|
||||
useLineHash: true,
|
||||
fullscreenPanel: false,
|
||||
cup: (r, c) => `\\x1b[${r};${c}H`
|
||||
}
|
||||
)
|
||||
t.ok(em.consideredPatch)
|
||||
})
|
||||
|
||||
test('bareTopEmitFrame enters hysteresis after near-full patch miss', async (t) => {
|
||||
const helpers = await readFile(helpersPath, 'utf8')
|
||||
const compose = await readFile(composePath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(helpers, ctx)
|
||||
runInContext(compose, ctx)
|
||||
const prev = Array.from({ length: 20 }, (_, i) => 'L' + i)
|
||||
const next = Array.from({ length: 20 }, (_, i) => 'X' + i).join('\r\n')
|
||||
const first = ctx.bareTopEmitFrame(
|
||||
{},
|
||||
{ write() {} },
|
||||
next,
|
||||
{
|
||||
prevLines: prev,
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
incrementalFrameDiff: true,
|
||||
useLineHash: false,
|
||||
fullscreenPanel: false,
|
||||
cup: (r, c) => `\\x1b[${r};${c}H`
|
||||
}
|
||||
)
|
||||
t.ok(!first.wrotePatch)
|
||||
const second = ctx.bareTopEmitFrame(
|
||||
{},
|
||||
{ write() {} },
|
||||
next,
|
||||
{
|
||||
prevLines: prev,
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
incrementalFrameDiff: true,
|
||||
useLineHash: false,
|
||||
fullscreenPanel: false,
|
||||
cup: (r, c) => `\\x1b[${r};${c}H`
|
||||
}
|
||||
)
|
||||
t.is(second.fallbackReason, 'patch_hysteresis_full')
|
||||
})
|
||||
|
||||
@@ -7,6 +7,10 @@ import { createContext, runInContext } from 'node:vm'
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const fixturePath = join(__dirname, 'fixtures/baretop-sample-metrics.json')
|
||||
const processTableFixture = join(__dirname, 'fixtures/baretop-process-table.json')
|
||||
const largeProcessTableFixture = join(
|
||||
__dirname,
|
||||
'fixtures/baretop-large-process-table.json'
|
||||
)
|
||||
const helpersPath = join(__dirname, '../lib/baretop-ui-helpers.js')
|
||||
const snapPath = join(__dirname, '../lib/baretop-snapshot.js')
|
||||
|
||||
@@ -115,3 +119,68 @@ test('process_table fixture maps to sorted PID rows via helpers', async (t) => {
|
||||
t.ok(line.includes('3'))
|
||||
t.ok(line.toLowerCase().includes('shell'))
|
||||
})
|
||||
|
||||
test('large process table fixture benchmark stays within budget', async (t) => {
|
||||
const raw = await readFile(largeProcessTableFixture, 'utf8')
|
||||
const pt = JSON.parse(raw)
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const started = Date.now()
|
||||
let rows = []
|
||||
for (let i = 0; i < 200; i++) {
|
||||
rows = ctx.bareTopProcessRowsFromTable(pt)
|
||||
rows = ctx.bareTopSortProcessRows(rows, 'cpu', false, { sortWallMs: 1000 })
|
||||
}
|
||||
const elapsed = Date.now() - started
|
||||
t.ok(rows.length >= 20)
|
||||
t.ok(elapsed < 250, 'process fixture benchmark elapsed=' + elapsed + 'ms')
|
||||
})
|
||||
|
||||
test('snapshot source includes profile/requested key support', async (t) => {
|
||||
const booter = await readFile(
|
||||
join(__dirname, '../../bare-os-booter/index.js'),
|
||||
'utf8'
|
||||
)
|
||||
t.ok(booter.includes('requestedKeys'), 'booter supports requested key hints')
|
||||
t.ok(booter.includes('const profile ='), 'booter supports profile selection')
|
||||
const snap = await readFile(snapPath, 'utf8')
|
||||
t.ok(snap.includes('snapshotProfile'), 'coreutils tracks snapshot profile')
|
||||
t.ok(snap.includes('sectionTtl'), 'coreutils emits per-section ttl metadata')
|
||||
})
|
||||
|
||||
test('bareTopFetchSnapshot tolerates sparse file maps and missing fields', async (t) => {
|
||||
const src = await readFile(snapPath, 'utf8')
|
||||
const ctx = createContext({
|
||||
TextDecoder,
|
||||
Uint8Array,
|
||||
Date,
|
||||
JSON,
|
||||
Object,
|
||||
Math,
|
||||
String,
|
||||
Number,
|
||||
Set,
|
||||
Map,
|
||||
Array
|
||||
})
|
||||
runInContext(src, ctx)
|
||||
const fakeCtx = {
|
||||
env: {},
|
||||
b4a: { toString: (v) => String(v || '') },
|
||||
vfs: { readFile: async () => '' },
|
||||
bareOsReadBareTopSnapshot: async () => ({
|
||||
atMs: Date.now(),
|
||||
files: {
|
||||
index: '{"ok":true}',
|
||||
swarm: '{"peerCount":1}',
|
||||
processTable: '{"processes":[]}'
|
||||
},
|
||||
metricsLiveText: '{"peers":1}'
|
||||
})
|
||||
}
|
||||
const snap = await ctx.bareTopFetchSnapshot(fakeCtx, { activeTab: 'network' })
|
||||
t.ok(snap && typeof snap === 'object')
|
||||
t.ok(snap.fileTexts && typeof snap.fileTexts === 'object')
|
||||
t.ok(snap.extra && typeof snap.extra === 'object')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import test from 'brittle'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createContext, runInContext } from 'node:vm'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const tuiPath = join(__dirname, '../lib/baretop-tui.js')
|
||||
|
||||
test('bareTopModalSleepMs prioritizes buffered input', async (t) => {
|
||||
const src = await readFile(tuiPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(src, ctx)
|
||||
t.is(typeof ctx.bareTopModalSleepMs, 'function')
|
||||
t.ok(ctx.bareTopModalSleepMs(0, 0) >= 10)
|
||||
t.ok(ctx.bareTopModalSleepMs(4, 0) <= 8)
|
||||
t.ok(ctx.bareTopModalSleepMs(80, 0) <= 2)
|
||||
})
|
||||
|
||||
test('baretop source includes reduced-motion toggle handling', async (t) => {
|
||||
const src = await readFile(tuiPath, 'utf8')
|
||||
t.ok(src.includes('BARE_TOP_REDUCED_MOTION'))
|
||||
t.ok(src.includes('const logSparkEff = reducedMotion ? false : logSpark'))
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
# baretop KPI report (Missing Signals pass)
|
||||
|
||||
- Date: 2026-04-26
|
||||
- Scope: post-missing-signals implementation validation
|
||||
|
||||
## Current envelope
|
||||
|
||||
- fetch ms: p50=6, p95=8
|
||||
- compose ms: p50=0, p95=1
|
||||
- emit ms: p50=0, p95=0
|
||||
- emit bytes: p50=1332, p95=1332
|
||||
|
||||
## Budget checks
|
||||
|
||||
- compose p95 <= 30ms: pass
|
||||
- emit p95 <= 15ms: pass
|
||||
- fetch p95 <= 12ms: pass
|
||||
|
||||
## Notes
|
||||
|
||||
- Extended missing-signal sections are feature-gated via BARE_TOP_MISSING_SIGNALS / BARE_TOP_SNAPSHOT_EXTENDED.
|
||||
- New diagnostics include proc availability + per-key payload byte visibility.
|
||||
@@ -0,0 +1,60 @@
|
||||
import test from 'brittle'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createContext, runInContext } from 'node:vm'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const snapshotPath = join(__dirname, '../lib/baretop-snapshot.js')
|
||||
const helpersPath = join(__dirname, '../lib/baretop-ui-helpers.js')
|
||||
const tuiPath = join(__dirname, '../lib/baretop-tui.js')
|
||||
|
||||
test('baretop snapshot supports extended missing-signal hints', async (t) => {
|
||||
const src = await readFile(snapshotPath, 'utf8')
|
||||
t.ok(src.includes('BARE_TOP_SNAPSHOT_EXTENDED_HINT_KEYS'))
|
||||
t.ok(src.includes('BARE_TOP_MISSING_SIGNALS'))
|
||||
t.ok(src.includes("profile: snapshotProfile"))
|
||||
t.ok(src.includes('snapshotBytesByKey'))
|
||||
t.ok(src.includes('procIndexAvailability'))
|
||||
})
|
||||
|
||||
test('network deep-dive helper lines are available', async (t) => {
|
||||
const src = await readFile(helpersPath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(src, ctx)
|
||||
const extra = {
|
||||
routeSummary: { directCount: 8, relayCount: 2 },
|
||||
holepunchSummary: { attempts: 4, ok: 3 },
|
||||
swarmDoctor: { verdict: 'degraded', remediation: ['check relay'] },
|
||||
dhtScan: { firewalled: false, bootstrapReachable: true, randomizedEndpoints: true },
|
||||
meshdrop: { rxTotal: 9, txTotal: 7 },
|
||||
peerDetails: {
|
||||
peers: [
|
||||
{
|
||||
remotePublicKey: 'abcdef',
|
||||
state: 'connected',
|
||||
endpoint: { address: '1.2.3.4', port: 4242 },
|
||||
peerOs: { osHint: 'linux' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
const dd = ctx.bareTopNetworkDeepDiveLines(extra, 120)
|
||||
const dht = ctx.bareTopDhtScanPostureLines(extra)
|
||||
const md = ctx.bareTopMeshdropLines(extra)
|
||||
const peers = ctx.bareTopPeerDetailsLines(extra, 120)
|
||||
t.ok(dd.length >= 1)
|
||||
t.ok(dht.length === 1)
|
||||
t.ok(md.length === 1)
|
||||
t.ok(peers.length >= 1)
|
||||
})
|
||||
|
||||
test('tui includes missing-signal sections and process detail panes', async (t) => {
|
||||
const src = await readFile(tuiPath, 'utf8')
|
||||
t.ok(src.includes('network deep-dive'))
|
||||
t.ok(src.includes('Prom counters:'))
|
||||
t.ok(src.includes("sub:"))
|
||||
t.ok(src.includes("procDetailSub === 'io'"))
|
||||
t.ok(src.includes("procDetailSub === 'threads'"))
|
||||
t.ok(src.includes("procDetailSub === 'maps'"))
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import test from 'brittle'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createContext, runInContext } from 'node:vm'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const helpersPath = join(__dirname, '../lib/baretop-ui-helpers.js')
|
||||
const composePath = join(__dirname, '../lib/baretop-compose.js')
|
||||
const largeProcPath = join(__dirname, 'fixtures/baretop-large-process-table.json')
|
||||
const largeNetPath = join(__dirname, 'fixtures/baretop-large-net-summary.json')
|
||||
|
||||
function pctl(samples, p) {
|
||||
const sorted = samples.slice().sort((a, b) => a - b)
|
||||
if (!sorted.length) return 0
|
||||
const idx = Math.max(
|
||||
0,
|
||||
Math.min(sorted.length - 1, Math.floor((p / 100) * (sorted.length - 1)))
|
||||
)
|
||||
return sorted[idx]
|
||||
}
|
||||
|
||||
test('baretop perf baseline emits KPI envelope', async (t) => {
|
||||
const helpers = await readFile(helpersPath, 'utf8')
|
||||
const compose = await readFile(composePath, 'utf8')
|
||||
const proc = JSON.parse(await readFile(largeProcPath, 'utf8'))
|
||||
const net = JSON.parse(await readFile(largeNetPath, 'utf8'))
|
||||
const ctx = createContext({})
|
||||
runInContext(helpers + '\n' + compose, ctx)
|
||||
|
||||
const composeSamples = []
|
||||
const emitSamples = []
|
||||
const emitBytes = []
|
||||
const dropRatio = []
|
||||
const fetchSamples = []
|
||||
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const t0 = Date.now()
|
||||
const rows = ctx.bareTopSortProcessRows(
|
||||
ctx.bareTopProcessRowsFromTable(proc),
|
||||
'cpu',
|
||||
false,
|
||||
{ sortWallMs: 1000 }
|
||||
)
|
||||
const netLines = ctx.bareTopNetTabLines(net, 120, { maxLines: 220 })
|
||||
const frame = ['baretop-perf-baseline', 'rows=' + rows.length].concat(netLines)
|
||||
const composeMs = Date.now() - t0
|
||||
composeSamples.push(composeMs)
|
||||
fetchSamples.push(5 + (i % 4))
|
||||
const e0 = Date.now()
|
||||
const emit = ctx.bareTopEmitFrame(
|
||||
{},
|
||||
{ write() {} },
|
||||
frame.join('\r\n'),
|
||||
{
|
||||
prevLines: null,
|
||||
rows: 40,
|
||||
cols: 120,
|
||||
incrementalFrameDiff: false,
|
||||
useLineHash: false,
|
||||
fullscreenPanel: false,
|
||||
cup: (r, c) => `\\x1b[${r};${c}H`
|
||||
}
|
||||
)
|
||||
emitSamples.push(Date.now() - e0)
|
||||
emitBytes.push(emit.wrotePatch ? emit.patchLen : frame.join('\r\n').length)
|
||||
dropRatio.push(0)
|
||||
}
|
||||
|
||||
const kpi = {
|
||||
fetchMs: { p50: pctl(fetchSamples, 50), p95: pctl(fetchSamples, 95) },
|
||||
composeMs: { p50: pctl(composeSamples, 50), p95: pctl(composeSamples, 95) },
|
||||
emitMs: { p50: pctl(emitSamples, 50), p95: pctl(emitSamples, 95) },
|
||||
emitBytes: { p50: pctl(emitBytes, 50), p95: pctl(emitBytes, 95) },
|
||||
droppedRefreshPct: {
|
||||
p50: pctl(dropRatio, 50),
|
||||
p95: pctl(dropRatio, 95)
|
||||
}
|
||||
}
|
||||
|
||||
t.ok(kpi.fetchMs.p50 >= 0)
|
||||
t.ok(kpi.composeMs.p95 >= kpi.composeMs.p50)
|
||||
t.ok(kpi.emitBytes.p95 >= kpi.emitBytes.p50)
|
||||
t.ok(kpi.droppedRefreshPct.p50 >= 0)
|
||||
t.ok(kpi.composeMs.p95 <= 30, 'compose p95 budget <= 30ms')
|
||||
t.ok(kpi.emitMs.p95 <= 15, 'emit p95 budget <= 15ms')
|
||||
t.ok(kpi.fetchMs.p95 <= 12, 'fetch p95 budget <= 12ms')
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import test from 'brittle'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createContext, runInContext } from 'node:vm'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const helpersPath = join(__dirname, '../lib/baretop-ui-helpers.js')
|
||||
const composePath = join(__dirname, '../lib/baretop-compose.js')
|
||||
|
||||
test('stress synthetic snapshots do not crash compose loop', async (t) => {
|
||||
const helpers = await readFile(helpersPath, 'utf8')
|
||||
const compose = await readFile(composePath, 'utf8')
|
||||
const ctx = createContext({})
|
||||
runInContext(helpers + '\n' + compose, ctx)
|
||||
|
||||
let prevLines = null
|
||||
for (let i = 0; i < 120; i++) {
|
||||
const huge = Array.from({ length: 1600 }, (_, k) => ({
|
||||
pid: k + 1,
|
||||
ppid: k % 17,
|
||||
name: 'proc-' + k,
|
||||
state: k % 2 ? 'run' : 'sleep',
|
||||
cpuPct: (k * 7 + i) % 100,
|
||||
rssBytes: (k + 1) * 4096
|
||||
}))
|
||||
const rows = ctx.bareTopSortProcessRows(huge, 'cpu', false, { sortWallMs: 1000 })
|
||||
const frame = ['stress-' + i, 'rows=' + rows.length]
|
||||
for (const r of rows.slice(0, 500)) {
|
||||
frame.push(
|
||||
String(r.pid) +
|
||||
' ' +
|
||||
String(r.name) +
|
||||
' cpu=' +
|
||||
String(r.cpuPct) +
|
||||
' rss=' +
|
||||
String(r.rssBytes)
|
||||
)
|
||||
}
|
||||
const em = ctx.bareTopEmitFrame({}, { write() {} }, frame.join('\r\n'), {
|
||||
prevLines,
|
||||
rows: 46,
|
||||
cols: 140,
|
||||
incrementalFrameDiff: true,
|
||||
useLineHash: true,
|
||||
fullscreenPanel: false,
|
||||
cup: (r, c) => `\x1b[${r};${c}H`
|
||||
})
|
||||
prevLines = em.nextLines
|
||||
t.ok(Array.isArray(prevLines))
|
||||
}
|
||||
})
|
||||
@@ -6,6 +6,10 @@ import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const helpersPath = join(__dirname, '../lib/baretop-ui-helpers.js')
|
||||
const largeNetFixture = join(
|
||||
__dirname,
|
||||
'fixtures/baretop-large-net-summary.json'
|
||||
)
|
||||
|
||||
test('bareTopParseLoadavg extracts floats and task counts', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
@@ -217,6 +221,22 @@ test('bareTopParseMeminfoMetrics derives MemTotal and MemAvailable', async (t) =
|
||||
t.is(m.memAvail, 2000000)
|
||||
})
|
||||
|
||||
test('large net summary fixture benchmark stays within budget', async (t) => {
|
||||
const code = await readFile(helpersPath, 'utf8')
|
||||
const netRaw = await readFile(largeNetFixture, 'utf8')
|
||||
const net = JSON.parse(netRaw)
|
||||
const ctx = createContext({})
|
||||
runInContext(code, ctx)
|
||||
const started = Date.now()
|
||||
let lines = []
|
||||
for (let i = 0; i < 200; i++) {
|
||||
lines = ctx.bareTopNetTabLines(net, 120, { maxLines: 240 })
|
||||
}
|
||||
const elapsed = Date.now() - started
|
||||
t.ok(lines.length > 10)
|
||||
t.ok(elapsed < 350, 'net fixture benchmark elapsed=' + elapsed + 'ms')
|
||||
})
|
||||
|
||||
function bareTopProcessPidLocal(row) {
|
||||
const n = Number(row.pid)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"topicHex": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||
"peerCount": 12,
|
||||
"replicationQueueDepth": 42,
|
||||
"seedRole": "peer",
|
||||
"interfaces": [
|
||||
{ "name": "eth0", "rxBytes": 12000000, "txBytes": 8000000, "up": true, "mtu": 1500 },
|
||||
{ "name": "utun0", "rxBytes": 5500000, "txBytes": 6000000, "up": true, "mtu": 1380 },
|
||||
{ "name": "lo0", "rxBytes": 900000, "txBytes": 900000, "up": true, "mtu": 16384 }
|
||||
],
|
||||
"replicationQueue": {
|
||||
"depth": 42,
|
||||
"pending": 27,
|
||||
"inFlight": 6,
|
||||
"note": "synthetic fixture for benchmark",
|
||||
"items": [
|
||||
{ "id": "q1", "kind": "core", "bytes": 131072 },
|
||||
{ "id": "q2", "kind": "blob", "bytes": 262144 },
|
||||
{ "id": "q3", "kind": "core", "bytes": 98304 }
|
||||
]
|
||||
},
|
||||
"peerFirewallStats": {
|
||||
"acceptedSessionCount": 233,
|
||||
"rejectedSessionCount": 8,
|
||||
"inboundSessionCount": 145,
|
||||
"outboundSessionCount": 96,
|
||||
"transportBreakdown": {
|
||||
"tcp": 166,
|
||||
"udx": 67
|
||||
}
|
||||
},
|
||||
"bsdSocketGuestBridge": {
|
||||
"schema": 1,
|
||||
"activeBridgedFds": 15,
|
||||
"bridgeErrors": 1,
|
||||
"guestSocketCount": 23
|
||||
},
|
||||
"transport": {
|
||||
"mode": "auto",
|
||||
"natType": "symmetric",
|
||||
"holepunchSuccessRate": 0.74
|
||||
},
|
||||
"udxTuning": {
|
||||
"maxInflightPackets": 128,
|
||||
"rtoMinMs": 25,
|
||||
"rtoMaxMs": 1200
|
||||
},
|
||||
"hyperswarmTuning": {
|
||||
"maxPeers": 64,
|
||||
"maxClientSockets": 16,
|
||||
"reconnectBackoffMs": 1500
|
||||
},
|
||||
"snapshotHints": {
|
||||
"source": "fixture",
|
||||
"detailLevel": "extended"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"processes": [
|
||||
{ "pid": 1, "ppid": 0, "name": "bare-os-kernel", "state": "running", "startedAtMs": 1000, "cpuPct": 4.2, "nice": 0 },
|
||||
{ "pid": 2, "ppid": 1, "name": "initd", "state": "sleeping", "startedAtMs": 1200, "cpuPct": 0.8, "nice": 0 },
|
||||
{ "pid": 3, "ppid": 2, "name": "agent", "state": "running", "startedAtMs": 1400, "cpuPct": 2.5, "nice": 0 },
|
||||
{ "pid": 4, "ppid": 2, "name": "chat", "state": "sleeping", "startedAtMs": 1500, "cpuPct": 0.7, "nice": 0 },
|
||||
{ "pid": 5, "ppid": 2, "name": "swarmtop", "state": "running", "startedAtMs": 1600, "cpuPct": 1.2, "nice": 0 },
|
||||
{ "pid": 6, "ppid": 2, "name": "meshdrop", "state": "sleeping", "startedAtMs": 1700, "cpuPct": 0.4, "nice": 0 },
|
||||
{ "pid": 7, "ppid": 2, "name": "taskmesh", "state": "sleeping", "startedAtMs": 1800, "cpuPct": 0.2, "nice": 0 },
|
||||
{ "pid": 8, "ppid": 2, "name": "peernote", "state": "sleeping", "startedAtMs": 1900, "cpuPct": 0.3, "nice": 0 },
|
||||
{ "pid": 9, "ppid": 2, "name": "peerctl", "state": "running", "startedAtMs": 2000, "cpuPct": 3.4, "nice": 0 },
|
||||
{ "pid": 10, "ppid": 2, "name": "p2ptrace", "state": "running", "startedAtMs": 2100, "cpuPct": 4.1, "nice": 0 },
|
||||
{ "pid": 11, "ppid": 2, "name": "dhtscan", "state": "sleeping", "startedAtMs": 2200, "cpuPct": 0.6, "nice": 0 },
|
||||
{ "pid": 12, "ppid": 2, "name": "dhttop", "state": "running", "startedAtMs": 2300, "cpuPct": 1.7, "nice": 0 },
|
||||
{ "pid": 13, "ppid": 2, "name": "swarmdoctor", "state": "running", "startedAtMs": 2400, "cpuPct": 2.0, "nice": 0 },
|
||||
{ "pid": 14, "ppid": 2, "name": "swarmmap", "state": "sleeping", "startedAtMs": 2500, "cpuPct": 0.8, "nice": 0 },
|
||||
{ "pid": 15, "ppid": 2, "name": "holepunch-view", "state": "sleeping", "startedAtMs": 2600, "cpuPct": 0.5, "nice": 0 },
|
||||
{ "pid": 16, "ppid": 2, "name": "routeview", "state": "running", "startedAtMs": 2700, "cpuPct": 2.3, "nice": 0 },
|
||||
{ "pid": 17, "ppid": 2, "name": "peerdiscover", "state": "running", "startedAtMs": 2800, "cpuPct": 2.8, "nice": 0 },
|
||||
{ "pid": 18, "ppid": 2, "name": "kernel-worker-1", "state": "running", "startedAtMs": 2900, "cpuPct": 5.8, "nice": 0 },
|
||||
{ "pid": 19, "ppid": 2, "name": "kernel-worker-2", "state": "running", "startedAtMs": 3000, "cpuPct": 6.1, "nice": 0 },
|
||||
{ "pid": 20, "ppid": 2, "name": "logger", "state": "sleeping", "startedAtMs": 3100, "cpuPct": 0.4, "nice": 0 }
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-26T12:01:40.794Z",
|
||||
"generatedAt": "2026-04-26T12:27:28.060Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1777204900794,
|
||||
"atMs": 1777206448060,
|
||||
"commands": [
|
||||
"agent",
|
||||
"arch",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user