;;; 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) (pcase-let* ((`(,elapsed . ,result) (etaf-performance-evaluator--timed-call (lambda () (funcall action index))))) (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