#!/usr/bin/env bash # Print one frame of the top network-consuming processes, then exit. # # Single-shot by design, exactly like topproc.sh: SysStats re-runs it every # netProcInterval while the Network drawer is open, and exiting per frame # guarantees stdout is flushed (a long-running loop would block-buffer its # frame and the QML side would never see it). topproc.sh is the reference for # the bash-script + single-shot + SplitParser-parse pattern. # # How per-process bandwidth is derived: `ss` reports, per TCP socket, the # cumulative bytes_sent / bytes_received plus the owning pid (users:) and a # stable socket inode (ino:). We take two snapshots netProcInterval apart, diff # each socket's byte counters by inode, sum the positive deltas per process, # and divide by the interval to get bytes/sec. Only TCP carries these counters, # so UDP/QUIC traffic is not attributed — that is a kernel/ss limitation, not a # bug here. # # IMPORTANT: needs `ss` (iproute2) on PATH — added to runtimeInputs in flake.nix # alongside bash + coreutils. No awk/getconf (absent at runtime); bash builtins # and coreutils (sort, head, sleep) only. # # Output: one "N " line per process, # highest combined throughput first. intv="${1:-2}" n="${2:-6}" # Snapshot every TCP socket's cumulative rx/tx bytes into the rx/tx assoc # arrays keyed by socket inode, and remember each inode's owning process in # pidOf/nameOf. `ss -O` prints one socket per line so a single read loop works. # -t TCP -i info(byte counters) -n numeric -H no header # -O oneline -p process -e extended(ino:) snapshot() { local -n _rx="$1" _tx="$2" _pid="$3" _name="$4" local line w ino bs br pid name tmp set -f # no globbing when word-splitting $line below while IFS= read -r line; do ino=""; bs=0; br=0 # ino: / bytes_*: tokens never contain spaces, so word-splitting is safe # even when a process name does (e.g. "Web Content"). for w in $line; do case "$w" in ino:*) ino="${w#ino:}" ;; bytes_sent:*) bs="${w#bytes_sent:}" ;; bytes_received:*) br="${w#bytes_received:}" ;; esac done [ -n "$ino" ] || continue _rx["$ino"]="$br" _tx["$ino"]="$bs" # name + pid from the users:(("name",pid=N,fd=M)) token; parse off the # whole line so a space inside the quoted name can't break it. tmp="${line#*users:((\"}"; name="${tmp%%\"*}" tmp="${line#*pid=}"; pid="${tmp%%,*}" _pid["$ino"]="$pid" _name["$ino"]="$name" done < <(ss -tinHOpe 2>/dev/null) set +f } declare -A rx1 tx1 rx2 tx2 pidOf nameOf snapshot rx1 tx1 pidOf nameOf sleep "$intv" snapshot rx2 tx2 pidOf nameOf # Per-process byte deltas over the interval. A socket present in both snapshots # contributes its counter delta; a socket new in the second snapshot contributes # its full byte count (all of it landed within the window). Aggregate by pid. declare -A prx ptx pname for ino in "${!rx2[@]}"; do pid="${pidOf[$ino]}" [ -n "$pid" ] || continue if [ -n "${rx1[$ino]:-}" ]; then drx=$(( ${rx2[$ino]} - ${rx1[$ino]} )) dtx=$(( ${tx2[$ino]} - ${tx1[$ino]} )) else drx="${rx2[$ino]}" dtx="${tx2[$ino]}" fi [ "$drx" -gt 0 ] || drx=0 [ "$dtx" -gt 0 ] || dtx=0 prx["$pid"]=$(( ${prx[$pid]:-0} + drx )) ptx["$pid"]=$(( ${ptx[$pid]:-0} + dtx )) pname["$pid"]="${nameOf[$ino]}" done # Emit " " per process, rank by total throughput, # take the top n, then reformat to the N-line protocol the QML side parses. for pid in "${!prx[@]}"; do r=$(( ${prx[$pid]} / intv )) t=$(( ${ptx[$pid]} / intv )) tot=$(( r + t )) [ "$tot" -gt 0 ] || continue printf '%d %d %d %s\n' "$tot" "$r" "$t" "${pname[$pid]}" done | sort -rn | head -n "$n" | while read -r tot r t name; do printf 'N %s %s %s\n' "$r" "$t" "$name" done