perf: keep playground mounts within render burst

This commit is contained in:
Kinneyzhang 2026-08-25 20:55:13 +08:00
parent 09ad2b5ae8
commit 30d3be63db
4 changed files with 86 additions and 13 deletions

View File

@ -84,6 +84,9 @@ frame."
;; keeps byte compilation from treating the protected binding as lexical. ;; keeps byte compilation from treating the protected binding as lexical.
(defvar read-eval) (defvar read-eval)
(declare-function ebox-call-with-render-burst
"ebox-buffer-backend" (function &rest arguments))
(cl-defstruct (etaf-playground-session (cl-defstruct (etaf-playground-session
(:constructor etaf-playground--session-create)) (:constructor etaf-playground--session-create))
"State shared by one source/preview workspace." "State shared by one source/preview workspace."
@ -476,13 +479,11 @@ only file evaluated by the framework."
(setq-local truncate-lines nil)) (setq-local truncate-lines nil))
buffer) buffer)
(defun etaf-playground-mount-example (defun etaf-playground--mount-example-now
(buffer-name name &optional session mount-options) (buffer-name name session mount-options)
"Mount example NAME into BUFFER-NAME and return its buffer. "Build and mount example NAME into BUFFER-NAME.
SESSION supplies authoritative source buffers. MOUNT-OPTIONS is forwarded to
This is the low-level consumer API. `etaf-playground-open-example' adds the `etaf-mount'. The caller owns the complete framework render burst."
source editors and side-by-side workspace around it. MOUNT-OPTIONS is
forwarded to `etaf-mount', including an optional initial viewport."
(let ((buffer (get-buffer-create buffer-name))) (let ((buffer (get-buffer-create buffer-name)))
(when-let ((runtime (etaf-runtime-for-buffer buffer))) (when-let ((runtime (etaf-runtime-for-buffer buffer)))
(with-current-buffer buffer (with-current-buffer buffer
@ -498,6 +499,19 @@ forwarded to `etaf-mount', including an optional initial viewport."
(setq-local etaf-playground-current-example name)) (setq-local etaf-playground-current-example name))
buffer)) buffer))
(defun etaf-playground-mount-example
(buffer-name name &optional session mount-options)
"Mount example NAME into BUFFER-NAME and return its buffer.
This is the low-level consumer API. `etaf-playground-open-example' adds the
source editors and side-by-side workspace around it. MOUNT-OPTIONS is
forwarded to `etaf-mount', including an optional initial viewport. Source
loading, root construction, ETAF publication, and Ebox rendering share one
public framework render burst so GC cannot split an interactive mount."
(ebox-call-with-render-burst
#'etaf-playground--mount-example-now
buffer-name name session mount-options))
(defun etaf-playground--render-error (session error-data) (defun etaf-playground--render-error (session error-data)
"Show ERROR-DATA in SESSION's preview buffer after a failed refresh." "Show ERROR-DATA in SESSION's preview buffer after a failed refresh."
(let ((buffer (etaf-playground-session-preview-buffer session))) (let ((buffer (etaf-playground-session-preview-buffer session)))

View File

@ -75,12 +75,21 @@ never included in SAMPLES. ACTION's result is passed to VERIFY and CLEANUP."
(dotimes (index samples) (dotimes (index samples)
(when setup (funcall setup index)) (when setup (funcall setup index))
(garbage-collect) (garbage-collect)
(pcase-let* ((`(,elapsed . ,result) (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 (etaf-performance-evaluator--timed-call
(lambda () (funcall action index))))) (lambda () (funcall action index)))))
(pcase-let ((`(,elapsed . ,result) timed))
(funcall verify result index) (funcall verify result index)
(when cleanup (funcall cleanup result index)) (when cleanup (funcall cleanup result index))
(push elapsed durations))) (push elapsed durations))))
(let ((statistics (let ((statistics
(etaf-performance-evaluator--statistics (nreverse durations)))) (etaf-performance-evaluator--statistics (nreverse durations))))
(etaf-performance-evaluator--print-statistics label statistics) (etaf-performance-evaluator--print-statistics label statistics)

View File

@ -37,6 +37,24 @@
(should (equal '(-1 0 1 2) (nreverse verifications))) (should (equal '(-1 0 1 2) (nreverse verifications)))
(should (= 3 (plist-get (cdr result) :count)))))) (should (= 3 (plist-get (cdr result) :count))))))
(ert-deftest etaf-performance-evaluator-models-visible-gc-boundary ()
"Measured actions defer GC like an interactive Ebox render burst."
(let ((gc-cons-threshold 1000)
(gc-cons-percentage 0.1)
observed)
(etaf-performance-evaluator--measure
"visible-gc-boundary"
(lambda (_index)
(push (list gc-cons-threshold gc-cons-percentage) observed)
t)
(lambda (result _index) (should result))
:samples 1)
;; Warmup is intentionally outside the timed visible transaction; the
;; measured sample owns the raised threshold and caller settings restore.
(should (equal (car observed) (list most-positive-fixnum 1.0)))
(should (= gc-cons-threshold 1000))
(should (= gc-cons-percentage 0.1))))
(ert-deftest etaf-performance-evaluator-reports-both-hard-budget-failures () (ert-deftest etaf-performance-evaluator-reports-both-hard-budget-failures ()
"Report p95 and max independently when both exceed the hard budget." "Report p95 and max independently when both exceed the hard budget."
(let ((failures (let ((failures

View File

@ -100,6 +100,38 @@ database and mounts a test buffer before running BODY."
" *etaf-playground-display-order*" "research-shelf")) " *etaf-playground-display-order*" "research-shelf"))
(should (equal '(display mount) (mapcar #'car (nreverse calls)))))) (should (equal '(display mount) (mapcar #'car (nreverse calls))))))
(ert-deftest etaf-playground-mount-owns-complete-render-burst ()
"Source construction and ETAF mount share the host render transaction."
(let ((buffer " *etaf-playground-burst-test*")
(inside nil)
calls)
(unwind-protect
(cl-letf (((symbol-function 'ebox-call-with-render-burst)
(lambda (function &rest arguments)
(push 'begin calls)
(setq inside t)
(unwind-protect
(prog1 (apply function arguments)
(push 'end calls))
(setq inside nil))))
((symbol-function 'etaf-playground-read-pair)
(lambda (_name _session)
(should inside)
(push 'root calls)
'test-view))
((symbol-function 'etaf-mount)
(lambda (target view options)
(should inside)
(should (equal view 'test-view))
(should (equal options '(:viewport-width 900)))
(push 'mount calls)
target)))
(should (bufferp
(etaf-playground-mount-example
buffer "test" nil '(:viewport-width 900))))
(should (equal '(begin root mount end) (nreverse calls))))
(when (get-buffer buffer) (kill-buffer buffer)))))
(ert-deftest etaf-playground-compile-builds-the-dependency-graph () (ert-deftest etaf-playground-compile-builds-the-dependency-graph ()
"Integration builds must compile the framework's dependency graph first." "Integration builds must compile the framework's dependency graph first."
(let ((makefile (with-temp-buffer (let ((makefile (with-temp-buffer