test: verify Flex resize through public Ebox reports

This commit is contained in:
Kinneyzhang 2026-08-28 01:51:45 +08:00
parent 94bac1c7c7
commit 81957acdf3
5 changed files with 249 additions and 72 deletions

View File

@ -2,26 +2,48 @@ EMACS ?= emacs
ECSS_DIR ?= ../ecss
TP_DIR ?= ../tp
LOAD_PATH = -L . -L ../ebox -L $(ECSS_DIR) -L $(TP_DIR)
EBOX_NATIVE_RELEASE_DIR = $(shell $(EMACS) -Q --batch $(LOAD_PATH) \
--eval '(setq native-comp-jit-compilation nil)' \
-l ../ebox/ebox-native-reflow.el \
--eval '(princ (expand-file-name (format "../ebox/native/target/%s/release/" (ebox-native-reflow--rust-target))))')
.PHONY: all compile test check checkdoc load performance-evaluator clean
.PHONY: all compile test check checkdoc load performance performance-prepare \
performance-evaluator clean
all: check
compile:
rm -f *.elc tests/*.elc
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
$(EMACS) -Q --batch $(LOAD_PATH) \
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil byte-compile-error-on-warn t)' \
--eval '(load-file "ebox-playground.el")' \
--eval '(byte-compile-file "ebox-playground.el")'
--eval '(byte-compile-file "ebox-playground.el")' \
--eval '(byte-compile-file "scripts/ebox-playground-flex-resize-evaluator.el")'
test: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
$(EMACS) -Q --batch $(LOAD_PATH) \
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
-l tests/ebox-playground-tests.el -f ert-run-tests-batch-and-exit
load: compile
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(require (quote ebox-playground))' \
$(EMACS) -Q --batch $(LOAD_PATH) \
--eval '(setq native-comp-jit-compilation nil)' \
--eval '(require (quote ebox-playground))' \
--eval '(princ "ebox-playground load OK\n")'
performance:
$(MAKE) performance-prepare EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
$(MAKE) performance-evaluator EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
performance-prepare:
$(MAKE) -C $(ECSS_DIR) compile EMACS="$(EMACS)"
$(MAKE) -C $(TP_DIR) compile EMACS="$(EMACS)"
$(MAKE) -C ../ebox compile EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
$(MAKE) compile EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
$(MAKE) -C ../ebox native-build EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
performance-evaluator:
EBOX_NATIVE_REFLOW_MODULE_PATH="$(EBOX_NATIVE_RELEASE_DIR)" \
EMACS="$(EMACS)" ./scripts/flex-resize-evaluator.sh
checkdoc:

View File

@ -28,3 +28,12 @@ The reference fixtures live directly under `examples/`. For example:
```
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` from this directory.
Run `make performance` for the committed Flex resize gate. It prepares ECSS,
TP, Ebox, Playground bytecode, and the Rust module, then renders a read-only
`HEAD:examples/flex-reference.ebox` snapshot without touching the working-tree
fixture. The GUI driver performs five warmups followed by 30 accepted viewport
updates; requested and published widths must match exactly, and both p95 and max
must remain at or below 50ms. For uncontaminated repeated samples, run
`make performance-prepare`, let compilation activity settle, then run
`make performance-evaluator` without rebuilding.

View File

@ -13,14 +13,21 @@
(require 'cl-lib)
(require 'ebox-playground)
(require 'ebox-native-reflow)
(defconst ebox-playground-flex-resize-evaluator--fixture
(expand-file-name "examples/flex-reference.ebox"
ebox-playground-directory)
"Flex fixture used by the performance evaluator.")
(defcustom ebox-playground-flex-resize-evaluator-fixture
(or (getenv "EBOX_PLAYGROUND_FLEX_FIXTURE")
(expand-file-name "examples/flex-reference.ebox"
ebox-playground-directory))
"Read-only Flex fixture used by the performance evaluator."
:type 'file
:group 'ebox-playground)
(defcustom ebox-playground-flex-resize-evaluator-max-seconds 0.5
"Maximum allowed end-to-end time for one settled preview resize."
(defconst ebox-playground-flex-resize-evaluator-warmup-count 5)
(defconst ebox-playground-flex-resize-evaluator-sample-count 30)
(defcustom ebox-playground-flex-resize-evaluator-max-seconds 2.0
"Maximum allowed end-to-end time for the measured resize burst."
:type 'number
:group 'ebox-playground)
@ -37,18 +44,21 @@
(defvar ebox-playground-flex-resize-evaluator--output-lines nil
"Report lines collected during one evaluator run.")
(defun ebox-playground-flex-resize-evaluator--percentile
(samples percentile)
"Return nearest-rank PERCENTILE from numeric SAMPLES."
(unless samples
(error "Flex evaluator cannot summarize empty samples"))
(let* ((ordered (sort (copy-sequence samples) #'<))
(rank (max 1 (ceiling (* percentile (length ordered))))))
(nth (1- rank) ordered)))
(defconst ebox-playground-flex-resize-evaluator--runtime-properties
(delete-dups
(append (mapcar #'cdr ebox-region-types)
'(ebox-content-idx ebox-content-owners ebox-scroll-window)))
"Ebox identity properties omitted from fresh-render comparison.")
(defun ebox-playground-flex-resize-evaluator--preview-buffer ()
"Return the preview buffer for the fixed flex fixture."
(get-buffer
(format "*Ebox Preview: %s*"
ebox-playground-flex-resize-evaluator--fixture)))
(defun ebox-playground-flex-resize-evaluator--line-widths (buffer)
"Return pixel widths for every rendered line in BUFFER."
(with-current-buffer buffer
@ -84,9 +94,10 @@ mounted the split. This setup publication is outside the measured splitter
action, so the action starts from a self-consistent runtime generation rather
than charging initial frame settling to resize."
(let* ((width (ebox-playground--window-viewport-width preview-window))
(state (and (buffer-live-p preview)
(ebox--buffer-render-state preview)))
(published-width (and state (plist-get state :viewport-width))))
(report (ebox-buffer-update-report preview))
(published-width (and report
(or (plist-get report :target-viewport-width)
(plist-get report :viewport-width)))))
(unless (numberp width)
(error "flex preview has no settled viewport width"))
(when (not (equal width published-width))
@ -109,26 +120,37 @@ concurrent cache warming CPU work."
(source nil)
(preview nil)
(preview-window nil)
(resize-advice nil)
(warmup-publications 0)
(resize-count 0)
(resize-elapsed 0.0)
(resize-finished nil)
(action-start nil)
(last-event-elapsed nil)
(resize-publications nil)
(resize-durations nil)
(publication-durations nil)
(requested-widths nil)
(accepted-widths nil)
(observer nil)
(native-report nil)
(playground-library (symbol-file 'ebox-playground-open 'defun))
(evaluator-library
(symbol-file 'ebox-playground-flex-resize-evaluator-run 'defun))
(completed-without-error nil)
(ebox-playground-flex-resize-evaluator--output-lines nil)
(checks nil))
(condition-case-unless-debug error-data
(progn
(delete-other-windows)
(setq native-report (ebox-native-reflow-runtime-report))
(unless (plist-get native-report :layout-ready-p)
(error "Flex evaluator requires the prepared native layout module"))
(unless (file-readable-p
ebox-playground-flex-resize-evaluator-fixture)
(error "Flex evaluator fixture is not readable: %s"
ebox-playground-flex-resize-evaluator-fixture))
(setq source
(find-file ebox-playground-flex-resize-evaluator--fixture))
(find-file ebox-playground-flex-resize-evaluator-fixture))
(ebox-dsl-mode)
(ebox-dsl-render)
(setq preview
(ebox-playground-flex-resize-evaluator--preview-buffer)
(setq preview (ebox-dsl-render)
preview-window (get-buffer-window preview (selected-frame)))
(unless (and (buffer-live-p preview)
(window-live-p preview-window))
@ -139,30 +161,52 @@ concurrent cache warming CPU work."
preview preview-window)
(redisplay t)
(sleep-for 0.2)
(setq resize-advice
(lambda (original &rest arguments)
(setq resize-count (1+ resize-count))
(let ((start (float-time)))
(unwind-protect
(apply original arguments)
(let ((duration (- (float-time) start)))
(setq resize-elapsed (+ resize-elapsed duration))
(push duration resize-durations))
(when action-start
(push (- (float-time) action-start)
resize-publications))
(setq resize-finished t)))))
(advice-add 'ebox-rerender-buffer-with-context
:around resize-advice)
;; Observe only accepted, completed Ebox publications. TP emits its
;; paired report first; filtering by provider keeps this evaluator on
;; the public Ebox boundary and avoids timing/advising the rerender
;; implementation itself.
(setq observer
(lambda (_buffer report)
(when (and (eq (plist-get report :provider) 'ebox)
(eq (plist-get report :stage) 'viewport))
(setq resize-count (1+ resize-count)
resize-finished t)
(when action-start
(push (- (float-time) action-start)
resize-publications))
(push (/ (or (plist-get report :duration-ms) 0.0)
1000.0)
publication-durations)
(push (or (plist-get report :target-viewport-width)
(plist-get report :viewport-width))
accepted-widths))))
(ebox-buffer-set-observer preview observer)
(dotimes (index ebox-playground-flex-resize-evaluator-warmup-count)
(window-resize preview-window (if (zerop (% index 2)) -1 1) t)
(redisplay t)
(sleep-for 0.01))
(setq warmup-publications resize-count
resize-count 0
resize-finished nil
resize-publications nil
publication-durations nil
requested-widths nil
accepted-widths nil)
(let* ((start (float-time))
(old-width
(ebox-playground--window-viewport-width preview-window)))
(setq action-start start)
(dolist (delta '(-4 2 -4 2 -4 2))
(setq last-event-elapsed (- (float-time) start))
(window-resize preview-window delta t)
(redisplay t)
(sleep-for 0.02))
(dotimes (index ebox-playground-flex-resize-evaluator-sample-count)
(let ((delta
(if (< index 28)
(if (zerop (% index 2)) -1 1)
-1)))
(setq last-event-elapsed (- (float-time) start))
(window-resize preview-window delta t)
(push (ebox-playground--window-viewport-width preview-window)
requested-widths)
(redisplay t)
(sleep-for 0.01)))
(let* ((elapsed (- (float-time) start))
(publication-times
(sort (copy-sequence resize-publications) #'<))
@ -173,7 +217,14 @@ concurrent cache warming CPU work."
(first-publication (car publication-times))
(last-publication (car (last publication-times)))
(max-publication
(if resize-durations (apply #'max resize-durations) 0.0))
(if publication-durations
(apply #'max publication-durations)
0.0))
(p95-publication
(if publication-durations
(ebox-playground-flex-resize-evaluator--percentile
publication-durations 0.95)
0.0))
(max-gap
(if publication-gaps (apply #'max publication-gaps) 0.0))
(final-latency
@ -183,10 +234,8 @@ concurrent cache warming CPU work."
(ebox-playground--window-viewport-width preview-window))
(report (ebox-buffer-update-report preview))
(projection-kind (plist-get report :projection-kind))
(native-flex-calls
(and (boundp
'ebox-native-reflow--flex-geometry-call-count)
ebox-native-reflow--flex-geometry-call-count))
(expected-widths (nreverse requested-widths))
(published-widths (nreverse accepted-widths))
(line-widths
(ebox-playground-flex-resize-evaluator--line-widths
preview))
@ -202,16 +251,14 @@ concurrent cache warming CPU work."
;; Compare the published visible scroll
;; window with a fresh render at the same
;; viewport height. Omitting this binding
;; rendered the entire lazy document and made
;; every correct retained resize look unequal.
(ebox-viewport-height
(or (plist-get
(ebox--buffer-render-state preview)
:viewport-height)
(or (plist-get report
:target-viewport-height)
(plist-get report :viewport-height)
36)))
(ebox-render
(ebox-playground-view
ebox-playground-flex-resize-evaluator--fixture
ebox-playground-flex-resize-evaluator-fixture
viewport-width)))))
(fresh-render-matches-p
(and (stringp fresh-rendered)
@ -230,24 +277,55 @@ concurrent cache warming CPU work."
(not (plist-get report :tp-scope-fallback)))))
(setq checks
(list
(ebox-playground-flex-resize-evaluator--check
"compiled evaluator artifacts"
(and (stringp playground-library)
(string-suffix-p ".elc" playground-library)
(stringp evaluator-library)
(string-suffix-p ".elc" evaluator-library))
(format "playground=%S evaluator=%S"
playground-library evaluator-library))
(ebox-playground-flex-resize-evaluator--check
"native module prepared"
(plist-get native-report :layout-ready-p)
(format "path=%S hash=%S"
(plist-get native-report :loaded-module-path)
(plist-get native-report :loaded-module-hash)))
(ebox-playground-flex-resize-evaluator--check
"preview mounted"
(and (buffer-live-p preview)
(window-live-p preview-window))
(format "source=%S preview=%S" source preview))
(ebox-playground-flex-resize-evaluator--check
"warmup publications"
(= warmup-publications
ebox-playground-flex-resize-evaluator-warmup-count)
(format "warmups=%d expected=%d"
warmup-publications
ebox-playground-flex-resize-evaluator-warmup-count))
(ebox-playground-flex-resize-evaluator--check
"intermediate resize updates"
(and (>= resize-count 4)
(and (= resize-count
ebox-playground-flex-resize-evaluator-sample-count)
(= resize-count (length expected-widths))
first-publication
(<= first-publication
ebox-playground-flex-resize-evaluator-max-update-seconds)
(< first-publication last-event-elapsed))
(format "updates=%d first=%.6fs last-event=%.6fs"
(format "updates=%d samples=%d first=%.6fs last-event=%.6fs"
resize-count
ebox-playground-flex-resize-evaluator-sample-count
(or first-publication 0.0)
last-event-elapsed))
(ebox-playground-flex-resize-evaluator--check
"per-update resize budget"
"per-update p95 budget"
(<= p95-publication
ebox-playground-flex-resize-evaluator-max-update-seconds)
(format "p95=%.6fs threshold=%.6fs"
p95-publication
ebox-playground-flex-resize-evaluator-max-update-seconds))
(ebox-playground-flex-resize-evaluator--check
"per-update max budget"
(<= max-publication
ebox-playground-flex-resize-evaluator-max-update-seconds)
(format "max=%.6fs threshold=%.6fs"
@ -277,11 +355,15 @@ concurrent cache warming CPU work."
"viewport was published"
(and (numberp viewport-width)
(not (equal old-width viewport-width))
(cl-every #'numberp published-widths)
(equal expected-widths published-widths)
(equal viewport-width (car (last published-widths)))
(eq (plist-get report :constraint-source)
'viewport)
(plist-get report :runtime-published))
(format "viewport=%S strategy=%S"
viewport-width (plist-get report :strategy)))
(format "viewport=%S expected=%S accepted=%S strategy=%S"
viewport-width expected-widths published-widths
(plist-get report :strategy)))
(ebox-playground-flex-resize-evaluator--check
"retained viewport projection"
projection-retained-p
@ -304,21 +386,22 @@ concurrent cache warming CPU work."
'((default (:background "#FAF7F0"))) )
(format "face-remapping=%S" background))
(ebox-playground-flex-resize-evaluator--check
"resize is millisecond-scale"
"bounded resize burst"
(<= elapsed
ebox-playground-flex-resize-evaluator-max-seconds)
(format "elapsed=%.6fs threshold=%.6fs"
elapsed
ebox-playground-flex-resize-evaluator-max-seconds))))
(push (format
"RESIZE old-viewport-px=%S new-viewport-px=%S total=%.6fs rerender=%.6fs first=%.6fs max-update=%.6fs max-gap=%.6fs final=%.6fs strategy=%S projection=%S axes=%S reconciled=%S native-flex=%S tp-ops=%S cache-hits=%S cache-misses=%S"
old-width viewport-width elapsed resize-elapsed
(or first-publication 0.0) max-publication max-gap
"RESIZE warmups=%d samples=%d old-viewport-px=%S new-viewport-px=%S total=%.6fs p95-update=%.6fs max-update=%.6fs first=%.6fs max-gap=%.6fs final=%.6fs strategy=%S projection=%S axes=%S reconciled=%S tp-ops=%S cache-hits=%S cache-misses=%S"
warmup-publications resize-count old-width viewport-width
elapsed p95-publication max-publication
(or first-publication 0.0) max-gap
(or final-latency -1.0)
(plist-get report :strategy) projection-kind
(plist-get report :viewport-axes)
(plist-get report :reconciled-objects)
native-flex-calls tp-operations
tp-operations
(plist-get report :cache-hit-count)
(plist-get report :cache-miss-count))
ebox-playground-flex-resize-evaluator--output-lines)
@ -333,8 +416,8 @@ concurrent cache warming CPU work."
(push "FAIL evaluator interrupted"
ebox-playground-flex-resize-evaluator--output-lines)
nil))
(when resize-advice
(advice-remove 'ebox-rerender-buffer-with-context resize-advice))
(when (and (buffer-live-p preview) observer)
(ebox-buffer-set-observer preview nil))
(when (buffer-live-p preview)
(kill-buffer preview))
(when (buffer-live-p source)

View File

@ -5,12 +5,42 @@ root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
emacs=${EMACS:-/Applications/Emacs.app/Contents/MacOS/Emacs}
emacsclient=${EMACSCLIENT:-/Applications/Emacs.app/Contents/MacOS/bin/emacsclient}
server="ebox-playground-flex-evaluator-$$"
fixture=$(mktemp "${TMPDIR:-/tmp}/ebox-flex-reference.XXXXXX")
require_fresh_elc() {
source_file=$1
elc_file=$2
if [ ! -r "$elc_file" ] || [ "$source_file" -nt "$elc_file" ]; then
echo "stale or missing evaluator artifact: $elc_file" >&2
echo "run make performance-prepare first" >&2
exit 1
fi
}
cleanup() {
"$emacsclient" -n -s "$server" -e '(kill-emacs 0)' >/dev/null 2>&1 || true
rm -f "$fixture"
}
trap cleanup EXIT INT TERM
require_fresh_elc "$root/ebox-playground.el" "$root/ebox-playground.elc"
require_fresh_elc \
"$root/scripts/ebox-playground-flex-resize-evaluator.el" \
"$root/scripts/ebox-playground-flex-resize-evaluator.elc"
require_fresh_elc "$root/../ebox/ebox.el" "$root/../ebox/ebox.elc"
require_fresh_elc "$root/../ebox/ebox-surface.el" "$root/../ebox/ebox-surface.elc"
require_fresh_elc "$root/../ecss/ecss.el" "$root/../ecss/ecss.elc"
require_fresh_elc "$root/../tp/tp.el" "$root/../tp/tp.elc"
: "${EBOX_NATIVE_REFLOW_MODULE_PATH:?run make performance-prepare first}"
if [ ! -r "$EBOX_NATIVE_REFLOW_MODULE_PATH/libebox_native_reflow.dylib" ]; then
echo "prepared Ebox native module is missing" >&2
exit 1
fi
git -C "$root" show HEAD:examples/flex-reference.ebox >"$fixture"
export EBOX_PLAYGROUND_FLEX_FIXTURE=$fixture
# Keep the evaluator package-only, but reproduce only the four startup values
# that directly affect this render path. Loading the complete user init would
# add unrelated packages, timers, and UI state to the performance gate.
@ -18,12 +48,15 @@ trap cleanup EXIT INT TERM
-L "$root" -L "$root/../ebox" -L "$root/../ecss" \
-L "$root/../tp" \
--eval '(setq load-prefer-newer t
native-comp-jit-compilation nil
ebox-native-reflow-module-path
(getenv "EBOX_NATIVE_REFLOW_MODULE_PATH")
gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6
file-name-handler-alist nil
auto-window-vscroll nil)' \
-l "$root/ebox-playground.el" \
-l "$root/scripts/ebox-playground-flex-resize-evaluator.el"
-l "$root/ebox-playground" \
-l "$root/scripts/ebox-playground-flex-resize-evaluator"
output=$("$emacsclient" -n -s "$server" -c -e \
'(ebox-playground-flex-resize-evaluator-run)' 2>&1)

View File

@ -137,6 +137,36 @@
"EXPLICIT PLACEMENT" "PUBLIC COMPOSITION"))
(should (string-match-p (regexp-quote label) rendered)))))
(ert-deftest ebox-playground-flex-evaluator-uses-public-observation ()
"Keep the Flex resize gate on public Ebox reports without advice."
(let ((source (with-temp-buffer
(insert-file-contents
(expand-file-name
"scripts/ebox-playground-flex-resize-evaluator.el"
ebox-playground-directory))
(buffer-string)))
(runner (with-temp-buffer
(insert-file-contents
(expand-file-name "scripts/flex-resize-evaluator.sh"
ebox-playground-directory))
(buffer-string)))
(makefile (with-temp-buffer
(insert-file-contents
(expand-file-name "Makefile" ebox-playground-directory))
(buffer-string))))
(should (string-match-p "ebox-buffer-set-observer" source))
(should (string-match-p "evaluator-warmup-count 5" source))
(should (string-match-p "evaluator-sample-count 30" source))
(should (string-match-p "per-update p95 budget" source))
(should (string-match-p "EBOX_PLAYGROUND_FLEX_FIXTURE" runner))
(should (string-match-p "show HEAD:examples/flex-reference.ebox" runner))
(should (string-match-p "performance-prepare:" makefile))
(should (string-match-p "native-build" makefile))
(should-not (string-match-p "-l \"\$root/ebox-playground.el\"" runner))
(dolist (private '("ebox--" "advice-add" "advice-remove"
"ebox-native-reflow--"))
(should-not (string-match-p (regexp-quote private) source)))))
(ert-deftest ebox-playground-keeps-layout-in-ebox-source ()
"Keep concrete gallery content out of the generic Elisp runner."
(let ((runner (with-temp-buffer