perf: coalesce viewport resize delivery

This commit is contained in:
Kinneyzhang 2026-08-25 22:45:13 +08:00
parent 175ae2ed4e
commit d58ec51650
9 changed files with 204 additions and 53 deletions

View File

@ -431,6 +431,12 @@ context. For pure rendering, callers may instead bind `ebox-viewport-width`
in pixels and `ebox-viewport-height` in lines around the render. Do not mutate in pixels and `ebox-viewport-height` in lines around the render. Do not mutate
a published node tree in place. a published node tree in place.
Visible mounted buffers follow their displaying window automatically. Ebox
coalesces continuous size events and publishes only the latest sample after
`ebox-viewport-resize-delay` (0.05 seconds by default).
`ebox-viewport-window-width` returns the same display-safe pixel width used by
that controller, so hosts must not install a second resize hook or width rule.
## 7. Selectors ## 7. Selectors
`ebox-selector-parse` delegates to ECSS and returns its structured AST. `ebox-selector-parse` delegates to ECSS and returns its structured AST.
@ -516,6 +522,7 @@ The main customization variables are:
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8; `ebox-scroll-lazy-idle-prefetch-lines` 128; `ebox-scroll-lazy-idle-prefetch-slice-lines` 16; `ebox-scroll-lazy-idle-prefetch-delay` 0.15 | | Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8; `ebox-scroll-lazy-idle-prefetch-lines` 128; `ebox-scroll-lazy-idle-prefetch-slice-lines` 16; `ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
| Runtime prewarm | `ebox-runtime-idle-prewarm` t; `ebox-runtime-idle-prewarm-delay` 0.1; `ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0; `ebox-runtime-idle-prewarm-slice-size` 32; `ebox-native-buffer-scroll` t (initial/visible-window handoff only; strict root-owner proof) | | Runtime prewarm | `ebox-runtime-idle-prewarm` t; `ebox-runtime-idle-prewarm-delay` 0.1; `ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0; `ebox-runtime-idle-prewarm-slice-size` 32; `ebox-native-buffer-scroll` t (initial/visible-window handoff only; strict root-owner proof) |
| Predicted reflow | `ebox-runtime-idle-reflow-cache-prewarm` t; `ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 | | Predicted reflow | `ebox-runtime-idle-reflow-cache-prewarm` t; `ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
| Viewport delivery | `ebox-viewport-resize-delay` 0.05; continuous window events replace one pending sample per mounted buffer |
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`; `ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2; `ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB; `ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB; `ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB; `ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24; `ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 | | Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`; `ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2; `ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB; `ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB; `ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB; `ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24; `ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
| Visual verification | `ebox-visual-check-output-dir` points to the temporary directory used for optional screenshots and reports. | | Visual verification | `ebox-visual-check-output-dir` points to the temporary directory used for optional screenshots and reports. |
@ -583,7 +590,7 @@ style-rule functions immediately below are public module-level style APIs.
| `ebox-flex`, `ebox-flex-item` | Build flex containers and item metadata. | | `ebox-flex`, `ebox-flex-item` | Build flex containers and item metadata. |
| `ebox-grid`, `ebox-grid-fr`, `ebox-grid-item` | Build Grid layouts, fractional tracks, and placement metadata. | | `ebox-grid`, `ebox-grid-fr`, `ebox-grid-item` | Build Grid layouts, fractional tracks, and placement metadata. |
| `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. | | `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. |
| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context` | Atomic root commit, report lookup, and viewport-context rerender. | | `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context`, `ebox-viewport-window-width` | Atomic root commit, report lookup, viewport-context rerender, and the shared display-safe width sampler. |
| `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. | | `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. |
| `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint` | Persistent material child segments, backend-anchor lookup, subtree replacement, and retained Host paint transactions. | | `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint` | Persistent material child segments, backend-anchor lookup, subtree replacement, and retained Host paint transactions. |
| `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. | | `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. |

View File

