Skip to content

Commit 6a4a069

Browse files
committed
Add tui-partial benchmark for status-line update workload
The existing tui-frame bench rewrites every row per iteration, so it cannot distinguish backends that honor per-row dirty tracking from those that re-render unconditionally. The new tui-partial section renders the static screen once, then updates only the bottom row per iteration — the workload that status bars, prompt redraws, and most TUI updates actually produce. On ghostel this exposes that incremental redraw (the per-row dirty-bit branch in render()) is 8-13x faster than full mode at 24x80 and 40x120. Wired into ghostel-bench-run-all alongside vterm, eat, and term so backends can be compared on this workload. Also adds a Makefile target (bench-tui-partial) that runs only this section, ghostel-only, for quick iteration when hacking on the renderer.
1 parent 5d73106 commit 6a4a069

2 files changed

Lines changed: 128 additions & 1 deletion

File tree

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ELC := lisp/ghostel.elc lisp/ghostel-debug.elc lisp/ghostel-compile.elc \
88
lisp/ghostel-eshell.elc \
99
extensions/evil-ghostel/evil-ghostel.elc
1010

11-
.PHONY: all build test test-native test-all test-evil lint melpazoid melpazoid-ghostel melpazoid-evil-ghostel byte-compile docquotes bench bench-quick clean regen-terminfo
11+
.PHONY: all build test test-native test-all test-evil lint melpazoid melpazoid-ghostel melpazoid-evil-ghostel byte-compile docquotes bench bench-quick bench-tui-partial clean regen-terminfo
1212

1313
all: build test-all test-evil lint
1414

@@ -115,6 +115,10 @@ bench:
115115
bench-quick:
116116
bash bench/run-bench.sh --quick
117117

118+
bench-tui-partial:
119+
$(EMACS) --batch -Q -L lisp -l bench/ghostel-bench.el \
120+
--eval '(progn (setq ghostel-bench-include-vterm nil ghostel-bench-include-eat nil ghostel-bench-include-term nil) (ghostel-bench--load-backends) (ghostel-bench--run-tui-partial-scenarios))'
121+
118122
clean:
119123
rm -f ghostel-module.dylib ghostel-module.so
120124
rm -f $(ELC)

bench/ghostel-bench.el

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,128 @@ content — relevant for apps like htop, vim, claude-code."
657657
(message " ^ %.0f fps" (/ 1000.0 (plist-get result :per-iter-ms))))
658658
(delete-process proc))))))))
659659

