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-*.
240 lines
9.6 KiB
EmacsLisp
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
|