fix: wait a fresh idle interval before predictive reflow

This commit is contained in:
Kinneyzhang 2026-09-05 16:09:24 +08:00
parent 39d079b2cc
commit 73357dee94
2 changed files with 80 additions and 8 deletions

View File

@ -2463,7 +2463,8 @@ publishes the prepared rendered-body entry plus its derived root templates."
(puthash
buffer
(run-with-idle-timer
(max 0 ebox-runtime-idle-reflow-cache-prewarm-delay)
(ebox--idle-continuation-delay
ebox-runtime-idle-reflow-cache-prewarm-delay)
nil #'ebox--run-root-width-cache-prewarm
buffer state revision region-id source-width target-width)
ebox--reflow-cache-prewarm-timers)))))
@ -2505,7 +2506,8 @@ predictions are skipped."
(puthash
buffer
(run-with-idle-timer
(max 0 ebox-runtime-idle-reflow-cache-prewarm-delay)
(ebox--idle-continuation-delay
ebox-runtime-idle-reflow-cache-prewarm-delay)
nil #'ebox--run-reflow-cache-prewarm
buffer state revision viewport-width target-width viewport-height)
ebox--reflow-cache-prewarm-timers))))))

View File

@ -1652,7 +1652,7 @@
(kill-buffer buffer)))))
(ert-deftest ebox-root-width-cache-prewarm-yields-to-user-input ()
"Ordinary root prediction should run only after an idle interval."
"Ordinary root prediction should wait a fresh interval even while idle."
(ebox-test--reset-runtime-state)
(let* ((layout
(ebox-test-box :id "root" :width '(240)
@ -1674,7 +1674,9 @@
(generate-new-buffer-name " *ebox-root-schedule*")
layout))
(let ((root-id (ebox-test--selector-region-id buffer "#root")))
(cl-letf (((symbol-function 'run-at-time)
(cl-letf (((symbol-function 'current-idle-time)
(lambda () (seconds-to-time 0.4)))
((symbol-function 'run-at-time)
(lambda (&rest _)
(ert-fail "Resize prediction must yield to input")))
((symbol-function 'run-with-idle-timer)
@ -1687,7 +1689,7 @@
(ebox--schedule-buffer-root-width-cache-prewarm
buffer root-id 240 260)))
(should (= scheduled-delay
ebox-runtime-idle-reflow-cache-prewarm-delay))
(+ 0.4 ebox-runtime-idle-reflow-cache-prewarm-delay)))
(should-not scheduled-repeat)
(should (eq scheduled-function
#'ebox--run-root-width-cache-prewarm))
@ -1697,6 +1699,72 @@
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-reflow-prewarm-replacement-and-one-shot-completion ()
"Fresh and zero-delay predictions replace old jobs and complete once."
(ebox-test--reset-runtime-state)
(let ((ebox-viewport-width 900)
(ebox-viewport-height 4)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm t)
(ebox--runtime-prewarm-allow-noninteractive t)
buffer)
(unwind-protect
(progn
(setq buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-prewarm-one-shot*")
(ebox-test-box :id "root" :width '(240) :height 1
(ebox-test-text "prediction"))))
(let ((state (ebox--buffer-render-state buffer))
(root-id (ebox-test--selector-region-id buffer "#root")))
(dolist (kind '(viewport root-width))
(dolist (case '((nil 0.15 0.15) (0.4 0 0.4)))
(pcase-let* ((`(,idle-age ,delay ,threshold) case)
(ebox-runtime-idle-reflow-cache-prewarm-delay delay)
(scratch (plist-get state :reflow-prewarm-scratch))
(cancel (symbol-function 'cancel-timer))
(registrations 0)
(scheduled nil)
(cancelled nil))
(cl-letf
(((symbol-function 'current-idle-time)
(lambda () (and idle-age (seconds-to-time idle-age))))
((symbol-function 'cancel-timer)
(lambda (timer)
(push timer cancelled)
(funcall cancel timer)))
((symbol-function 'run-with-idle-timer)
(lambda (seconds repeat function &rest arguments)
(cl-incf registrations)
(setq scheduled (list seconds repeat function arguments))
(timer-create))))
(dotimes (attempt 2)
(let ((old (gethash buffer ebox--reflow-cache-prewarm-timers)))
(pcase kind
('viewport
(ebox--schedule-buffer-reflow-cache-prewarm
buffer 880 900))
('root-width
(ebox--schedule-buffer-root-width-cache-prewarm
buffer root-id 240 260)))
(when (= attempt 1)
(should (memq old cancelled))
(should-not
(eq old (gethash buffer ebox--reflow-cache-prewarm-timers))))))
(should (= registrations 2))
(should (= (car scheduled) threshold))
(should-not (cadr scheduled))
(should (eq scratch (plist-get state :reflow-prewarm-scratch)))
;; Advance the same idle period beyond the fixed threshold.
;; Run the real callback, including its state/revision guards.
(setq idle-age (+ threshold 1))
(should (apply (nth 2 scheduled) (nth 3 scheduled)))
(should (plist-get state :reflow-prewarm-scratch))
(should-not (gethash buffer ebox--reflow-cache-prewarm-timers))
(should (= registrations 2))))))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-scheduled-reflow-prewarms-use-offscreen-gc-pressure ()
"Both predicted reflow paths should move GC pressure off visible frames."
(let ((prewarm-count 0)
@ -10656,7 +10724,7 @@
:rendered-region-line-span-index-deferred))))))
(ert-deftest ebox-reflow-cache-prewarm-uses-first-valid-width-delta ()
"Predictive warming should start from the first valid resize velocity."
"First resize prediction should wait a fresh interval even while idle."
(ebox-test--reset-runtime-state)
(let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height 2
@ -10676,7 +10744,9 @@
(setq buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-test*") layout))
(cl-letf (((symbol-function 'run-at-time)
(cl-letf (((symbol-function 'current-idle-time)
(lambda () (seconds-to-time 0.4)))
((symbol-function 'run-at-time)
(lambda (&rest _)
(ert-fail "Resize prediction must yield to input")))
((symbol-function 'run-with-idle-timer)
@ -10688,7 +10758,7 @@
'scheduled-reflow-timer)))
(ebox-rerender-buffer-with-context buffer 260 4))
(should (= scheduled-delay
ebox-runtime-idle-reflow-cache-prewarm-delay))
(+ 0.4 ebox-runtime-idle-reflow-cache-prewarm-delay)))
(should-not scheduled-repeat)
(should (eq scheduled-function #'ebox--run-reflow-cache-prewarm))
(should (equal (car scheduled-arguments) buffer))