@ -392,6 +392,11 @@ report若后续 phase 失败rollback 最多一次接收同一个 report
render 外部动态绑定 `ebox-viewport-width`(像素)与 `ebox-viewport-height` render 外部动态绑定 `ebox-viewport-width`(像素)与 `ebox-viewport-height`
(行数)。不要原地修改已经发布的 node tree。 (行数)。不要原地修改已经发布的 node tree。
可见的 mounted buffer 会自动跟随展示它的 window。Ebox 合并连续 size event
并在 `ebox-viewport-resize-delay`(默认 0.05 秒)后只发布最新 sample。
`ebox-viewport-window-width` 返回 controller 使用的同一个显示安全像素宽度;
宿主不应再安装第二套 resize hook 或宽度算法。
## 7. Selector ## 7. Selector
`ebox-selector-parse` 委托 ECSS 并返回结构化 AST `ebox-selector-parse` 委托 ECSS 并返回结构化 AST
@ -468,6 +473,7 @@ text scale 和固定 `display` space它也是布局使用的公共测量原
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8`ebox-scroll-lazy-idle-prefetch-lines` 128`ebox-scroll-lazy-idle-prefetch-slice-lines` 16`ebox-scroll-lazy-idle-prefetch-delay` 0.15 | | Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8`ebox-scroll-lazy-idle-prefetch-lines` 128`ebox-scroll-lazy-idle-prefetch-slice-lines` 16`ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
| Runtime prewarm | `ebox-runtime-idle-prewarm` t`ebox-runtime-idle-prewarm-delay` 0.1`ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0`ebox-runtime-idle-prewarm-slice-size` 32`ebox-native-buffer-scroll` t仅 initial/visible-window handoff严格 root-owner proof | | Runtime prewarm | `ebox-runtime-idle-prewarm` t`ebox-runtime-idle-prewarm-delay` 0.1`ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0`ebox-runtime-idle-prewarm-slice-size` 32`ebox-native-buffer-scroll` t仅 initial/visible-window handoff严格 root-owner proof |
| 预测 reflow | `ebox-runtime-idle-reflow-cache-prewarm` t`ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 | | 预测 reflow | `ebox-runtime-idle-reflow-cache-prewarm` t`ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
| Viewport delivery | `ebox-viewport-resize-delay` 0.05;连续 window event 对每个 mounted buffer 只保留一个最新 sample |
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto``ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2`ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB`ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB`ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB`ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24`ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 | | Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto``ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2`ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB`ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB`ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB`ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24`ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
| Visual verification | `ebox-visual-check-output-dir` 指向可选 screenshot/report 使用的临时目录。 | | Visual verification | `ebox-visual-check-output-dir` 指向可选 screenshot/report 使用的临时目录。 |
@ -531,7 +537,7 @@ style API。
| `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 | | `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 |
| `ebox-grid`、`ebox-grid-fr`、`ebox-grid-item` | 构建 Grid、分数轨道和 placement metadata。 | | `ebox-grid`、`ebox-grid-fr`、`ebox-grid-item` | 构建 Grid、分数轨道和 placement metadata。 |
| `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 | | `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 |
| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context` | 原子根提交、报告查询和 viewport-context rerender。 | | `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width` | 原子根提交、报告查询、viewport-context rerender 与统一的显示安全宽度采样。 |
| `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 | | `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 |
| `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint` | 持久 material child segment、backend anchor 查询、子树替换与 retained Host paint transaction。 | | `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint` | 持久 material child segment、backend anchor 查询、子树替换与 retained Host paint transaction。 |
| `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 | | `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 |

103
ebox-viewport.el Normal file
View File

