ekp/tests/ekp-buffer-live-bench.el
Kinneyzhang 83352c4571 feat!: add source-clean live buffer layout
Replace the physical ekp-region renderer with ekp-buffer text-property projection backed by shared semantic layout plans and stable live-edit transactions.

Optimize the C-backed resize hot path, and add deterministic performance, ERT, GUI, fuzz, release, and documentation coverage for task017 through task032.

BREAKING CHANGE: require ekp-buffer instead of ekp-region and rename module-owned ekp-region-* settings to ekp-buffer-*.
2026-07-30 01:07:25 +08:00

240 lines
9.6 KiB
EmacsLisp

;;; ekp-buffer-live-bench.el --- Live edit benchmarks for EKP -*- lexical-binding: t; -*-
;;; Commentary:
;; Reproducible public-path latency measurements for `ekp-auto-justify-mode'.
;;
;; emacs -Q --batch -L . -L tests -l tests/ekp-buffer-live-bench.el
;;
;; To benchmark the C backend, load it before this file. Batch glyph
;; measurements are internally comparable but are not a replacement for
;; the graphical dynamic verification. Bind `gc-cons-threshold' to
;; `most-positive-fixnum' to separate mutator latency from GC pauses.
;;; Code:
(require 'cl-lib)
(require 'ekp-buffer)
(defconst ekp-buffer-live-bench--width 80)
(defconst ekp-buffer-live-bench--base
(concat
"alpha beta gamma delta epsilon zeta eta theta iota kappa lambda "
"中文混排让前面的完整语义行共同调整 while the active line stays natural"))
(defvar ekp-buffer-live-bench--expected nil)
(defvar ekp-buffer-live-bench--times nil)
(defvar ekp-buffer-live-bench--gc-events 0)
(defvar ekp-buffer-live-bench--gc-seconds 0.0)
(defun ekp-buffer-live-bench--percentile (values percentile)
"Return PERCENTILE from numeric VALUES."
(let* ((sorted (sort (copy-sequence values) #'<))
(index (1- (ceiling (* percentile (length sorted))))))
(nth (max 0 (min index (1- (length sorted)))) sorted)))
(defun ekp-buffer-live-bench--statistics ()
"Return latency statistics for the current scenario."
(let ((times (nreverse ekp-buffer-live-bench--times)))
(list :edits (length times)
:median (ekp-buffer-live-bench--percentile times 0.50)
:p95 (ekp-buffer-live-bench--percentile times 0.95)
:p99 (ekp-buffer-live-bench--percentile times 0.99)
:max (apply #'max times)
:gc-events ekp-buffer-live-bench--gc-events
:gc-ms (* 1000 ekp-buffer-live-bench--gc-seconds))))
(defun ekp-buffer-live-bench--record-command (command expected)
"Run public COMMAND, verify EXPECTED source, and record its latency."
(let ((started (float-time))
(gcs-before gcs-done)
(gc-before gc-elapsed))
(funcall command)
(run-hooks 'post-command-hook)
(push (* 1000 (- (float-time) started))
ekp-buffer-live-bench--times)
(cl-incf ekp-buffer-live-bench--gc-events
(- gcs-done gcs-before))
(cl-incf ekp-buffer-live-bench--gc-seconds
(- gc-elapsed gc-before)))
(setq ekp-buffer-live-bench--expected expected)
(unless (equal (substring-no-properties (buffer-string)) expected)
(error "Live benchmark source mismatch"))
(when (overlays-in (point-min) (point-max))
(error "Live benchmark created an overlay")))
(defun ekp-buffer-live-bench--insert (character)
"Insert CHARACTER through `self-insert-command'."
(ekp-buffer-live-bench--record-command
(lambda ()
(let ((last-command-event character))
(call-interactively #'self-insert-command)))
(concat ekp-buffer-live-bench--expected (string character))))
(defun ekp-buffer-live-bench--delete-backward ()
"Delete one character through `delete-backward-char'."
(ekp-buffer-live-bench--record-command
(lambda () (call-interactively #'delete-backward-char))
(substring ekp-buffer-live-bench--expected 0 -1)))
(defun ekp-buffer-live-bench--append-workload ()
"Type enough mixed prose to cross several semantic boundaries."
(dotimes (_ 3)
(mapc #'ekp-buffer-live-bench--insert
(string-to-list
(concat
" extraordinary editing continues smoothly 中文拉丁混排"
" and all earlier complete lines may move together")))))
(defun ekp-buffer-live-bench--cache-workload ()
"Revisit two historical paragraph states repeatedly."
(dotimes (_ 80)
(ekp-buffer-live-bench--insert ?x)
(ekp-buffer-live-bench--delete-backward)))
(defun ekp-buffer-live-bench--motion-workload ()
"Move point across semantic boundaries without changing projection."
(let* ((state ekp-buffer--live-state)
(transaction ekp-buffer--live-edit)
(projection (buffer-substring (point-min) (point-max)))
(active (ekp-buffer--live-state-active-index state))
(signatures (ekp-buffer--live-state-signatures state))
(prefix-end (ekp-buffer--live-state-prefix-end state))
(prefix-position (marker-position prefix-end))
(edit-end
(and transaction
(ekp-buffer--live-edit-edit-end transaction)))
(edit-position (and edit-end (marker-position edit-end))))
(dotimes (_ 48)
(ekp-buffer-live-bench--record-command
(lambda () (call-interactively #'backward-char))
ekp-buffer-live-bench--expected))
(dotimes (_ 48)
(ekp-buffer-live-bench--record-command
(lambda () (call-interactively #'forward-char))
ekp-buffer-live-bench--expected))
(unless (and (eq ekp-buffer--live-state state)
(= (ekp-buffer--live-state-active-index state) active)
(eq (ekp-buffer--live-state-signatures state) signatures)
(eq ekp-buffer--live-edit transaction)
(eq (ekp-buffer--live-state-prefix-end state) prefix-end)
(= (marker-position prefix-end) prefix-position)
(or (null edit-end)
(and (eq (ekp-buffer--live-edit-edit-end transaction)
edit-end)
(= (marker-position edit-end) edit-position)))
(equal-including-properties
(buffer-substring (point-min) (point-max))
projection))
(error "Point motion changed the live projection"))))
(defun ekp-buffer-live-bench--boundary-workload ()
"Create and remove hard boundaries through public commands."
(dotimes (_ 24)
(ekp-buffer-live-bench--record-command
(lambda () (call-interactively #'newline))
(concat ekp-buffer-live-bench--expected "\n"))
(ekp-buffer-live-bench--delete-backward)))
(defun ekp-buffer-live-bench--instrument (workload)
"Run WORKLOAD and return planner/cache counters."
(let ((plan-calls 0)
(cache-gets 0)
(cache-hits 0)
(original-plan (symbol-function 'ekp-layout-plan))
(original-get (symbol-function 'ekp-buffer--live-cache-get)))
(cl-letf (((symbol-function 'ekp-layout-plan)
(lambda (&rest arguments)
(cl-incf plan-calls)
(apply original-plan arguments)))
((symbol-function 'ekp-buffer--live-cache-get)
(lambda (key)
(cl-incf cache-gets)
(let ((plan (funcall original-get key)))
(when plan (cl-incf cache-hits))
plan))))
(funcall workload))
(list :plan-calls plan-calls
:cache-hits cache-hits
:cache-misses (- cache-gets cache-hits))))
(defun ekp-buffer-live-bench--scenario (name workload)
"Run named live-edit WORKLOAD and return its measurements."
(with-temp-buffer
(text-mode)
(insert ekp-buffer-live-bench--base)
(goto-char (point-max))
(cl-letf (((symbol-function 'ekp-buffer--window-pixel)
(lambda (&optional _window)
ekp-buffer-live-bench--width)))
(ekp-auto-justify-mode 1)
(let ((ekp-buffer-live-bench--expected
(substring-no-properties (buffer-string)))
(ekp-buffer-live-bench--times nil)
(ekp-buffer-live-bench--gc-events 0)
(ekp-buffer-live-bench--gc-seconds 0.0))
(garbage-collect)
(unwind-protect
(append (list :name name)
(ekp-buffer-live-bench--statistics-after
workload))
(ekp-auto-justify-mode -1))))))
(defun ekp-buffer-live-bench--statistics-after (workload)
"Run WORKLOAD with instrumentation and return combined statistics."
(let ((counters (ekp-buffer-live-bench--instrument workload)))
(append (ekp-buffer-live-bench--statistics)
counters
(list :source-ok t
:conflicts (length ekp-buffer--conflicts)
:cache-size (length ekp-buffer--live-plan-cache)))))
(defun ekp-buffer-live-bench--format (result)
"Format live benchmark RESULT as one report row."
(format
"%-15s %5d %8.3f %8.3f %8.3f %8.3f %6d %6d %6d %3d %7.2f %5d %s"
(plist-get result :name)
(plist-get result :edits)
(plist-get result :median)
(plist-get result :p95)
(plist-get result :p99)
(plist-get result :max)
(plist-get result :plan-calls)
(plist-get result :cache-hits)
(plist-get result :cache-misses)
(plist-get result :gc-events)
(plist-get result :gc-ms)
(plist-get result :cache-size)
(if (<= (plist-get result :p99) 16.0) "PASS" "MISS")))
;;;###autoload
(defun ekp-buffer-live-bench-run ()
"Run live-edit scenarios and print percentile/caching evidence."
(let ((scenarios
`(("append" . ,#'ekp-buffer-live-bench--append-workload)
("cache-revisit" . ,#'ekp-buffer-live-bench--cache-workload)
("point-motion" . ,#'ekp-buffer-live-bench--motion-workload)
("hard-boundary" . ,#'ekp-buffer-live-bench--boundary-workload))))
(princ
(format "EKP live edit benchmark: engine=%s width=%dpx\n"
(if (and ekp-use-c-module (ekp--c-available-p)) "C" "elisp")
ekp-buffer-live-bench--width))
(princ
(concat
"scenario edits median p95 p99 max"
" plans hits misses gc gc-ms cache frame\n"))
(dolist (scenario scenarios)
(princ
(concat
(ekp-buffer-live-bench--format
(ekp-buffer-live-bench--scenario
(car scenario) (cdr scenario)))
"\n")))))
(ekp-buffer-live-bench-run)
(provide 'ekp-buffer-live-bench)
;;; ekp-buffer-live-bench.el ends here