L
ling-sys
↓ 0 downloadsSystem activity and performance monitor in the Ling Design System — dials, meshes, drums and crosshatch for CPU, GPU, memory, disk, network, board, processes and audio, with every subsystem given a voice. A LingOS app for rpi5: live board/CPU/memory/process stats from the kernel (stats-lingos.ling).
lingfu install ling-sysCode / Files (13)
stats.ling
# ============================================================
# stats.ling — feeds the instruments. Ling has no sensor
# builtins, so a PowerShell collector writes stats.txt once a
# second and info.txt once; this module launches it, reads the
# files, and parses key=value lines into the global store as
# s_<key> (numbers) and s_proc0..s_proc5 (name|cpu|mem strings).
# Static facts land as i_<key> strings.
# ============================================================
fn stats_start() {
# tm_sys_os() answers on a desktop host ("windows" / "linux" / "macos") and not
# at all on LingOS (nothing comes back), which is how the two are told apart
bind osn = tm_sys_os()
if num_str(osn) == "()" {
# LingOS: the kernel's own sources, in stats-lingos.ling
st_set("os_win", 0.0)
lstats_info()
return 0
}
# a desktop host: the host runtime's draw_arc and audio_blip exist
st_set("os_win", 1.0)
if osn == "windows" {
bind id = proc_start("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "collector.ps1"], "collector.log")
if num_str(id) == "()" {
# no proc_start at all: this was built by a ling without the proc (or web)
# feature. A small .bat starts the same two collectors, hidden.
tm_sys_run(absolute_path("launch-collectors.bat"))
} else {
st_set("collector_id", id)
bind id2 = proc_start("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "collector-audio.ps1"], "collector-audio.log")
st_set("collector_aid", id2)
}
}
# macOS and Linux have no collector yet, so their instruments read zero
return 0
}
fn stats_stop() {
bind id = st_get("collector_id", 0.0 - 1.0)
if id >= 0.0 { proc_kill(id) }
bind id2 = st_get("collector_aid", 0.0 - 1.0)
if id2 >= 0.0 { proc_kill(id2) }
return 0
}
# fast audio meters: read every few frames for VU-like response
fn stats_audio(t) {
if t - st_get("aud_at", 0.0 - 9.0) < 0.07 { return 0 }
st_set("aud_at", t)
if file_exists("audio.txt") {
bind raw = read_file("audio.txt")
bind lines = str_split(raw, "
")
bind n = len(lines)
bind i = 0
while i < n {
bind kv = str_split(str_trim(list_get(lines, i)), "=")
if len(kv) >= 2 {
st_set("s_aud_" + list_get(kv, 0), to_number(list_get(kv, 1)))
}
bind i = i + 1
}
}
return 0
}
fn stats_info() {
if st_get("info_done", 0.0) == 1.0 { return 0 }
if file_exists("info.txt") {
bind raw = read_file("info.txt")
if len(raw) > 20 {
bind lines = str_split(raw, "
")
bind n = len(lines)
bind i = 0
while i < n {
bind ln = str_trim(list_get(lines, i))
bind kv = str_split(ln, "=")
if len(kv) >= 2 {
st_set("i_" + list_get(kv, 0), str_trim(list_get(kv, 1)))
}
bind i = i + 1
}
# complete only once the device list (the last section) is in
if st_get("i_snd_n", "") != "" {
st_set("info_done", 1.0)
}
}
}
return 0
}
# ---------- rolling history, one entry per collector tick ----------
fn hist_push(name, v) {
bind l = st_get("h_" + name, list_new())
bind l = list_push(l, v)
bind n = len(l)
if n > 96 {
bind nl = list_new()
bind i = n - 96
while i < n {
bind nl = list_push(nl, list_get(l, i))
bind i = i + 1
}
bind l = nl
}
st_set("h_" + name, l)
return 0
}
fn hist_tick(t) {
hist_push("cpu", st_get("s_cpu_total", 0.0))
hist_push("gpu", st_get("s_gpu_load", 0.0))
bind mt = st_get("s_mem_total", 1.0)
hist_push("mem", st_get("s_mem_used", 0.0) / mt * 100.0)
hist_push("rx", log10(1.0 + st_get("s_net_rx", 0.0)))
hist_push("tx", log10(1.0 + st_get("s_net_tx", 0.0)))
hist_push("rd", log10(1.0 + st_get("s_disk_read", 0.0)))
hist_push("wr", log10(1.0 + st_get("s_disk_write", 0.0)))
hist_push("cs", log10(1.0 + st_get("s_cswitch", 0.0)))
hist_push("flt", log10(1.0 + st_get("s_mem_faults", 0.0)))
hist_push("aout", st_get("s_aud_out", 0.0))
hist_push("ain", st_get("s_aud_in", 0.0))
# session byte totals, integrated over real tick time
bind prev = st_get("tick_t", t)
bind dtk = t - prev
if dtk < 0.2 { bind dtk = 1.0 }
if dtk > 5.0 { bind dtk = 5.0 }
st_set("tick_t", t)
st_set("tot_rx", st_get("tot_rx", 0.0) + st_get("s_net_rx", 0.0) * dtk)
st_set("tot_tx", st_get("tot_tx", 0.0) + st_get("s_net_tx", 0.0) * dtk)
st_set("tot_rd", st_get("tot_rd", 0.0) + st_get("s_disk_read", 0.0) * dtk)
st_set("tot_wr", st_get("tot_wr", 0.0) + st_get("s_disk_write", 0.0) * dtk)
return 0
}
# true when this key keeps its value as a string
# (proc0..proc5, dsk0..dsk3, drv0..drv3)
fn stat_is_str(k) {
if len(k) == 4 {
bind p3 = str_slice(k, 0, 3)
if p3 == "dsk" { return 1 }
if p3 == "drv" { return 1 }
return 0
}
if len(k) != 5 { return 0 }
if str_slice(k, 0, 4) != "proc" { return 0 }
if k == "procs" { return 0 }
return 1
}
fn stats_poll(t) {
if st_get("os_win", 1.0) < 1.0 {
lstats_poll(t)
return 0
}
stats_info()
if t - st_get("poll_at", 0.0 - 9.0) < 0.45 { return 0 }
st_set("poll_at", t)
if file_exists("stats.txt") {
bind raw = read_file("stats.txt")
if len(raw) > 10 {
if raw != st_get("poll_raw", "") {
st_set("poll_raw", raw)
bind oldprocs = st_get("s_procs", 0.0)
bind lines = str_split(raw, "
")
bind n = len(lines)
bind i = 0
while i < n {
bind ln = str_trim(list_get(lines, i))
if len(ln) > 2 {
bind kv = str_split(ln, "=")
if len(kv) >= 2 {
bind k = list_get(kv, 0)
bind v = str_trim(list_get(kv, 1))
if stat_is_str(k) == 1 {
st_set("s_" + k, v)
} else {
st_set("s_" + k, to_number(v))
}
}
}
bind i = i + 1
}
st_set("stat_ticks", st_get("stat_ticks", 0.0) + 1.0)
hist_tick(t)
# events for the sound engine
bind newprocs = st_get("s_procs", 0.0)
if oldprocs > 0.0 {
if newprocs != oldprocs {
st_set("ev_proc", abs(newprocs - oldprocs))
}
}
}
}
}
return 0
}