@ -0,0 +1,103 @@
;;; ebox-viewport.el --- Coalesced Emacs viewport delivery -*- lexical-binding: t; -*-
;;; Commentary:
;; Emacs owns window events; Ebox owns the mounted viewport. This module is
;; the single bridge between them. Size hooks only sample the latest visible
;; dimensions. One deferred callback per buffer publishes the newest sample,
;; so continuous frame dragging cannot enqueue synchronous layout transactions.
;;; Code:
(require 'cl-lib)
(require 'ebox-surface)
(declare-function ebox--buffer-render-state "ebox-incremental" (buffer))
(declare-function ebox-rerender-buffer-with-context
"ebox" (buffer viewport-width &optional viewport-height))
(defcustom ebox-viewport-resize-delay 0.05
"Seconds after the latest size event before publishing its viewport.
Continuous size events replace one pending sample for each mounted buffer."
:type 'number
:group 'ebox)
(defvar ebox--viewport-resize-pending-table (make-hash-table :test #'eq)
"Buffer-keyed latest viewport samples awaiting publication.")
(defvar ebox--viewport-resize-generation 0
"Monotonic identity for pending viewport samples.")
(defun ebox-viewport-window-width (window)
"Return Ebox's display-safe viewport width for live WINDOW."
(and (window-live-p window)
(ebox-surface--window-content-width window)))
(defun ebox--cancel-buffer-viewport-resize (buffer)
"Cancel BUFFER's pending viewport publication."
(when-let* ((pending (gethash buffer ebox--viewport-resize-pending-table)))
(let ((timer (aref pending 4)))
(when (timerp timer) (cancel-timer timer)))
(remhash buffer ebox--viewport-resize-pending-table))
nil)
(defun ebox--viewport-resize-apply (buffer generation)
"Publish BUFFER's pending viewport identified by GENERATION."
(when-let* ((pending (gethash buffer ebox--viewport-resize-pending-table)))
(when (= generation (aref pending 0))
(remhash buffer ebox--viewport-resize-pending-table)
(let ((window (aref pending 1))
(width (aref pending 2))
(height (aref pending 3)))
(when (and (buffer-live-p buffer)
(window-live-p window)
(eq (window-buffer window) buffer)
(ebox-surface-buffer-mounted-p buffer))
(let ((state (ebox--buffer-render-state buffer)))
(unless (and (= width (or (plist-get state :viewport-width) -1))
(= height (or (plist-get state :viewport-height) -1)))
(ebox-rerender-buffer-with-context buffer width height))))))))
(defun ebox--schedule-buffer-viewport-resize
(buffer window width height)
"Replace BUFFER's pending viewport with WINDOW, WIDTH, and HEIGHT."
(ebox--cancel-buffer-viewport-resize buffer)
(let* ((generation (cl-incf ebox--viewport-resize-generation))
(pending (vector generation window width height nil)))
(puthash buffer pending ebox--viewport-resize-pending-table)
(aset pending 4
(run-at-time
(max 0 ebox-viewport-resize-delay) nil
#'ebox--viewport-resize-apply buffer generation)))
buffer)
(defun ebox--window-size-change (frame)
"Sample latest viewports for mounted Ebox buffers visible in FRAME."
(when (and (frame-live-p frame) (not noninteractive))
(let ((seen (make-hash-table :test #'eq)))
(dolist (window (window-list frame 'no-minibuf))
(when (window-live-p window)
(let ((buffer (window-buffer window)))
(unless (gethash buffer seen)
(puthash buffer t seen)
(when (and (buffer-live-p buffer)
(ebox-surface-buffer-mounted-p buffer))
(let* ((state (ebox--buffer-render-state buffer))
(width (ebox-viewport-window-width window))
(height (window-body-height window)))
(when (and width
state
(not (and
(= width
(or (plist-get state :viewport-width) -1))
(= height
(or (plist-get state :viewport-height)
-1)))))
(ebox--schedule-buffer-viewport-resize
buffer window width height)))))))))))
(when (boundp 'window-size-change-functions)
(add-hook 'window-size-change-functions #'ebox--window-size-change))
(provide 'ebox-viewport)
;;; ebox-viewport.el ends here

42
ebox.el
View File

@ -26,7 +26,8 @@
'("ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" '("ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
"ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el"
"ebox-native-commit.el" "ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
"ebox-dsl.el" "ebox-selector.el" "ebox.el"
"ebox-native-reflow.el") "ebox-native-reflow.el")
"Active Ebox Lisp sources compiled by `ebox-byte-compile'.") "Active Ebox Lisp sources compiled by `ebox-byte-compile'.")
@ -49,6 +50,7 @@
(require 'ebox-buffer-backend) (require 'ebox-buffer-backend)
(require 'ebox-incremental) (require 'ebox-incremental)
(require 'ebox-surface) (require 'ebox-surface)
(require 'ebox-viewport)
(require 'ebox-dsl) (require 'ebox-dsl)
(require 'ebox-selector) (require 'ebox-selector)
@ -2742,42 +2744,6 @@ that was rendered off-window cannot miss the native-scroll handoff."
(when (boundp 'window-state-change-functions) (when (boundp 'window-state-change-functions)
(add-hook 'window-state-change-functions #'ebox--window-state-change)) (add-hook 'window-state-change-functions #'ebox--window-state-change))
(defvar ebox--window-size-change-in-progress nil
"Non-nil while visible Ebox buffers are synchronizing their viewport.")
(defun ebox--window-size-change (frame)
"Synchronize visible Ebox buffers after FRAME changes size.
Ebox stores the containing-block viewport in its mounted surface. A frame
resize must publish the new window content width before the next render;
otherwise responsive Flex/Grid trees keep using the width from mount time.
One mounted surface represents one viewport, so when a buffer is visible in
multiple windows the first window in FRAME supplies the shared dimensions."
(when (and (frame-live-p frame)
(not noninteractive)
(not ebox--window-size-change-in-progress))
(let ((ebox--window-size-change-in-progress t)
(seen (make-hash-table :test #'eq)))
(dolist (window (window-list frame 'no-minibuf))
(when (and (window-live-p window)
(not (gethash (window-buffer window) seen)))
(let ((buffer (window-buffer window)))
(puthash buffer t seen)
(when (and (buffer-live-p buffer)
(ebox--buffer-render-state buffer)
(ebox-surface-buffer-mounted-p buffer))
(let* ((state (ebox--buffer-render-state buffer))
(width (ebox-surface--window-content-width window))
(height (window-body-height window)))
(unless (and (= width (or (plist-get state :viewport-width)
-1))
(= height (or (plist-get state :viewport-height)
-1)))
(ebox-rerender-buffer-with-context buffer width height))))))))))
(when (boundp 'window-size-change-functions)
(add-hook 'window-size-change-functions #'ebox--window-size-change))
(defun ebox--runtime-prewarm-record-viewport-node (defun ebox--runtime-prewarm-record-viewport-node
(job node width-allowed) (job node width-allowed)
"Record NODE's direct viewport dependencies in JOB. "Record NODE's direct viewport dependencies in JOB.
@ -3882,6 +3848,7 @@ With prefix ARG, scroll by that many content lines."
"Remove all runtime caches owned by BUFFER. "Remove all runtime caches owned by BUFFER.
Defaults to the current buffer." Defaults to the current buffer."
(let ((buffer (or buffer (current-buffer)))) (let ((buffer (or buffer (current-buffer))))
(ebox--cancel-buffer-viewport-resize buffer)
(ebox--cancel-buffer-runtime-prewarm buffer) (ebox--cancel-buffer-runtime-prewarm buffer)
(when-let ((root (ebox--buffer-root-node buffer))) (when-let ((root (ebox--buffer-root-node buffer)))
(ebox--clear-region-runtime-caches (ebox--clear-region-runtime-caches
@ -4716,6 +4683,7 @@ through dirty-set and patch-set execution before falling back to root rerender."
ebox-stack ebox-stack
ebox-string-pixel-width ebox-string-pixel-width
ebox-update-selector ebox-update-selector
ebox-viewport-window-width
ebox-wheel-scroll-down ebox-wheel-scroll-down
ebox-wheel-scroll-up) ebox-wheel-scroll-up)
"Stable core Ebox entry points available to applications and tooling. "Stable core Ebox entry points available to applications and tooling.

View File

@ -4,6 +4,11 @@
(require 'ebox-child-range) (require 'ebox-child-range)
(require 'ebox) (require 'ebox)
;; This suite locks child-range and Elisp projection semantics. Native frame
;; execution is verified separately and must not replace the named fallback
;; strategies merely because a developer has built the optional module.
(setq ebox-native-reflow-module-path nil)
(defconst ebox-child-range-test--root (defconst ebox-child-range-test--root
(expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name))) (expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name)))
"Ebox repository root for source audits.") "Ebox repository root for source audits.")

View File

@ -5,6 +5,12 @@
(require 'ebox) (require 'ebox)
(require 'ebox-native-commit) (require 'ebox-native-commit)
;; These tests lock the named Elisp projection proofs. Native commit has its
;; own focused contract tests below; disable runtime module discovery here so
;; a locally built optional module cannot silently replace paint/span/mixed
;; plans with `native-frame' and make this suite environment-dependent.
(setq ebox-native-reflow-module-path nil)
(defun ebox-commit-test--buffer-string (buffer) (defun ebox-commit-test--buffer-string (buffer)
"Return BUFFER's complete propertized contents." "Return BUFFER's complete propertized contents."
(with-current-buffer buffer (with-current-buffer buffer

View File

@ -10,6 +10,11 @@
(load-file (expand-file-name "../ebox.el" (file-name-directory load-file-name))) (load-file (expand-file-name "../ebox.el" (file-name-directory load-file-name)))
;; Core tests assert named Elisp render/patch strategies. Keep optional local
;; native artifacts from replacing those strategies; native execution has
;; separate module and integration gates.
(setq ebox-native-reflow-module-path nil)
(defvar ebox--scroll-window-initial-lookahead-lines-override nil) (defvar ebox--scroll-window-initial-lookahead-lines-override nil)
(defun ebox-test--reset-runtime-state () (defun ebox-test--reset-runtime-state ()

View File

@ -51,7 +51,8 @@
(dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure (dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure
ebox-fragment ebox-layout ebox-flex ebox-grid ebox-fragment ebox-layout ebox-flex ebox-grid
ebox-buffer-backend ebox-incremental ebox-buffer-backend ebox-incremental
ebox-surface ebox-dsl ebox-selector)) ebox-surface ebox-viewport ebox-dsl
ebox-selector))
(should (featurep feature)))) (should (featurep feature))))
(ert-deftest ebox-facade-exposes-native-workflow-without-loading-it () (ert-deftest ebox-facade-exposes-native-workflow-without-loading-it ()
@ -70,8 +71,8 @@
"ebox-tree.el" "ebox-measure.el" "ebox-tree.el" "ebox-measure.el"
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
"ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el"
"ebox-native-commit.el" "ebox-surface.el" "ebox-dsl.el" "ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
"ebox-selector.el" "ebox.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el"
"ebox-native-reflow.el")))) "ebox-native-reflow.el"))))
(ert-deftest ebox-surface-depends-only-on-public-tp-api () (ert-deftest ebox-surface-depends-only-on-public-tp-api ()

View File

@ -171,11 +171,15 @@
(when (get-buffer " *other-window*") (when (get-buffer " *other-window*")
(kill-buffer " *other-window*"))))) (kill-buffer " *other-window*")))))
(ert-deftest ebox-window-size-change-publishes-visible-viewport () (ert-deftest ebox-window-size-change-coalesces-to-latest-visible-viewport ()
"A live frame resize updates the mounted surface's containing block." "Continuous frame changes publish only the latest sampled viewport."
(let* ((buffer (generate-new-buffer " *ebox-window-size-change*")) (let* ((buffer (generate-new-buffer " *ebox-window-size-change*"))
(window (selected-window)) (window (selected-window))
(old-buffer (window-buffer window)) (old-buffer (window-buffer window))
(sampled-height (window-body-height window))
(sampled-widths (number-sequence 240 430 10))
scheduled
cancelled
calls) calls)
(unwind-protect (unwind-protect
(progn (progn
@ -186,18 +190,58 @@
((symbol-function 'ebox-surface-buffer-mounted-p) ((symbol-function 'ebox-surface-buffer-mounted-p)
(lambda (_buffer) t)) (lambda (_buffer) t))
((symbol-function 'ebox-surface--window-content-width) ((symbol-function 'ebox-surface--window-content-width)
(lambda (_window) 240)) (lambda (_window) (pop sampled-widths)))
((symbol-function 'window-body-height)
(lambda (_window &optional _pixelwise) 20))
((symbol-function 'ebox-rerender-buffer-with-context) ((symbol-function 'ebox-rerender-buffer-with-context)
(lambda (target width height) (lambda (target width height)
(setq calls (list target width height)))) (push (list target width height) calls)))
((symbol-function 'run-at-time)
(lambda (delay _repeat function &rest arguments)
(let ((timer (intern (format "ebox-resize-timer-%d"
(1+ (length scheduled))))))
(push (list timer delay function arguments) scheduled)
timer)))
((symbol-function 'timerp)
(lambda (value)
(string-prefix-p "ebox-resize-timer-"
(symbol-name value))))
((symbol-function 'cancel-timer)
(lambda (timer) (push timer cancelled)))
(noninteractive nil)) (noninteractive nil))
(ebox--window-size-change (selected-frame)) (dotimes (_index 20)
(should (equal calls (list buffer 240 20))))) (ebox--window-size-change (selected-frame)))
(should-not calls)
(should (= (length scheduled) 20))
(should (= (length cancelled) 19))
(pcase-let ((`(,_timer ,delay ,function ,arguments)
(car scheduled)))
(should (= delay ebox-viewport-resize-delay))
(apply function arguments))
(should (equal calls
(list (list buffer 430 sampled-height))))))
(set-window-buffer window old-buffer) (set-window-buffer window old-buffer)
(when (boundp 'ebox--viewport-resize-pending-table)
(clrhash ebox--viewport-resize-pending-table))
(kill-buffer buffer)))) (kill-buffer buffer))))
(ert-deftest ebox-buffer-cleanup-cancels-pending-viewport-resize ()
"A dead mounted buffer cannot retain a viewport timer or pending sample."
(let ((buffer (generate-new-buffer " *ebox-viewport-cleanup*"))
(timer 'ebox-test-viewport-cleanup-timer)
cancelled)
(unwind-protect
(progn
(puthash buffer (vector 1 (selected-window) 400 20 timer)
ebox--viewport-resize-pending-table)
(cl-letf (((symbol-function 'timerp)
(lambda (value) (eq value timer)))
((symbol-function 'cancel-timer)
(lambda (value) (setq cancelled value))))
(with-current-buffer buffer (ebox--cleanup-current-buffer)))
(should (eq cancelled timer))
(should-not (gethash buffer ebox--viewport-resize-pending-table)))
(remhash buffer ebox--viewport-resize-pending-table)
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(defun ebox-surface-test--hash-fingerprint (table) (defun ebox-surface-test--hash-fingerprint (table)
"Return a stable content fingerprint for hash TABLE. "Return a stable content fingerprint for hash TABLE.
The fingerprint checks entries rather than only table identity, so a failed The fingerprint checks entries rather than only table identity, so a failed
@ -732,10 +776,13 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ert-deftest ebox-surface-plan-stays-free-of-runtime-state () (ert-deftest ebox-surface-plan-stays-free-of-runtime-state ()
"The surface plan should not contain markers or runtime handles." "The surface plan should not contain markers or runtime handles."
(require 'ebox-native-commit)
(let ((original (symbol-function 'tp-surface-result-create)) (let ((original (symbol-function 'tp-surface-result-create))
(original-owned (symbol-function 'tp-surface-result-create-owned)) (original-owned (symbol-function 'tp-surface-result-create-owned))
captured client-state) captured client-state)
(cl-letf (((symbol-function 'tp-surface-result-create) (cl-letf (((symbol-function 'ebox-native-commit-render)
(lambda (&rest _) nil))
((symbol-function 'tp-surface-result-create)
(lambda (plan &optional state) (lambda (plan &optional state)
(setq captured plan (setq captured plan
client-state state) client-state state)
@ -753,7 +800,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(let* ((fragments-plan (car (tp-surface-plan-children captured))) (let* ((fragments-plan (car (tp-surface-plan-children captured)))
(text-plan (car (tp-surface-plan-children fragments-plan))) (text-plan (car (tp-surface-plan-children fragments-plan)))
(plan-text (tp-surface-plan-text text-plan)) (plan-text (tp-surface-plan-text text-plan))
(state-fragments (plist-get client-state :surface-fragments)) (state-fragments
(ebox-surface--materialized-fragment-ledger client-state))
(owner-position (owner-position
(cl-loop for position from 0 below (length plan-text) (cl-loop for position from 0 below (length plan-text)
when (get-text-property position when (get-text-property position
@ -1323,7 +1371,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(plist-put node :key 'growing-source) (plist-put node :key 'growing-source)
node)) node))
(let ((original-render (symbol-function 'ebox-render))) (let ((original-render (symbol-function 'ebox-render)))
(cl-letf (((symbol-function 'ebox-render) (cl-letf (((symbol-function 'ebox-native-commit-render)
(lambda (&rest _) nil))
((symbol-function 'ebox-render)
(lambda (node) (lambda (node)
(cond (cond
((equal (plist-get node :key) 'stable-source) ((equal (plist-get node :key) 'stable-source)