Skip to content

Instantly share code, notes, and snippets.

@d12frosted
Created August 12, 2026 08:41
Show Gist options
  • Select an option

  • Save d12frosted/50487b2c03f8977387b6e3d0349daf00 to your computer and use it in GitHub Desktop.

Select an option

Save d12frosted/50487b2c03f8977387b6e3d0349daf00 to your computer and use it in GitHub Desktop.
vui btop restructured: same UI, 2.4x faster via band-level memo components

vui btop, restructured

A follow-up to yibie's vui btop experiment. Same UI, same rendered output (the benchmark asserts byte-identical buffer text), but structured the way a vui app is meant to be structured: per-band :memo components and memoized derived data, so a state change only re-renders the parts that depend on it.

No framework changes, no new helpers: the pure rendering code (parsing, graphs, panel drawing) is reused from the original file.

What changed

  1. The UI is split into three keyed :memo t band components (cpu, lower, footer). A selection change re-renders only the lower band.
  2. The filtered+sorted process list is computed once per render via vui-use-memo (the original sorted it twice per render).
  3. The memory/disk/network line nodes are cached with vui-use-memo, so selection moves reuse them untouched.
  4. Keymaps are built once, dispatching through a ref, instead of re-running define-key on every render.
  5. The per-line memo wrapper component is gone. It could never hit for process rows anyway: their :on-click closures are fresh every render, so props never compare equal.

Results

50-process fixture, 110x42 viewport, 200 timed runs after 20 warmups, medians stable across 3 repetitions. Emacs 31, macOS.

scenario original restructured speedup
selection move 3.08 ms 1.29 ms 2.4x
snapshot tick (all panels change) 4.47 ms 2.80 ms 1.6x
no-op set-state 2.68 ms 0.32 ms 8.4x

The original measures 3.0-3.1 ms here vs the 3.4 ms reported in yibie's environment, so the numbers translate across setups.

The no-op row is the most instructive one: the original pays 2.7 ms to change nothing, because memo on leaf line components cannot save the parent's work; the app component still rebuilds every string in the frame before 40+ line wrappers each conclude "no change". Memo placement matters more than memo quantity.

I also tried going finer (a :memo component per process row, keyed by PID) and it was a regression: 1.29 ms -> 1.71 ms. Component instances are not free; for cheap rows the bookkeeping costs more than the strings it saves. Band-level granularity is the sweet spot for this UI.

Run it

Save the original gist file as vui-btop-state-prototype.el in this directory, then:

emacs -Q --batch -L /path/to/vui.el -L . -l btop-bench.el -f btop-bench-run

To use it interactively: M-x vui-btop-improved-open (macOS only, same as the original).

