etaf-playground/scripts/benchmark-research-shelf.el
2026-08-25 20:55:13 +08:00

562 lines
27 KiB
EmacsLisp

;;; benchmark-research-shelf.el --- Cross-package ETAF latency gate -*- lexical-binding: t; -*-
;;; Commentary:
;; Generic evaluator primitives exercised with Research Shelf as one fixed,
;; deterministic, real cross-package workload. Trace assertions consume only
;; public `etaf-performance' records; application concepts stay out of the
;; recorder itself.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'etaf-playground)
(require 'etaf-performance)
(require 'ebox-native-reflow)
(defvar etaf-research-shelf-database-file)
(defvar etaf-research-shelf-fixture-size)
(defvar etaf-research-shelf-page-size)
(declare-function etaf-research-shelf--ensure-database
"../examples/research-shelf")
(defconst etaf-performance-evaluator-sample-count 20)
(defconst etaf-performance-evaluator-latency-budget-ms 50.0)
(defconst etaf-performance-evaluator-scenario-budgets-ms
'(("theme-toggle" :p95 90.0 :max 110.0))
"User-approved scenario budgets overriding the default latency budget.")
(defconst etaf-performance-evaluator-overhead-budget-ms 2.0)
(defconst etaf-performance-evaluator-fixture-size 256)
(defconst etaf-performance-evaluator-page-size 12)
(defconst etaf-performance-evaluator-viewport-width 1413)
(defconst etaf-performance-evaluator-viewport-height 62)
(defun etaf-performance-evaluator--percentile (samples percentile)
"Return nearest-rank PERCENTILE from non-empty numeric SAMPLES."
(unless samples (error "Cannot summarize an empty performance sample"))
(let* ((ordered (sort (copy-sequence samples) #'<))
(rank (max 1 (ceiling (* percentile (length ordered))))))
(nth (1- rank) ordered)))
(defun etaf-performance-evaluator--statistics (samples)
"Return min, p50, p95, and max statistics for SAMPLES."
(list :count (length samples) :min (apply #'min samples)
:p50 (etaf-performance-evaluator--percentile samples 0.50)
:p95 (etaf-performance-evaluator--percentile samples 0.95)
:max (apply #'max samples)))
(defun etaf-performance-evaluator--print-statistics (label statistics)
"Print LABEL and STATISTICS in stable machine-readable form."
(princ (format (concat "perf-scenario=%s samples=%d min=%.3fms "
"p50=%.3fms p95=%.3fms max=%.3fms\n")
label (plist-get statistics :count)
(plist-get statistics :min) (plist-get statistics :p50)
(plist-get statistics :p95) (plist-get statistics :max))))
(defun etaf-performance-evaluator--timed-call (function)
"Call FUNCTION and return `(ELAPSED-MS . RESULT)'."
(let ((started (float-time)) result)
(setq result (funcall function))
(cons (* 1000.0 (- (float-time) started)) result)))
(cl-defun etaf-performance-evaluator--measure
(label action verify &key setup cleanup
(samples etaf-performance-evaluator-sample-count))
"Warm, then measure ACTION under LABEL and verify every result.
ACTION, VERIFY, SETUP, and CLEANUP receive an index. Warmup uses -1 and is
never included in SAMPLES. ACTION's result is passed to VERIFY and CLEANUP."
(let (durations)
(when setup (funcall setup -1))
;; Batch Emacs does not run the interactive 0.2s deferred-GC timer between
;; synthetic inputs. Settle that idle work before, never inside, samples.
(garbage-collect)
(let ((result (funcall action -1)))
(funcall verify result -1)
(when cleanup (funcall cleanup result -1)))
(dotimes (index samples)
(when setup (funcall setup index))
(garbage-collect)
(let (timed)
;; Interactive Ebox render bursts postpone GC until after visible
;; completion. A batch burst restores the process threshold
;; synchronously, which can make the harness collect before its Lisp
;; caller returns. Stop the timer before restoring that batch-only
;; threshold; the explicit collection above still settles every run.
(let ((gc-cons-threshold most-positive-fixnum)
(gc-cons-percentage 1.0))
(setq timed
(etaf-performance-evaluator--timed-call
(lambda () (funcall action index)))))
(pcase-let ((`(,elapsed . ,result) timed))
(funcall verify result index)
(when cleanup (funcall cleanup result index))
(push elapsed durations))))
(let ((statistics
(etaf-performance-evaluator--statistics (nreverse durations))))
(etaf-performance-evaluator--print-statistics label statistics)
(cons label statistics))))
(defun etaf-performance-evaluator--text (buffer)
"Return BUFFER's rendered text without properties."
(with-current-buffer buffer
(substring-no-properties (buffer-string))))
(defun etaf-performance-evaluator--surface-invariant (runtime buffer label)
"Assert committed RUNTIME/publication invariants in BUFFER for LABEL."
(unless (and (buffer-live-p (get-buffer buffer))
(eq runtime (etaf-runtime-for-buffer buffer)))
(error "%s: mounted Runtime/buffer invariant failed" label))
(let ((generation (etaf-runtime-current-generation runtime)))
(unless (and (etaf-generation-p generation)
(integerp (etaf-runtime-generation runtime))
(> (etaf-runtime-generation runtime) 0)
(integerp (etaf-generation-root-semantic-id generation))
(etaf-runtime-handler-entries runtime)
(etaf-runtime-host-props-entries runtime))
(error "%s: committed generation/publication structure is incomplete"
label)))
(unless (> (buffer-size (get-buffer buffer)) 0)
(error "%s: publication produced an empty visible buffer" label))
t)
(defun etaf-performance-evaluator--load-workload ()
"Load the deterministic integration workload companion."
(let* ((source (expand-file-name "examples/research-shelf.el"
default-directory))
(compiled (concat (file-name-sans-extension source) ".elc")))
(load (if (and (file-readable-p compiled)
(not (file-newer-than-file-p source compiled)))
compiled source)
nil nil t)))
(defun etaf-performance-evaluator--prepare-database (database)
"Prepare DATABASE outside the measured mount operation."
(let ((etaf-research-shelf-database-file database)
(etaf-research-shelf-fixture-size
etaf-performance-evaluator-fixture-size)
(etaf-research-shelf-page-size etaf-performance-evaluator-page-size))
(etaf-research-shelf--ensure-database))
(unless (and (file-exists-p database)
(> (file-attribute-size (file-attributes database)) 0))
(error "Prepared SQLite fixture was not created: %s" database)))
(defun etaf-performance-evaluator--verify-runtime-accelerator ()
"Require the optional Ebox native runtime accelerator."
(unless (and (require 'ebox-native-reflow nil t)
(ebox-native-reflow-layout-ready-p))
(error "Performance evaluator requires the Ebox native runtime"))
(princ "perf-runtime-accelerator=ready\n")
t)
(defun etaf-performance-evaluator--verify-environment (environment)
"Require ENVIRONMENT to be suitable for an absolute latency gate."
(let* ((power (plist-get environment :power-state))
(source (or (plist-get power :source) 'unknown))
(low-power (or (plist-get power :low-power-mode) 'unknown))
(load (plist-get environment :load-average)))
(princ (format "perf-environment power-source=%s low-power-mode=%s load=%S\n"
source low-power load))
(when (eq low-power 'on)
(error (concat "Performance evaluator requires low-power mode off; "
"current source=%s")
source)))
environment)
(defun etaf-performance-evaluator--mount (buffer)
"Mount the fixed workload in BUFFER at the fixed viewport."
(etaf-playground-mount-example
buffer "research-shelf" nil
(list :viewport-width etaf-performance-evaluator-viewport-width
:viewport-height etaf-performance-evaluator-viewport-height))
(etaf-runtime-for-buffer buffer))
(defun etaf-performance-evaluator--close-buffer (buffer)
"Unmount and close BUFFER when it is live."
(when (get-buffer buffer) (etaf-playground-close buffer)))
(defun etaf-performance-evaluator--visible-match-p (buffer regexp)
"Return non-nil when BUFFER's visible text matches REGEXP."
(string-match-p regexp (etaf-performance-evaluator--text buffer)))
(defun etaf-performance-evaluator--trace-categories (records)
"Return unique stage categories present in performance RECORDS."
(let (categories)
(dolist (operation records)
(dolist (stage (etaf-performance-operation-stages operation))
(cl-pushnew (etaf-performance-stage-category stage) categories)))
categories))
(defun etaf-performance-evaluator--verify-trace-records (records)
"Verify generic cross-package RECORDS are successful and complete."
(unless records (error "Trace-on run produced no etaf-performance records"))
(dolist (operation records)
(unless (eq (etaf-performance-operation-status operation) 'success)
(error "Trace operation %s (%s) finished with status %s"
(etaf-performance-operation-id operation)
(etaf-performance-operation-label operation)
(etaf-performance-operation-status operation)))
(dolist (stage (etaf-performance-operation-stages operation))
(unless (eq (etaf-performance-stage-status stage) 'success)
(error "Trace stage %s/%s finished with status %s"
(etaf-performance-stage-category stage)
(etaf-performance-stage-function stage)
(etaf-performance-stage-status stage)))))
(let ((categories (etaf-performance-evaluator--trace-categories records)))
(dolist (required '(runtime ebox tp sqlite))
(unless (memq required categories)
(error "Trace records lack required %s stage; present=%S"
required categories)))
(setq categories
(sort categories (lambda (left right)
(string< (symbol-name left)
(symbol-name right)))))
(princ (format "perf-trace records=%d categories=%S status=success\n"
(length records) categories))))
(defun etaf-performance-evaluator--print-slowest-records (records)
"Print the two slowest generic operation RECORDS per kind/label."
(let ((groups (make-hash-table :test #'equal)) selected)
(dolist (operation records)
(let ((key (cons (etaf-performance-operation-kind operation)
(etaf-performance-operation-label operation))))
(puthash key (cons operation (gethash key groups)) groups)))
(maphash
(lambda (_key operations)
(setq selected
(append
(seq-take
(sort operations
(lambda (left right)
(> (etaf-performance-operation-elapsed left)
(etaf-performance-operation-elapsed right))))
2)
selected)))
groups)
(dolist
(operation
(sort selected
(lambda (left right)
(> (etaf-performance-operation-elapsed left)
(etaf-performance-operation-elapsed right)))))
(let* ((gc-count
(- (etaf-performance-operation-gc-count-after operation)
(etaf-performance-operation-gc-count-before operation)))
(gc-ms
(* 1000.0
(- (etaf-performance-operation-gc-elapsed-after operation)
(etaf-performance-operation-gc-elapsed-before operation))))
(stages
(mapcar
(lambda (entry)
(cons (plist-get entry :category)
(plist-get entry :exclusive-ms)))
(etaf-performance-operation-stage-summary operation)))
(ebox
(plist-get (etaf-performance-operation-metadata operation) :ebox)))
(princ
(format (concat "perf-slowest id=%d kind=%s label=%S elapsed=%.3fms "
"gc=%d/%.3fms stages=%S ebox=%S\n")
(etaf-performance-operation-id operation)
(etaf-performance-operation-kind operation)
(etaf-performance-operation-label operation)
(etaf-performance-operation-elapsed operation)
gc-count gc-ms stages ebox))))))
(defun etaf-performance-evaluator--select-ref (runtime ref)
"Select workload row REF through RUNTIME's public event path."
(etaf-dispatch-event runtime ref 'press)
ref)
(defun etaf-performance-evaluator--measure-overhead (runtime buffer)
"Measure trace overhead for RUNTIME with visible state in BUFFER."
(let (signed-deltas absolute-deltas traced-samples plain-samples)
(dotimes (index etaf-performance-evaluator-sample-count)
(let ((order (if (zerop (% index 2)) '(nil t) '(t nil))) plain traced)
(dolist (trace-p order)
(etaf-performance-mode (if trace-p 1 -1))
;; Restore equivalent state outside the timed region; AB/BA order
;; cancels drift without retries or skipped real work.
(garbage-collect)
(etaf-focus runtime 'research-shelf-row-1)
(when trace-p (etaf-performance-clear))
(let ((before (etaf-focused-host-ref runtime))
(elapsed
(let ((gc-cons-threshold most-positive-fixnum)
(gc-cons-percentage 1.0))
(car (etaf-performance-evaluator--timed-call
(lambda () (etaf-focus-next runtime)))))))
(unless (and (buffer-live-p (get-buffer buffer))
(not (equal before
(etaf-focused-host-ref runtime))))
(error "Trace-overhead: equivalent focus-navigation work failed"))
(if trace-p
(progn (setq traced elapsed)
(unless (etaf-performance-records)
(error "Trace-overhead: trace-on sample recorded nothing")))
(setq plain elapsed))))
(push plain plain-samples)
(push traced traced-samples)
(push (- traced plain) signed-deltas)
(push (abs (- traced plain)) absolute-deltas)))
(etaf-performance-mode -1)
(let ((signed
(etaf-performance-evaluator--statistics (nreverse signed-deltas)))
(jitter
(etaf-performance-evaluator--statistics
(nreverse absolute-deltas))))
(princ (format (concat "perf-trace-overhead pairs=%d off-p50=%.3fms "
"on-p50=%.3fms signed-p50=%.3fms "
"signed-p95=%.3fms jitter-p50=%.3fms "
"budget=%.3fms\n")
etaf-performance-evaluator-sample-count
(etaf-performance-evaluator--percentile plain-samples .5)
(etaf-performance-evaluator--percentile traced-samples .5)
(plist-get signed :p50) (plist-get signed :p95)
(plist-get jitter :p50)
etaf-performance-evaluator-overhead-budget-ms))
(list :signed signed :jitter jitter))))
(defun etaf-performance-evaluator--latency-failures (results)
"Return hard latency budget failures from scenario RESULTS."
(cl-loop for (label . statistics) in results append
(let* ((scenario
(cdr (assoc label
etaf-performance-evaluator-scenario-budgets-ms)))
(p95-budget
(or (plist-get scenario :p95)
etaf-performance-evaluator-latency-budget-ms))
(max-budget
(or (plist-get scenario :max)
etaf-performance-evaluator-latency-budget-ms))
failures)
(when (> (plist-get statistics :p95)
p95-budget)
(push (format "%s p95 %.3fms > %.3fms" label
(plist-get statistics :p95)
p95-budget)
failures))
(when (> (plist-get statistics :max)
max-budget)
(push (format "%s max %.3fms > %.3fms" label
(plist-get statistics :max)
max-budget)
failures))
(nreverse failures))))
(defun etaf-performance-evaluator-run ()
"Run the fixed cross-package evaluator and return non-nil on success."
(etaf-performance-evaluator--load-workload)
(etaf-performance-evaluator--verify-environment
(etaf-performance-environment-data))
(let* ((database (make-temp-file "etaf-perf-fixture-" nil ".sqlite"))
(buffer " *etaf-cross-package-perf*")
(mount-buffer " *etaf-cross-package-mount-perf*")
(etaf-research-shelf-database-file database)
(etaf-research-shelf-fixture-size
etaf-performance-evaluator-fixture-size)
(etaf-research-shelf-page-size etaf-performance-evaluator-page-size)
(etaf-performance-max-records 10000)
results runtime trace-records overhead failures)
(unwind-protect
(progn
(etaf-performance-evaluator--prepare-database database)
(etaf-performance-evaluator--verify-runtime-accelerator)
;; Latency samples measure the product path without observer work.
;; Trace behavior and overhead have separate, explicit gates below.
(etaf-performance-mode -1)
(etaf-performance-clear)
(push
(etaf-performance-evaluator--measure
"prepared-database-mount"
(lambda (_index)
(etaf-performance-evaluator--mount mount-buffer))
(lambda (mounted _index)
(etaf-performance-evaluator--surface-invariant
mounted mount-buffer "prepared-database-mount")
(let ((text (etaf-performance-evaluator--text mount-buffer)))
(unless (and (string-match-p "256 items · SQLite-backed" text)
(string-match-p "Page 1 / 22" text))
(error "Prepared-database-mount: fixture/page invariant failed"))))
:cleanup (lambda (_result _index)
(etaf-performance-evaluator--close-buffer mount-buffer)))
results)
(setq runtime (etaf-performance-evaluator--mount buffer))
(etaf-performance-evaluator--surface-invariant runtime buffer "setup")
(push
(etaf-performance-evaluator--measure
"row-selection"
(lambda (index)
(let* ((ref (if (zerop (% (1+ index) 2))
'research-shelf-row-1 'research-shelf-row-2))
(before (etaf-runtime-generation runtime)))
(etaf-performance-evaluator--select-ref runtime ref)
(list ref before (etaf-runtime-generation runtime))))
(lambda (result _index)
(pcase-let ((`(,ref ,before ,after) result))
(unless (and (>= after before)
(string-match-p
"selected"
(or (plist-get
(etaf-runtime-host-props-for runtime ref)
:class) "")))
(error "Row-selection: selection/generation invariant failed")))
(etaf-performance-evaluator--surface-invariant
runtime buffer "row-selection")))
results)
(push
(etaf-performance-evaluator--measure
"theme-toggle"
(lambda (_index)
(let ((before (etaf-runtime-generation runtime)))
(etaf-dispatch-event runtime 'research-shelf-theme-toggle 'press)
(list before (etaf-runtime-generation runtime))))
(lambda (result _index)
(unless (and (> (cadr result) (car result))
(etaf-performance-evaluator--visible-match-p
buffer "\\(Dark theme\\|Light theme\\)"))
(error "Theme-toggle: theme/generation invariant failed"))
(etaf-performance-evaluator--surface-invariant
runtime buffer "theme-toggle")))
results)
(push
(etaf-performance-evaluator--measure
"filter-query"
(lambda (index)
(let* ((reading-p (zerop (% (1+ index) 2)))
(ref (if reading-p 'research-shelf-filter-reading
'research-shelf-filter-all))
(before (etaf-runtime-generation runtime)))
(etaf-dispatch-event runtime ref 'press)
(list reading-p before (etaf-runtime-generation runtime))))
(lambda (result _index)
(unless (and (> (nth 2 result) (nth 1 result))
(etaf-performance-evaluator--visible-match-p
buffer (if (car result) "Showing Reading"
"Showing All")))
(error "Filter-query: query/generation invariant failed"))
(etaf-performance-evaluator--surface-invariant
runtime buffer "filter-query")))
results)
(etaf-dispatch-event runtime 'research-shelf-filter-all 'press)
(push
(etaf-performance-evaluator--measure
"pagination"
(lambda (_index)
(let* ((on-first (etaf-performance-evaluator--visible-match-p
buffer "Page 1 / 22"))
(ref (if on-first 'research-shelf-page-next
'research-shelf-page-previous))
(before (etaf-runtime-generation runtime)))
(etaf-dispatch-event runtime ref 'press)
(list (if on-first 2 1) before
(etaf-runtime-generation runtime))))
(lambda (result _index)
(unless (and (> (nth 2 result) (nth 1 result))
(etaf-performance-evaluator--visible-match-p
buffer (format "Page %d / 22" (car result))))
(error "Pagination: page/generation invariant failed"))
(etaf-performance-evaluator--surface-invariant
runtime buffer "pagination")))
results)
(unless (etaf-performance-evaluator--visible-match-p
buffer "Page 1 / 22")
(etaf-dispatch-event runtime 'research-shelf-page-previous 'press))
(let ((refs '(research-shelf-row-2 research-shelf-row-4
research-shelf-row-5 research-shelf-row-8
research-shelf-row-9 research-shelf-row-10)))
(push
(etaf-performance-evaluator--measure
"progress-mutation"
(lambda (_index)
(let ((before (etaf-runtime-generation runtime)))
(etaf-dispatch-event runtime 'research-shelf-progress 'press)
(list before (etaf-runtime-generation runtime))))
(lambda (result _index)
(unless (and (> (cadr result) (car result))
(etaf-performance-evaluator--visible-match-p
buffer "Progress saved"))
(error "Progress-mutation: persistence invariant failed"))
(etaf-performance-evaluator--surface-invariant
runtime buffer "progress-mutation"))
:setup (lambda (index)
(etaf-performance-evaluator--select-ref
runtime (nth (% (1+ index) (length refs)) refs))))
results))
(push
(etaf-performance-evaluator--measure
"focus-navigation"
(lambda (_index)
(let ((before (etaf-focused-host-ref runtime)))
(etaf-focus-next runtime)
(list before (etaf-focused-host-ref runtime))))
(lambda (result _index)
(unless (and (cadr result)
(not (equal (car result) (cadr result))))
(error "Focus-navigation: focus did not advance"))
(etaf-performance-evaluator--surface-invariant
runtime buffer "focus-navigation")))
results)
;; Capture one representative cross-package trace after the latency
;; samples. This validates coverage without folding observer work
;; into the product latency distribution.
(etaf-performance-mode 1)
(etaf-performance-clear)
(etaf-performance-evaluator--select-ref
runtime 'research-shelf-row-1)
(etaf-performance-evaluator--select-ref
runtime 'research-shelf-row-2)
(etaf-dispatch-event runtime 'research-shelf-theme-toggle 'press)
(etaf-dispatch-event runtime 'research-shelf-theme-toggle 'press)
(etaf-dispatch-event runtime 'research-shelf-filter-reading 'press)
(etaf-dispatch-event runtime 'research-shelf-filter-all 'press)
(etaf-dispatch-event runtime 'research-shelf-page-next 'press)
(etaf-dispatch-event runtime 'research-shelf-page-previous 'press)
(etaf-performance-evaluator--select-ref
runtime 'research-shelf-row-2)
(etaf-dispatch-event runtime 'research-shelf-progress 'press)
(etaf-focus-next runtime)
(setq results (nreverse results)
trace-records (etaf-performance-records))
(etaf-performance-evaluator--verify-trace-records trace-records)
(etaf-performance-evaluator--print-slowest-records trace-records)
(etaf-performance-mode -1)
(setq overhead
(etaf-performance-evaluator--measure-overhead runtime buffer)
failures (etaf-performance-evaluator--latency-failures results))
(when (> (abs (plist-get (plist-get overhead :signed) :p50))
etaf-performance-evaluator-overhead-budget-ms)
(push (format "trace signed |p50| %.3fms > %.3fms"
(abs (plist-get (plist-get overhead :signed) :p50))
etaf-performance-evaluator-overhead-budget-ms)
failures))
(if failures
(let ((failure-count (length failures)))
(princ "etaf-cross-package-perf FAIL\n")
(dolist (failure (reverse failures))
(princ (format "PERF-GATE-FAIL: %s\n" failure)))
(error "Cross-package performance gate failed (%d conditions)"
failure-count))
(princ "etaf-cross-package-perf PASS\n") t))
(etaf-performance-mode -1)
(etaf-performance-evaluator--close-buffer mount-buffer)
(etaf-performance-evaluator--close-buffer buffer)
(when (file-exists-p database) (delete-file database)))))
(defun etaf-performance-evaluator-batch ()
"Batch entry point for the cross-package performance evaluator."
(condition-case condition
(progn (etaf-performance-evaluator-run) (kill-emacs 0))
(error
(princ (format "PERF-EVALUATOR-ERROR: %s\n"
(error-message-string condition)))
(kill-emacs 1))))
(provide 'benchmark-research-shelf)
;;; benchmark-research-shelf.el ends here