660+
;; =========================================================================
661+
;; SECTION 3b: TUI partial-update — static screen + status-line update
662+
;; =========================================================================
663+
664+
(defun ghostel-bench--run-tui-partial-scenarios ()
665+
"Benchmark partial-update workload (status-line update over static screen).
666+
The `tui-frame' scenario rewrites every row per iteration, so it cannot
667+
distinguish backends that honor per-row dirty tracking from those that
668+
re-render unconditionally. Here the static screen is rendered once and
669+
only the bottom row is rewritten per iteration — the workload that
670+
status bars, prompt redraws, and most TUI updates actually produce."
671+
(message "\n--- TUI Partial Update (bottom-row update over static screen) ---")
672+
(message " %-50s %5s %8s %10s %8s" "SCENARIO" "ITERS" "TOTAL(s)" "ITER(ms)" "fps")
673+
(message " %s" (make-string 90 ?-))
674+
(let ((partial-iters (* ghostel-bench-iterations 1000)))
675+
(dolist (size ghostel-bench-terminal-sizes)
676+
(let* ((rows (car size))
677+
(cols (cdr size))
678+
(label (format "%dx%d" rows cols))
679+
(static-frame (ghostel-bench--gen-tui-frame rows cols))
680+
(status-template (format "\e[%d;1H\e[1;33;41m%%-%ds\e[0m" rows cols)))
681+
;; ghostel incremental
682+
(with-temp-buffer
683+
(let* ((static (ghostel-bench--encode-for-backend static-frame 'ghostel))
684+
(term (ghostel-bench--make-ghostel rows cols))
685+
(ghostel-enable-url-detection nil)
686+
(ghostel-enable-file-detection nil)
687+
(inhibit-read-only t)
688+
(counter 0))
689+
(ghostel--write-input term static)
690+
(ghostel--redraw term t)
691+
(let ((result
692+
(ghostel-bench--measure
693+
(format "tui-partial/ghostel-incr/%s" label)
694+
cols partial-iters
695+
(lambda ()
696+
(cl-incf counter)
697+
(ghostel--write-input
698+
term (format status-template (format "status #%d" counter)))
699+
(ghostel--redraw term nil)))))
700+
(message " ^ %.0f fps" (/ 1000.0 (plist-get result :per-iter-ms))))))
701+
;; ghostel full
702+
(with-temp-buffer
703+
(let* ((static (ghostel-bench--encode-for-backend static-frame 'ghostel))
704+
(term (ghostel-bench--make-ghostel rows cols))
705+
(ghostel-enable-url-detection nil)
706+
(ghostel-enable-file-detection nil)
707+
(inhibit-read-only t)
708+
(counter 0))
709+
(ghostel--write-input term static)
710+
(ghostel--redraw term t)
711+
(let ((result
712+
(ghostel-bench--measure
713+
(format "tui-partial/ghostel-full/%s" label)
714+
cols partial-iters
715+
(lambda ()
716+
(cl-incf counter)
717+
(ghostel--write-input
718+
term (format status-template (format "status #%d" counter)))
719+
(ghostel--redraw term t)))))
720+
(message " ^ %.0f fps" (/ 1000.0 (plist-get result :per-iter-ms))))))
721+
;; vterm
722+
(when ghostel-bench-include-vterm
723+
(with-temp-buffer
724+
(let* ((static (ghostel-bench--encode-for-backend static-frame 'vterm))
725+
(term (ghostel-bench--make-vterm rows cols))
726+
(counter 0))
727+
(vterm--write-input term static)
728+
(vterm--redraw term)
729+
(let ((result
730+
(ghostel-bench--measure
731+
(format "tui-partial/vterm/%s" label)
732+
cols partial-iters
733+
(lambda ()
734+
(cl-incf counter)
735+
(vterm--write-input
736+
term (format status-template (format "status #%d" counter)))
737+
(vterm--redraw term)))))
738+
(message " ^ %.0f fps" (/ 1000.0 (plist-get result :per-iter-ms)))))))
739+
;; eat
740+
(when ghostel-bench-include-eat
741+
(with-temp-buffer
742+
(let* ((static (ghostel-bench--encode-for-backend static-frame 'eat))
743+
(term (ghostel-bench--make-eat rows cols))
744+
(inhibit-read-only t)
745+
(counter 0))
746+
(eat-term-process-output term static)
747+
(eat-term-redisplay term)
748+
(let ((result
749+
(ghostel-bench--measure
750+
(format "tui-partial/eat/%s" label)
751+
cols partial-iters
752+
(lambda ()
753+
(cl-incf counter)
754+
(eat-term-process-output
755+
term (ghostel-bench--encode-for-backend
756+
(format status-template (format "status #%d" counter))
757+
'eat))
758+
(eat-term-redisplay term)))))
759+
(message " ^ %.0f fps" (/ 1000.0 (plist-get result :per-iter-ms))))
760+
(eat-term-delete term))))
761+
;; term
762+
(when ghostel-bench-include-term
763+
(with-temp-buffer
764+
(let* ((static (ghostel-bench--encode-for-backend static-frame 'term))
765+
(proc (ghostel-bench--make-term rows cols))
766+
(inhibit-read-only t)
767+
(counter 0))
768+
(term-emulate-terminal proc static)
769+
(let ((result
770+
(ghostel-bench--measure
771+
(format "tui-partial/term/%s" label)
772+
cols partial-iters
773+
(lambda ()
774+
(cl-incf counter)
775+
(term-emulate-terminal
776+
proc (ghostel-bench--encode-for-backend
777+
(format status-template (format "status #%d" counter))
778+
'term))))))
779+
(message " ^ %.0f fps" (/ 1000.0 (plist-get result :per-iter-ms))))
780+
(delete-process proc))))))))
781+
660782
;; =========================================================================
661783
;; SECTION 4: Engine micro-benchmarks (bulk parse/render, single call)
662784
;; =========================================================================
@@ -855,6 +977,7 @@ real-world performance (see PTY and streaming benchmarks for that)."
855977
(ghostel-bench--run-pty-scenarios)
856978
(ghostel-bench--run-stream-scenarios)
857979
(ghostel-bench--run-tui-scenarios)
980+
(ghostel-bench--run-tui-partial-scenarios)
858981
(ghostel-bench--run-engine-scenarios)
859982
(ghostel-bench--print-summary))
860983

0 commit comments

Comments
 (0)