;;; btop-bench.el --- Benchmark gist btop vs improved btop -*- lexical-binding: t; -*-
;; Measures the gist implementation (vui-btop-state-prototype) against
;; the restructured one (vui-btop-improved) on the same 50-process
;; fixture at the author's 110x42 viewport. Batch-safe: the sampler
;; timer never fires because nothing calls sit-for.
;;
;; Run:
;; emacs -Q --batch -L <vui-worktree> -L <this-dir> \
;; -l btop-bench.el -f btop-bench-run
;;; Code:
(require 'cl-lib)
(require 'vui)
(require 'vui-btop-state-prototype)
(require 'vui-btop-improved)
(defconst btop-bench--viewport '(110 . 42))
(defconst btop-bench--runs 200)
(defconst btop-bench--warmup 20)
;;; Fixture
(defun btop-bench--fixture (tick)
"Return a deterministic 50-process snapshot for TICK."
(let ((history
(lambda (seed)
(cl-loop for j below 120
collect (/ (mod (* (+ j seed tick) 104729) 1000) 10.0)))))
(list :tick tick
:cpu-history (funcall history 1)
:download-history (funcall history 2)
:upload-history (funcall history 3)
:processes
(cl-loop for i from 1 to 50
collect (list :pid (+ 1000 i)
:cpu (/ (mod (* i 7919 (1+ tick)) 1000) 10.0)
:mem (mod (* i 337) 2048)
:user (if (= 0 (mod i 3)) "root" "boris")
:state "S"
:etime "01:23:45"
:command (format "/usr/bin/tool-%02d --flag" i)
:name (format "tool-%02d" i)))
:cpu 37.5 :cpu-model "Apple M3 Max"
:load-average '(2.5 2.1 1.9)
:memory-total (* 64 1024 1024 1024)
:memory-used (* 40 1024 1024 1024)
:memory-free (* 24 1024 1024 1024)
:memory-cached (* 10 1024 1024 1024)
:disk-total (* 2 1024 1024 1024 1024)
:disk-used (* 1024 1024 1024 1024)
:disk-free (* 1024 1024 1024 1024)
:download-rate 1234567.0 :upload-rate 234567.0
:download-total 999999999 :upload-total 88888888
:sample-ms 12.0 :sampled-at "12:00:00")))
;;; Driving a mounted app from outside
(defun btop-bench--set-state (root buffer &rest kvs)
"Apply KVS state changes to ROOT in BUFFER and flush synchronously."
(with-current-buffer buffer
(let ((vui--current-instance root)
(vui--root-instance root))
(cl-loop for (key value) on kvs by #'cddr
do (vui-set-state key value))
(vui-flush-sync))))
(defun btop-bench--mount (component buffer-name)
"Mount COMPONENT into BUFFER-NAME with the fixture; return (ROOT . BUFFER)."
(let* ((root (vui-mount (vui-component component) buffer-name))
(buffer (get-buffer buffer-name)))
(with-current-buffer buffer
(setq-local vui-incremental-render t
truncate-lines t))
(btop-bench--set-state root buffer
:viewport btop-bench--viewport
:snapshot (btop-bench--fixture 1)
:process-index 5)
(cons root buffer)))
;;; Measurement
(defun btop-bench--measure (thunk)
"Run THUNK warmup + timed rounds; return sorted list of seconds."
(dotimes (_ btop-bench--warmup) (funcall thunk))
(garbage-collect)
(let (times)
(dotimes (_ btop-bench--runs)
(let ((start (float-time)))
(funcall thunk)
(push (- (float-time) start) times)))
(sort times #'<)))
(defun btop-bench--report (label times)
"Print LABEL with min/median/max of TIMES in ms."
(let* ((n (length times))
(median (nth (/ n 2) times)))
(message " %-28s min %7.3f median %7.3f max %7.3f (ms, n=%d)"
label
(* 1000 (car times))
(* 1000 median)
(* 1000 (car (last times)))
n)))
(defun btop-bench--scenarios (root buffer)
"Run all scenarios against ROOT in BUFFER."
;; Selection move: the author's headline number.
(let ((flip nil))
(btop-bench--report
"selection move (j/k)"
(btop-bench--measure
(lambda ()
(setq flip (not flip))
(btop-bench--set-state root buffer :process-index (if flip 6 5))))))
;; Snapshot tick: a new sample arrives, everything changes.
(let ((tick 1))
(btop-bench--report
"snapshot tick (all panels)"
(btop-bench--measure
(lambda ()
(setq tick (1+ tick))
(btop-bench--set-state root buffer
:snapshot (btop-bench--fixture tick))))))
;; No-op: same value set again; measures the dirty-check floor.
(btop-bench--report
"no-op set-state"
(btop-bench--measure
(lambda ()
(btop-bench--set-state root buffer :process-index 5)))))
;;; Parity
(defun btop-bench--first-difference (a b)
"Return (LINE ORIG IMPROVED) for the first differing line of A and B."
(let ((lines-a (split-string a "\n"))
(lines-b (split-string b "\n"))
(index 0)
result)
(while (and (not result) (or lines-a lines-b))
(let ((line-a (pop lines-a))
(line-b (pop lines-b)))
(unless (equal line-a line-b)
(setq result (list index line-a line-b))))
(setq index (1+ index)))
result))
(defun btop-bench--parity (buffer-a buffer-b)
"Compare rendered text of BUFFER-A and BUFFER-B."
(let ((text-a (with-current-buffer buffer-a
(buffer-substring-no-properties (point-min) (point-max))))
(text-b (with-current-buffer buffer-b
(buffer-substring-no-properties (point-min) (point-max)))))
(if (equal text-a text-b)
(message "PARITY: identical rendered text (%d chars)" (length text-a))
(let ((difference (btop-bench--first-difference text-a text-b)))
(message "PARITY FAILED at line %d:" (nth 0 difference))
(message " orig: %S" (nth 1 difference))
(message " improved: %S" (nth 2 difference))
(kill-emacs 1)))))
;;; Entry point
(defun btop-bench-run ()
"Mount both apps, check parity, run all scenarios."
(message "Emacs %s, vui %s, fixture: 50 processes, viewport %sx%s"
emacs-version
(or (ignore-errors
(package-get-version))
"worktree")
(car btop-bench--viewport) (cdr btop-bench--viewport))
(let* ((orig (btop-bench--mount 'vui-btop-state-prototype-app "*orig*"))
(improved (btop-bench--mount 'vui-btop-improved-app "*improved*")))
(btop-bench--parity (cdr orig) (cdr improved))
(message "")
(message "gist version (vui-btop-state-prototype):")
(btop-bench--scenarios (car orig) (cdr orig))
(message "")
(message "improved version (vui-btop-improved):")
(btop-bench--scenarios (car improved) (cdr improved))))
(provide 'btop-bench)
;;; btop-bench.el ends here
;;; vui-btop-improved.el --- VUI btop, restructured for granular updates -*- lexical-binding: t; -*-
;; EXPERIMENT: same btop UI as vui-btop-state-prototype.el (the gist),
;; same rendered output, but structured the way a vui app is meant to be
;; structured: per-band memoized components and memoized derived data,
;; so a state change only re-renders the parts that depend on it.
;;
;; Differences from the gist version:
;;
;; 1. The UI is split into three keyed band components (cpu, lower,
;; footer), each `:memo t'. A selection change re-renders only the
;; lower band; cpu graphs and footer bail out on shallow prop
;; comparison.
;; 2. The filtered+sorted process list is computed once per render via
;; `vui-use-memo' (the gist sorted it twice per render: once in the
;; app render, once again inside the process panel).
;; 3. The non-process lower-panel lines (memory, disks, network) are
;; cached with `vui-use-memo' keyed on (snapshot width height boxes),
;; so moving the selection reuses them as-is.
;; 4. Keymaps are built once; keys dispatch through a ref that the
;; render refreshes, instead of re-running `define-key' every render.
;; 5. The per-line memoized wrapper component is gone. It could never
;; hit for process rows anyway: their :on-click closures are fresh
;; every render, so the props never compare equal.
;;
;; Pure helpers (parsing, graphs, panel string drawing) are reused from
;; the gist file, which must be on `load-path'.
;;
;; Run:
;; emacs -Q -L /path/to/vui.el -L /path/to/gist -l vui-btop-improved.el
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'vui)
(require 'vui-btop-state-prototype)
(defconst vui-btop-improved--buffer-name "*VUI btop IMPROVED*")
;;; Process panel (takes the already-ordered list; the gist re-sorted here)
(defun vui-btop-improved--process-panel
(processes width height process-index sort-index reversed details paused
select-function row-keymap)
"Return responsive process panel lines for ordered PROCESSES and UI state."
(let* ((maximum (max 0 (1- (length processes))))
(selected (min process-index maximum))
(process (nth selected processes))
(detail-height (if (and details process) 6 0))
(fixed (+ 3 detail-height))
(slots (max 1 (- height fixed)))
(start (max 0 (min (- (length processes)
(min slots (length processes)))
(- selected (/ slots 2)))))
(visible (cl-subseq processes start
(min (length processes) (+ start slots))))
(sort (aref [cpu memory pid] sort-index))
(attributes (and process (process-attributes (plist-get process :pid))))
(threads (or (alist-get 'thcount attributes) 0))
lines)
(push (vui-btop-state-prototype--text
(vui-btop-state-prototype--title-line
width "4" "proc"
(format "%s%s%s" sort (if reversed " reverse" "")
(if paused " paused" ""))
vui-btop-state-prototype--process-face))
lines)
(when (and details process)
(dolist (line
(list
(format " Status: %-8s Elapsed: %s"
(plist-get process :state) (plist-get process :etime))
(format " PID: %-8d User: %-10s Threads: %d"
(plist-get process :pid) (plist-get process :user) threads)
(format " CPU: %.1f%% Memory: %d MiB"
(float (plist-get process :cpu)) (plist-get process :mem))
"" (format " %s" (plist-get process :command))))
(push (vui-btop-state-prototype--text
(vui-btop-state-prototype--inside-line width line))
lines))
(push (vui-btop-state-prototype--text
(vui-btop-state-prototype--separator-line
width "proc filter per-core reverse tree"
vui-btop-state-prototype--process-face))
lines))
(push (vui-btop-state-prototype--text
(vui-btop-state-prototype--inside-line
width (vui-btop-state-prototype--process-header (max 1 (- width 2))))
vui-btop-state-prototype--process-face)
lines)
(let ((index start))
(dolist (row visible)
(push (vui-btop-state-prototype--process-row
row index width (= index selected) select-function row-keymap)
lines)
(setq index (1+ index))))
(dotimes (_ (- slots (length visible)))
(push (vui-btop-state-prototype--text
(vui-btop-state-prototype--inside-line width ""))
lines))
(push (vui-btop-state-prototype--text
(vui-btop-state-prototype--bottom-line
width (format "%d/%d" (if process (1+ selected) 0)
(length processes))
vui-btop-state-prototype--process-face))
lines)
(nreverse lines)))
;;; Non-process lower lines (pure; result is cached by the lower band)
(defun vui-btop-improved--static-lower-lines (snapshot width height boxes)
"Return the non-process lower lines for SNAPSHOT, or nil when proc-only."
(let* ((mem (memq 'mem boxes))
(net (memq 'net boxes))
(left-visible (or mem net))
(proc (memq 'proc boxes)))
(cond
((and (>= width 108) left-visible proc)
(let ((left (max 46 (/ (* width 45) 100))))
(vui-btop-state-prototype--left-column snapshot left height boxes)))
((and left-visible proc)
(let* ((left-count (+ (if mem 1 0) (if net 1 0)))
(small (max 6 (/ height (+ left-count 2))))
lines)
(when mem
(setq lines (append lines
(vui-btop-state-prototype--memory-group
snapshot width small))))
(when net
(setq lines (append lines
(vui-btop-state-prototype--network-panel
snapshot width small))))
lines))
(left-visible
(vui-btop-state-prototype--left-column snapshot width height boxes))
(proc nil)
(t
(vui-btop-state-prototype--simple-panel
width height nil "btop" ""
'(" No boxes visible. Press 1, 2, 3, or 4.")
vui-btop-state-prototype--muted-face)))))
;;; Band components
(vui-defcomponent vui-btop-improved-cpu (snapshot width height)
"Full-width CPU panel. Skips re-render unless the snapshot changes."
:memo t
:render
(apply #'vui-vstack :spacing 0
(vui-btop-state-prototype--cpu-panel snapshot width height)))
(vui-defcomponent vui-btop-improved-lower
(snapshot processes width height boxes process-index sort-index reversed
details paused select-fn row-keymap)
"Lower band: memory, disks, network, and the process table."
:memo t
:render
(let* ((mem (memq 'mem boxes))
(net (memq 'net boxes))
(left-visible (or mem net))
(proc-visible (memq 'proc boxes))
(wide (and (>= width 108) left-visible proc-visible))
;; Cached: selection moves reuse these nodes untouched.
(static-lines
(vui-use-memo (snapshot width height boxes)
(vui-btop-improved--static-lower-lines snapshot width height boxes)))
(proc-lines
(when proc-visible
(let ((proc-width
(if wide (- width (max 46 (/ (* width 45) 100))) width))
(proc-height
(cond
(wide height)
(left-visible
(let* ((left-count (+ (if mem 1 0) (if net 1 0)))
(small (max 6 (/ height (+ left-count 2)))))
(max 6 (- height (* small left-count)))))
(t height))))
(vui-btop-improved--process-panel
processes proc-width proc-height process-index sort-index
reversed details paused select-fn row-keymap)))))
(apply #'vui-vstack :spacing 0
(cond
(wide (vui-btop-state-prototype--combine-lines
static-lines proc-lines))
((and left-visible proc-visible) (append static-lines proc-lines))
(left-visible static-lines)
(proc-visible proc-lines)
(t static-lines)))))
(vui-defcomponent vui-btop-improved-footer (paused width)
"Key hint line."
:memo t
:render
(vui-btop-state-prototype--text
(vui-btop-state-prototype--fit
(format
"↑↓ select Enter details ←→ sort / filter r reverse p %s 1-4 boxes q quit"
(if paused "resume" "pause"))
width)
vui-btop-state-prototype--muted-face))
;;; Keymaps: built once, dispatching through a ref the render refreshes
(defun vui-btop-improved--command (actions-ref key)
"Return a command running the KEY action stored in ACTIONS-REF."
(lambda ()
(interactive)
(funcall (plist-get (car actions-ref) key))))
(defun vui-btop-improved--ensure-keymaps (keymap-ref row-keymap-ref actions-ref)
"Populate KEYMAP-REF and ROW-KEYMAP-REF once, dispatching via ACTIONS-REF."
(unless (car keymap-ref)
(let ((map (make-sparse-keymap))
(row (make-sparse-keymap)))
(dolist (key '("j" "\C-n" [down] [wheel-down] [mouse-5]))
(define-key map key (vui-btop-improved--command actions-ref :down)))
(dolist (key '("k" "\C-p" [up] [wheel-up] [mouse-4]))
(define-key map key (vui-btop-improved--command actions-ref :up)))
(define-key map (kbd "RET")
(vui-btop-improved--command actions-ref :details))
(define-key row (kbd "RET")
(vui-btop-improved--command actions-ref :details))
(define-key map "p" (vui-btop-improved--command actions-ref :pause))
(define-key map [left] (vui-btop-improved--command actions-ref :sort-left))
(define-key map [right] (vui-btop-improved--command actions-ref :sort-right))
(define-key map "r" (vui-btop-improved--command actions-ref :reverse))
(define-key map "/" (vui-btop-improved--command actions-ref :filter))
(define-key map "1" (vui-btop-improved--command actions-ref :box-1))
(define-key map "2" (vui-btop-improved--command actions-ref :box-2))
(define-key map "3" (vui-btop-improved--command actions-ref :box-3))
(define-key map "4" (vui-btop-improved--command actions-ref :box-4))
(define-key map "q" (vui-btop-improved--command actions-ref :quit))
(setcar keymap-ref map)
(setcar row-keymap-ref row))))
;;; App
(vui-defcomponent vui-btop-improved-app ()
:state ((snapshot (vui-btop-state-prototype--empty-snapshot))
(process-index 0)
(sort-index 0)
(reversed nil)
(filter "")
(paused nil)
(details t)
(boxes '(cpu mem net proc))
(viewport nil))
:render
(let* ((process-ref (vui-use-ref nil))
(started-ref (vui-use-ref 0.0))
(network-ref (vui-use-ref nil))
(keymap-ref (vui-use-ref nil))
(row-keymap-ref (vui-use-ref nil))
(actions-ref (vui-use-ref nil))
(select-ref (vui-use-ref nil))
(finished
(vui-async-callback (process _event)
(when (memq (process-status process) '(exit signal))
(let* ((output (process-buffer process))
(status (process-exit-status process))
(text (and (buffer-live-p output)
(with-current-buffer output (buffer-string)))))
(when (buffer-live-p output) (kill-buffer output))
(setcar process-ref nil)
(when (and (= status 0) text)
(vui-set-state
:snapshot
(lambda (previous)
(let ((parsed
(vui-btop-state-prototype--parse-output
text previous (car network-ref)
(* 1000.0
(- (float-time) (car started-ref))))))
(setcar network-ref (cdr parsed))
(car parsed)))))))))
(sample
(vui-with-async-context
(unless (process-live-p (car process-ref))
(let ((output (generate-new-buffer " *vui-btop-sample*")))
(setcar started-ref (float-time))
(setcar process-ref
(make-process
:name "vui-btop-sample"
:buffer output
:command vui-btop-state-prototype--sample-command
:coding 'utf-8-unix
:noquery t
:connection-type 'pipe
:sentinel finished))))))
(size (or viewport (vui-btop-state-prototype--window-size)))
(width (car size))
;; Sorted and filtered once per render; the gist did this twice.
(processes
(vui-use-memo (snapshot sort-index reversed filter)
(vui-btop-state-prototype--ordered-processes
snapshot sort-index reversed filter)))
(maximum (max 0 (1- (length processes))))
(select-process
(or (car select-ref)
(setcar select-ref
(lambda (index) (vui-set-state :process-index index)))))
(move
(lambda (amount)
(vui-set-state
:process-index
(max 0 (min maximum
(+ (min maximum process-index) amount))))))
(toggle-box
(lambda (box)
(vui-set-state :boxes
(lambda (current-boxes)
(vui-btop-state-prototype--toggle
box current-boxes)))))
(body-height (max 12 (1- (cdr size))))
(cpu-height (if (memq 'cpu boxes)
(max 8 (min 14 (/ body-height 3)))
0))
(lower-height (max 8 (- body-height cpu-height))))
(setcar actions-ref
(list
:down (vui-with-async-context (funcall move 1))
:up (vui-with-async-context (funcall move -1))
:details (vui-with-async-context
(vui-set-state :details (not details)))
:pause (vui-with-async-context
(vui-set-state :paused (not paused)))
:sort-left (vui-with-async-context
(vui-batch
(vui-set-state :sort-index (mod (1- sort-index) 3))
(vui-set-state :process-index 0)))
:sort-right (vui-with-async-context
(vui-batch
(vui-set-state :sort-index (mod (1+ sort-index) 3))
(vui-set-state :process-index 0)))
:reverse (vui-with-async-context
(vui-batch
(vui-set-state :reversed (not reversed))
(vui-set-state :process-index 0)))
:filter (vui-with-async-context
(vui-batch
(vui-set-state :filter
(if (string-empty-p filter) "emacs" ""))
(vui-set-state :process-index 0)))
:box-1 (vui-with-async-context (funcall toggle-box 'cpu))
:box-2 (vui-with-async-context (funcall toggle-box 'mem))
:box-3 (vui-with-async-context (funcall toggle-box 'net))
:box-4 (vui-with-async-context (funcall toggle-box 'proc))
:quit (lambda () (kill-buffer (current-buffer)))))
(vui-btop-improved--ensure-keymaps keymap-ref row-keymap-ref actions-ref)
(vui-use-effect (paused)
(unless paused
(let ((timer (run-with-timer
0 vui-btop-state-prototype--interval sample)))
(lambda ()
(cancel-timer timer)
(when-let* ((process (car process-ref)))
(let ((output (process-buffer process)))
(when (process-live-p process) (delete-process process))
(when (buffer-live-p output) (kill-buffer output))
(setcar process-ref nil)))))))
(vui-use-effect ()
(let ((resize
(vui-with-async-context
(let ((next (vui-btop-state-prototype--window-size)))
(vui-set-state
:viewport
(lambda (current) (if (equal current next) current next)))))))
(add-hook 'window-configuration-change-hook resize nil t)
(lambda ()
(remove-hook 'window-configuration-change-hook resize t))))
(vui-vstack :spacing 0 :keymap (car keymap-ref)
(when (memq 'cpu boxes)
(vui-component 'vui-btop-improved-cpu :key 'cpu
:snapshot snapshot :width width :height cpu-height))
(vui-component 'vui-btop-improved-lower :key 'lower
:snapshot snapshot :processes processes
:width width :height lower-height :boxes boxes
:process-index process-index :sort-index sort-index
:reversed reversed :details details :paused paused
:select-fn select-process
:row-keymap (car row-keymap-ref))
(vui-component 'vui-btop-improved-footer :key 'footer
:paused paused :width width))))
(defun vui-btop-improved-open ()
"Open the improved live VUI btop experiment."
(interactive)
(unless (eq system-type 'darwin)
(user-error "This throwaway sampler currently targets macOS"))
(vui-mount (vui-component 'vui-btop-improved-app)
vui-btop-improved--buffer-name)
(with-current-buffer vui-btop-improved--buffer-name
(setq-local truncate-lines t
cursor-type nil
mode-line-format nil
vui-incremental-render t)
(face-remap-add-relative
'default '(:background "#070a0f" :foreground "#c8d3f5"))))
(provide 'vui-btop-improved)
;;; vui-btop-improved.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment