灵符lingfu
L

ling-sys

↓ 0 downloads

System 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).

Windows · x86-64macOS · Intel x86-64Linux · x86-64LingOS · x86-64LingOS · Pi 5 ARM64Web · WASM
lingfu install ling-sys
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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
}

← Back to index