From 155fc661cb921d9be122691019d7fd25137aa645 Mon Sep 17 00:00:00 2001 From: FAZuH Date: Sat, 5 Sep 2026 03:16:46 +0700 Subject: [PATCH 1/4] feat: add the rssprobe RSS sampler to the debug probes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hunting the daemon memory regression needs a series, not a btop glance: scripts/rssprobe.sh samples VmRSS plus the VmHWM high-water mark of a PID on an interval into a CSV, with labeled scenario markers appended from a second shell (the mark subcommand). RSS far below HWM means freed-but-retained memory; RSS at HWM means real growth — the doc explains reading that pair and carries the runbook for the hyprlayd scenario series (idle, hover-storm, VC churn). While in docs/dev/debug-probes.md, fix two stale adapter paths (src/adapters/ipc.rs and src/adapters/discord.rs moved under src/daemon/adapters/). --- docs/dev/debug-probes.md | 65 ++++++++++++++++++++++++----- scripts/rssprobe.sh | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 10 deletions(-) create mode 100755 scripts/rssprobe.sh diff --git a/docs/dev/debug-probes.md b/docs/dev/debug-probes.md index bafe37c..f7c8e0a 100644 --- a/docs/dev/debug-probes.md +++ b/docs/dev/debug-probes.md @@ -1,14 +1,16 @@ # Debug probes -Two throwaway-style probes dump raw Discord RPC traffic. They do not +Three probes live here. Two dump raw Discord RPC traffic; they do not import any crate code — they speak the wire protocols directly, so they -keep working (and stay truthful) even when the adapters change. +keep working (and stay truthful) even when the adapters change. The +third is a plain shell sampler for RSS-hunting on a process that does +not look like a leak in the code but does in btop. -They live in their own mini-crate under `scripts/`, deliberately separate -from the main package, so they never build with or get installed by -`hyprlay`. Their targets are examples, not bins: that keeps the repo's -`cargo install --git` scan at exactly one binary package, so bare -installs work. Run them from that directory: +The two Discord probes live in their own mini-crate under `scripts/`, +deliberately separate from the main package, so they never build with or +get installed by `hyprlay`. Their targets are examples, not bins: that +keeps the repo's `cargo install --git` scan at exactly one binary +package, so bare installs work. Run them from that directory: ```sh cd scripts @@ -16,12 +18,55 @@ cargo run --example ipcprobe # unix-socket IPC probe (current transport) cargo run --example wsprobe # historical websocket bridge probe ``` +## Daemon memory sampler (`rssprobe.sh`) + +`scripts/rssprobe.sh` samples one process's memory every couple of +seconds and appends one CSV row per interval: +`epoch,timestamp,rss_kb,hwm_kb,note`. It reads `/proc//status` +directly — VmRSS plus the peak VmHWM — so every row shows both the +live footprint and the historical high-water mark. That pair is the +diagnosis signal: + +- **RSS at or near HWM** — the process is at its historical peak; + what you are watching is real growth. +- **RSS well below HWM** — memory was freed but not returned to the + OS (allocator retention). The interesting question becomes *what + spiked it earlier*, not *what is growing now*. + +Usage — sampler in one terminal, markers from another while you +work: + +```sh +scripts/rssprobe.sh # sample `pidof hyprlayd` every 2s +scripts/rssprobe.sh -p -i 5 -o d.csv # explicit target/interval/output +scripts/rssprobe.sh mark "VC join" # append a labeled marker row +scripts/rssprobe.sh mark -o d.csv "VC join" # marker into the same CSV +``` + +Ctrl-C stops the sampler. Both commands append to the same CSV +(`rssprobe.csv` in the CWD by default; the header is written once), +so a later plot or diff shows exactly which scenario moved the +needle. + +Runbook for the hyprlayd memory regression (spec: v031 memory, H1–H5): + +1. Cold idle: start the sampler on a freshly restarted daemon, mark + `idle-start`, wait 10 min, mark `idle-end`. +2. H1 bait: mark `window-move-storm`, sweep a window across the + overlay with `dim-on-hover` on (the poll path opens a fresh + Hyprland socket per 50ms tick while connected with a non-empty + roster), then repeat with it off. +3. VC join/leave cycles, camera/stream toggles: mark each; watch RSS + vs HWM after the leave. +4. Repeat the identical series on the v0.3.0 release build (`hyprlayd` + from that tag) and diff the CSVs. + ## Current transport — local IPC (`ipcprobe`) The daemon talks to Discord over Discord's local unix socket at `$XDG_RUNTIME_DIR/discord-ipc-N`; the client side lives in -`src/adapters/ipc.rs`. The wire format is 8-byte little-endian framing -(opcode u32 + payload length u32) carrying JSON payloads. +`src/daemon/adapters/ipc.rs`. The wire format is 8-byte little-endian +framing (opcode u32 + payload length u32) carrying JSON payloads. The probe connects to the socket, does the same framing, sends `HANDSHAKE` then `AUTHORIZE`, and prints every frame for 60s. Use it to @@ -29,7 +74,7 @@ see what a stock Discord client answers and how it frames data. Start here when Discord changes something: confirm at the protocol level whether opcodes or payloads moved before touching -`src/adapters/discord.rs`. +`src/daemon/adapters/discord.rs`. The socket has no HTTP layer, so there is no origin validation — any properly registered application id connects with zero portal diff --git a/scripts/rssprobe.sh b/scripts/rssprobe.sh new file mode 100755 index 0000000..54bedc5 --- /dev/null +++ b/scripts/rssprobe.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# rssprobe.sh — sample one process's memory to CSV for RSS-hunting. +# +# The sampler writes one CSV row per interval: epoch, ISO-8601 timestamp, +# VmRSS, and VmHWM (peak) in kB. RSS below HWM means memory was freed but +# not returned to the OS (allocator retention); RSS climbing toward or +# past the old HWM means real growth. Scenario markers are rows whose +# note column carries the label, so a plot or a diff shows exactly which +# scenario moved the needle. +# +# Usage: +# scripts/rssprobe.sh # sample `pidof hyprlayd` every 2s +# scripts/rssprobe.sh -p PID -i 2 -o f.csv +# scripts/rssprobe.sh mark "VC join" # append a marker row (other shell) +# scripts/rssprobe.sh mark # marker with no label ("scenario") +# +# Ctrl-C stops the sampler. Both commands append to the same CSV (header +# written once), so run the sampler in one terminal and mark scenarios +# from another while you work. + +set -euo pipefail + +usage() { + grep '^# ' "$0" | sed 's/^# //' + exit "${1:-0}" +} + +csv=rssprobe.csv +pid="" +interval=2 + +cmd=${1:-sample} +case $cmd in + mark) + shift + label="" + while [ $# -gt 0 ]; do + case $1 in + -o) csv=$2; shift 2 ;; + -i) shift 2 ;; # tolerated for copy-paste symmetry, unused here + *) label="$label${label:+ }$1"; shift ;; + esac + done + [ -n "$label" ] || label=scenario + [ -f "$csv" ] || echo "epoch,timestamp,rss_kb,hwm_kb,note" > "$csv" + printf '%s,%s,,,%s\n' "$(date +%s)" "$(date +%FT%T)" "$label" >> "$csv" + exit 0 + ;; + sample) + shift || true + ;; + -p | -i | -o | --help) + ;; + *) + usage 2 + ;; +esac + +while [ $# -gt 0 ]; do + case $1 in + -p) pid=$2; shift 2 ;; + -i) interval=$2; shift 2 ;; + -o) csv=$2; shift 2 ;; + --help) usage ;; + *) usage 2 ;; + esac +done + +if [ -z "$pid" ]; then + pid=$(pidof hyprlayd 2>/dev/null | tr ' ' '\n' | head -1) || { + echo "rssprobe: no hyprlayd running; pass -p PID" >&2 + exit 1 + } +fi +if [ "$(pidof hyprlayd 2>/dev/null | wc -w)" -gt 1 ] && [ "$pid" = "$(pidof hyprlayd | tr ' ' '\n' | head -1)" ]; then + echo "rssprobe: multiple hyprlayd processes; sampling $pid (first). Pass -p to pick." >&2 +fi + +status=/proc/$pid/status +[ -r "$status" ] || { echo "rssprobe: cannot read $status" >&2; exit 1; } + +[ -f "$csv" ] || echo "epoch,timestamp,rss_kb,hwm_kb,note" > "$csv" +echo "rssprobe: sampling pid $pid every ${interval}s -> $csv (Ctrl-C to stop)" >&2 + +while :; do + rss=$(awk '/^VmRSS:/{print $2}' "$status") || exit 0 # process gone + hwm=$(awk '/^VmHWM:/{print $2}' "$status") + printf '%s,%s,%s,%s,\n' "$(date +%s)" "$(date +%FT%T)" "$rss" "$hwm" >> "$csv" + sleep "$interval" +done From 7e47616221867dc424a7ded1188b1458f5a90a22 Mon Sep 17 00:00:00 2001 From: FAZuH Date: Sat, 5 Sep 2026 03:16:46 +0700 Subject: [PATCH 2/4] chore: drop the stale tomo demo target from dev.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd_demo exec'd target/release/tomo --config-path /tmp/tomo-demo — a binary and flag from the project-ops template this script was copied from. No tomo exists here, and scripts/demo.tape never did, so the target could not run. hyprlay has no tape-driven demo to repoint it at (the surface is a Wayland overlay, not a terminal session), so remove the command — function, description, and help/usage mentions — instead of shipping a dead target. --- dev.sh | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/dev.sh b/dev.sh index 7347086..62a1a62 100755 --- a/dev.sh +++ b/dev.sh @@ -2,7 +2,7 @@ # Development helper script # Usage: ./dev.sh [command1] [command2] ... -# commands: format | lint | test | docs | demo | all | help +# commands: format | lint | test | docs | all | help # plus any commands provided by modules (scripts/dev-*.sh, dev/*.sh, dev-*.sh) # Multiple commands can be specified and will execute left to right @@ -101,33 +101,6 @@ cmd_docs() { } dev_desc docs "Compile Mermaid diagrams to images" -cmd_demo() { - inf "Building release binary..." - cargo build --release - scs "Release build completed" - - inf "Creating wrapper script..." - local wrapper_dir="/tmp/tomo-demo-bin" - mkdir -p "$wrapper_dir" - cat > "$wrapper_dir/tomo" << SCRIPT -#!/bin/bash -exec $PWD/target/release/tomo --config-path /tmp/tomo-demo "\$@" -SCRIPT - chmod +x "$wrapper_dir/tomo" - export PATH="$wrapper_dir:$PATH" - trap "rm -rf $wrapper_dir" EXIT - scs "Wrapper created at $wrapper_dir/tomo" - - if ! command -v vhs &> /dev/null; then - wrn "vhs not found. Install it: https://github.com/charmbracelet/vhs" - fi - - inf "Running demo tape..." - vhs scripts/demo.tape - scs "Demo tape completed" -} -dev_desc demo "Build release, alias, and run vhs demo tape" - cmd_all() { inf "Running all tasks..." cmd_format @@ -180,7 +153,6 @@ Examples: ./dev.sh lint # Run linter ./dev.sh test # Run tests ./dev.sh docs # Compile Mermaid diagrams - ./dev.sh demo # Build release, alias, and run demo tape ./dev.sh format lint # Format then lint ./dev.sh all # Run format, lint, and test From 586f5a501a41193dbc446ed851106236a1a32ae1 Mon Sep 17 00:00:00 2001 From: FAZuH Date: Fri, 11 Sep 2026 15:42:54 +0700 Subject: [PATCH 3/4] fix: pin winit-core so lockless installs compile cargo install without --locked re-resolves and ignores Cargo.lock. winit-core 0.31.0-beta.3 added NativeKeyCode::Android, which iced_exdevtools 0.19.1 (unconditional dep of iced_layershell 0.19.1, pinned to the 0.19 line) does not match: E0004. Pin winit-core exactly so every resolution path lands on beta.2, and document --locked in the README install line. Drop the pin once iced_layershell releases on iced_exdevtools 0.20. --- Cargo.lock | 1 + Cargo.toml | 5 +++++ README.md | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f9d9afc..24232a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1670,6 +1670,7 @@ dependencies = [ "tray-icon", "ureq", "windows-sys 0.59.0", + "winit-core", "winresource", "x11rb", ] diff --git a/Cargo.toml b/Cargo.toml index 1713fed..18dd3ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,10 @@ iced = { version = "0.14", default-features = false, features = [ ] } iced_layershell = "0.19.1" iced_runtime = "0.14" +# pinned: iced_exdevtools 0.19.1 (unconditional dep of iced_layershell 0.19.1) +# fails to compile against winit-core 0.31.0-beta.3; remove once iced_layershell +# ships a release on the fixed iced_exdevtools 0.20 line +winit-core = "=0.31.0-beta.2" tokio = { version = "1", features = ["rt", "net", "io-util", "sync", "time", "macros"] } futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } futures-channel = { version = "0.3", default-features = false, features = ["std"] } @@ -64,6 +68,7 @@ hyprlay-core = { workspace = true } clap = { workspace = true } iced = { workspace = true } iced_runtime = { workspace = true } +winit-core = { workspace = true } tokio = { workspace = true } futures-util = { workspace = true } futures-channel = { workspace = true } diff --git a/README.md b/README.md index 54b01b1..c0c62a5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Download the latest binaries from [releases page](https://github.com/FAZuH/hyprl Or install with [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html): ```sh -cargo install --git https://github.com/FAZuH/hyprlay +cargo install --locked --git https://github.com/FAZuH/hyprlay # Or build from source: cargo build --release From f65be15dddd5b8866591117e6f92e6bcf59cfe53 Mon Sep 17 00:00:00 2001 From: FAZuH Date: Fri, 11 Sep 2026 19:41:53 +0700 Subject: [PATCH 4/4] docs: note the lockless-install fix in the changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c711f5..2cec237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Platforms + +- Fixed source installs failing on a fresh dependency resolution + ## 0.3.0 (2026-09-03) ### Platforms