perf: eliminate cold-scroll flicker and GC stalls
This commit is contained in:
parent
0c7e7b974a
commit
9f48b2f283
175
ebox-surface.el
175
ebox-surface.el
@ -223,6 +223,11 @@ owned; no per-fragment substring is allocated."
|
|||||||
(setq line-index (1+ line-index)))
|
(setq line-index (1+ line-index)))
|
||||||
(nreverse result))))
|
(nreverse result))))
|
||||||
|
|
||||||
|
(defun ebox-surface--scroll-lines-owned-p (lines)
|
||||||
|
"Return non-nil when every rendered line has an owned fragment template."
|
||||||
|
(and lines
|
||||||
|
(cl-every #'ebox-surface--scroll-line-fragment-template lines)))
|
||||||
|
|
||||||
(defun ebox-surface--style-state-table (previous-state)
|
(defun ebox-surface--style-state-table (previous-state)
|
||||||
"Copy retained style state from PREVIOUS-STATE into a weak table."
|
"Copy retained style state from PREVIOUS-STATE into a weak table."
|
||||||
(let ((table (make-hash-table :test 'eq :weakness 'key)))
|
(let ((table (make-hash-table :test 'eq :weakness 'key)))
|
||||||
@ -3310,12 +3315,14 @@ box with wrapper chrome falls back to the retained wrapper renderer."
|
|||||||
scroll-state))))
|
scroll-state))))
|
||||||
(if (and visible (cadr visible))
|
(if (and visible (cadr visible))
|
||||||
(let* ((lines (cadr visible))
|
(let* ((lines (cadr visible))
|
||||||
(output (ebox-lines-join lines))
|
(output (ebox-lines-join lines)))
|
||||||
(fragment-data
|
(when (and (ebox-surface--scroll-lines-owned-p lines)
|
||||||
(ebox-surface--scroll-fragment-data lines output)))
|
|
||||||
(when (and fragment-data
|
|
||||||
(plist-get state :retained-scroll-content-p))
|
(plist-get state :retained-scroll-content-p))
|
||||||
(plist-put state :scroll-patch-fragment-data fragment-data))
|
;; Retain immutable line handles and stream their cached role
|
||||||
|
;; runs directly into ownership planning. Materializing one
|
||||||
|
;; fragment plist per visible property run on every wheel
|
||||||
|
;; event made scroll allocation O(all visible fragments).
|
||||||
|
(plist-put state :scroll-patch-lines lines))
|
||||||
(when (> (length output) 0)
|
(when (> (length output) 0)
|
||||||
(add-text-properties
|
(add-text-properties
|
||||||
0 (length output)
|
0 (length output)
|
||||||
@ -3745,50 +3752,83 @@ fragment's complete paint-owner ancestor chain."
|
|||||||
(push copy metadata)
|
(push copy metadata)
|
||||||
(setq offset (+ offset length))))))
|
(setq offset (+ offset length))))))
|
||||||
|
|
||||||
(defun ebox-surface--owned-ranges
|
(defun ebox-surface--owned-ranges-from-runs
|
||||||
(context leaf fragments state node-objects region-objects
|
(context leaf state node-objects region-objects attach-p visit-runs)
|
||||||
&optional attach-p)
|
"Attach merged ownership produced by VISIT-RUNS to candidate LEAF.
|
||||||
"Attach merged owner ranges for FRAGMENTS to candidate LEAF.
|
VISIT-RUNS receives one callback accepting ROLE-IDS, START, and END.
|
||||||
Adjacent ranges merge only when both their owner and opaque tags match."
|
Adjacent ranges merge only when both their owner and opaque tags match."
|
||||||
(let ((active (make-hash-table :test #'eq))
|
(let ((active (make-hash-table :test #'eq))
|
||||||
(owner-cache (make-hash-table :test #'equal))
|
(owner-cache (make-hash-table :test #'equal))
|
||||||
(offset 0)
|
|
||||||
ranges)
|
ranges)
|
||||||
(dolist (fragment fragments)
|
(funcall
|
||||||
(let* ((role-ids (plist-get fragment :role-ids))
|
visit-runs
|
||||||
(owners (gethash role-ids owner-cache 'ebox/no-owners))
|
(lambda (role-ids start end)
|
||||||
(source-p (plist-get fragment :text-source-p))
|
(let ((owners (gethash role-ids owner-cache 'ebox/no-owners)))
|
||||||
(start (if source-p
|
(when (eq owners 'ebox/no-owners)
|
||||||
(plist-get fragment :start)
|
(setq owners
|
||||||
offset))
|
(ebox-surface--fragment-owners
|
||||||
(end (if source-p
|
role-ids state node-objects region-objects))
|
||||||
(plist-get fragment :end)
|
(puthash role-ids owners owner-cache))
|
||||||
(+ offset (length (plist-get fragment :text))))))
|
(dolist (owner owners)
|
||||||
(when (eq owners 'ebox/no-owners)
|
(let* ((object (nth 0 owner))
|
||||||
(setq owners
|
(tags (nth 1 owner))
|
||||||
(ebox-surface--fragment-owners
|
(tag-ranges
|
||||||
role-ids state node-objects region-objects))
|
(or (gethash object active)
|
||||||
(puthash role-ids owners owner-cache))
|
(let ((table (make-hash-table :test #'equal)))
|
||||||
(dolist (owner owners)
|
(puthash object table active)
|
||||||
(let* ((object (nth 0 owner))
|
table)))
|
||||||
(tags (nth 1 owner))
|
(current (gethash tags tag-ranges)))
|
||||||
(tag-ranges (or (gethash object active)
|
(if (and current (= (plist-get current :end) start))
|
||||||
(let ((table (make-hash-table :test #'equal)))
|
(plist-put current :end end)
|
||||||
(puthash object table active)
|
(let ((range (list :object object :start start :end end
|
||||||
table)))
|
:tags tags)))
|
||||||
(current (gethash tags tag-ranges)))
|
(push range ranges)
|
||||||
(if (and current (= (plist-get current :end) start))
|
(puthash tags range tag-ranges))))))))
|
||||||
(plist-put current :end end)
|
|
||||||
(let ((range (list :object object :start start :end end
|
|
||||||
:tags tags)))
|
|
||||||
(push range ranges)
|
|
||||||
(puthash tags range tag-ranges)))))
|
|
||||||
(setq offset (if source-p end (+ offset (length (plist-get fragment :text)))))))
|
|
||||||
(setq ranges (nreverse ranges))
|
(setq ranges (nreverse ranges))
|
||||||
(when attach-p
|
(when attach-p
|
||||||
(tp-object-attach-content-ranges-owned context leaf ranges))
|
(tp-object-attach-content-ranges-owned context leaf ranges))
|
||||||
ranges))
|
ranges))
|
||||||
|
|
||||||
|
(defun ebox-surface--owned-ranges
|
||||||
|
(context leaf fragments state node-objects region-objects
|
||||||
|
&optional attach-p)
|
||||||
|
"Attach merged owner ranges for FRAGMENTS to candidate LEAF."
|
||||||
|
(ebox-surface--owned-ranges-from-runs
|
||||||
|
context leaf state node-objects region-objects attach-p
|
||||||
|
(lambda (visit)
|
||||||
|
(let ((offset 0))
|
||||||
|
(dolist (fragment fragments)
|
||||||
|
(let* ((source-p (plist-get fragment :text-source-p))
|
||||||
|
(start (if source-p
|
||||||
|
(plist-get fragment :start)
|
||||||
|
offset))
|
||||||
|
(end (if source-p
|
||||||
|
(plist-get fragment :end)
|
||||||
|
(+ offset (length (plist-get fragment :text))))))
|
||||||
|
(funcall visit (plist-get fragment :role-ids) start end)
|
||||||
|
(setq offset
|
||||||
|
(if source-p
|
||||||
|
end
|
||||||
|
(+ offset (length (plist-get fragment :text)))))))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--scroll-owned-ranges
|
||||||
|
(context leaf lines state node-objects region-objects attach-p)
|
||||||
|
"Attach merged owner ranges for retained scroll LINES to candidate LEAF."
|
||||||
|
(ebox-surface--owned-ranges-from-runs
|
||||||
|
context leaf state node-objects region-objects attach-p
|
||||||
|
(lambda (visit)
|
||||||
|
(let ((offset 0))
|
||||||
|
(dolist (line lines)
|
||||||
|
(let ((template
|
||||||
|
(or (ebox-surface--scroll-line-fragment-template line)
|
||||||
|
(error "Ebox retained scroll line lost fragment ownership"))))
|
||||||
|
(dolist (fragment template)
|
||||||
|
(funcall visit
|
||||||
|
(plist-get fragment :role-ids)
|
||||||
|
(+ offset (plist-get fragment :start))
|
||||||
|
(+ offset (plist-get fragment :end))))
|
||||||
|
(setq offset (+ offset (length line) 1))))))))
|
||||||
|
|
||||||
(defun ebox-surface--native-owned-ranges
|
(defun ebox-surface--native-owned-ranges
|
||||||
(context leaf template state node-objects region-objects attach-p)
|
(context leaf template state node-objects region-objects attach-p)
|
||||||
"Attach owner ranges from compact native fragment TEMPLATE."
|
"Attach owner ranges from compact native fragment TEMPLATE."
|
||||||
@ -3832,11 +3872,17 @@ Adjacent ranges merge only when both their owner and opaque tags match."
|
|||||||
|
|
||||||
(defun ebox-surface--materialized-fragment-ledger (state)
|
(defun ebox-surface--materialized-fragment-ledger (state)
|
||||||
"Return STATE's paint ledger, expanding a native frame only on demand."
|
"Return STATE's paint ledger, expanding a native frame only on demand."
|
||||||
(let ((ledger (plist-get state :surface-fragments)))
|
(let* ((ledger (plist-get state :surface-fragments))
|
||||||
(if-let* ((frame (and (listp ledger)
|
(frame (and (listp ledger) (plist-get ledger :native-frame)))
|
||||||
(plist-get ledger :native-frame))))
|
(lines (and (listp ledger) (plist-get ledger :scroll-lines))))
|
||||||
(ebox-native-reflow-frame-fragments frame)
|
(cond
|
||||||
ledger)))
|
(frame (ebox-native-reflow-frame-fragments frame))
|
||||||
|
(lines
|
||||||
|
(ebox-surface--fragment-metadata
|
||||||
|
(or (ebox-surface--scroll-fragment-data lines "")
|
||||||
|
(error "Ebox retained scroll lines lost fragment ownership"))
|
||||||
|
state))
|
||||||
|
(t ledger))))
|
||||||
|
|
||||||
(defun ebox-surface--formatting-context-reflow-owned-ranges
|
(defun ebox-surface--formatting-context-reflow-owned-ranges
|
||||||
(context leaf ranges state node-objects)
|
(context leaf ranges state node-objects)
|
||||||
@ -3875,24 +3921,26 @@ all descendant ranges remain unchanged."
|
|||||||
&optional transfer-text-p)
|
&optional transfer-text-p)
|
||||||
"Return one shared-text TP plan for OUTPUT and STATE.
|
"Return one shared-text TP plan for OUTPUT and STATE.
|
||||||
NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
||||||
(let* ((fragment-root
|
(let* ((scroll-lines
|
||||||
|
(and transfer-text-p
|
||||||
|
(plist-get state :retained-scroll-content-p)
|
||||||
|
(plist-get state :scroll-patch-lines)))
|
||||||
|
(fragment-root
|
||||||
(tp-object-ensure context surface-root
|
(tp-object-ensure context surface-root
|
||||||
ebox-surface--fragments-key 'ebox/fragments))
|
ebox-surface--fragments-key 'ebox/fragments))
|
||||||
(fragment-data
|
(fragment-data
|
||||||
(or (and transfer-text-p
|
(unless scroll-lines
|
||||||
(plist-get state :retained-scroll-content-p)
|
(or (and (eq output (plist-get state :native-render-output))
|
||||||
(plist-get state :scroll-patch-fragment-data))
|
(plist-get state :native-render-fragment-template))
|
||||||
(and (eq output (plist-get state :native-render-output))
|
(and (eq (plist-get state :projection-kind)
|
||||||
(plist-get state :native-render-fragment-template))
|
'mixed-owner-reflow)
|
||||||
(and (eq (plist-get state :projection-kind)
|
(plist-get state :mixed-owner-fragment-data))
|
||||||
'mixed-owner-reflow)
|
(and (memq (plist-get state :projection-kind)
|
||||||
(plist-get state :mixed-owner-fragment-data))
|
'(span-patch owner-scoped))
|
||||||
(and (memq (plist-get state :projection-kind)
|
(plist-get state :span-patch-fragment-data))
|
||||||
'(span-patch owner-scoped))
|
(if (stringp output)
|
||||||
(plist-get state :span-patch-fragment-data))
|
(ebox-surface--rendered-fragments output)
|
||||||
(if (stringp output)
|
output))))
|
||||||
(ebox-surface--rendered-fragments output)
|
|
||||||
output)))
|
|
||||||
(rendered
|
(rendered
|
||||||
(if (stringp output)
|
(if (stringp output)
|
||||||
output
|
output
|
||||||
@ -3922,6 +3970,10 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
|||||||
(eq output (plist-get state :native-render-output))))
|
(eq output (plist-get state :native-render-output))))
|
||||||
(owned-ranges
|
(owned-ranges
|
||||||
(cond
|
(cond
|
||||||
|
(scroll-lines
|
||||||
|
(ebox-surface--scroll-owned-ranges
|
||||||
|
context text-leaf scroll-lines state node-objects
|
||||||
|
region-objects attach-p))
|
||||||
((and native-template-p
|
((and native-template-p
|
||||||
(plist-get state :native-reuse-ownership-p))
|
(plist-get state :native-reuse-ownership-p))
|
||||||
(let ((ranges
|
(let ((ranges
|
||||||
@ -4009,6 +4061,8 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
|||||||
(cond
|
(cond
|
||||||
((plist-get state :native-render-frame)
|
((plist-get state :native-render-frame)
|
||||||
(list :native-frame (plist-get state :native-render-frame)))
|
(list :native-frame (plist-get state :native-render-frame)))
|
||||||
|
(scroll-lines
|
||||||
|
(list :scroll-lines (copy-sequence scroll-lines)))
|
||||||
((or (plist-get state :mixed-owner-content-p)
|
((or (plist-get state :mixed-owner-content-p)
|
||||||
(plist-get state :span-patch-content-p))
|
(plist-get state :span-patch-content-p))
|
||||||
;; Mixed projection already produced fresh offset-only
|
;; Mixed projection already produced fresh offset-only
|
||||||
@ -4020,6 +4074,7 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
|||||||
(cl-remf state :mixed-owner-content-p)
|
(cl-remf state :mixed-owner-content-p)
|
||||||
(cl-remf state :span-patch-fragment-data)
|
(cl-remf state :span-patch-fragment-data)
|
||||||
(cl-remf state :span-patch-content-p)
|
(cl-remf state :span-patch-content-p)
|
||||||
|
(cl-remf state :scroll-patch-lines)
|
||||||
(let ((rendered-length (length rendered)))
|
(let ((rendered-length (length rendered)))
|
||||||
(list plan rendered owned-ranges
|
(list plan rendered owned-ranges
|
||||||
(list (list :object text-leaf :start 0 :end rendered-length
|
(list (list :object text-leaf :start 0 :end rendered-length
|
||||||
|
|||||||
22
ebox.el
22
ebox.el
@ -954,10 +954,11 @@ several costly rows before yielding back to input."
|
|||||||
|
|
||||||
This is the synchronous edge of the lazy scroll model: visible scrolling can
|
This is the synchronous edge of the lazy scroll model: visible scrolling can
|
||||||
only replace cached line strings after those strings exist. When the user
|
only replace cached line strings after those strings exist. When the user
|
||||||
reaches the current lazy prefix boundary, render at most one configured
|
reaches the current lazy prefix boundary, render one configured interactive
|
||||||
interactive slice instead of pausing for an idle timer or materializing the
|
slice plus the fixed lazy lookahead instead of pausing for an idle timer or
|
||||||
entire source. BUDGET-LINES caps how many new source lines this call may
|
materializing the entire source. The lookahead amortizes composite source
|
||||||
request; callers that represent GUI animation ticks pass their tick size."
|
rows across following wheel events. BUDGET-LINES caps the visible slice;
|
||||||
|
callers that represent GUI animation ticks pass their tick size."
|
||||||
(let* ((content-lines (plist-get state :content-lines))
|
(let* ((content-lines (plist-get state :content-lines))
|
||||||
(content-height (or (plist-get state :content-height) 0))
|
(content-height (or (plist-get state :content-height) 0))
|
||||||
(current-lines (length content-lines))
|
(current-lines (length content-lines))
|
||||||
@ -970,8 +971,7 @@ request; callers that represent GUI animation ticks pass their tick size."
|
|||||||
(slice-lines (max 1 (or budget-lines ebox-scroll-step 1)))
|
(slice-lines (max 1 (or budget-lines ebox-scroll-step 1)))
|
||||||
(slice-end (+ current-lines slice-lines))
|
(slice-end (+ current-lines slice-lines))
|
||||||
(required-lines (max current-lines
|
(required-lines (max current-lines
|
||||||
(min visible-end slice-end)))
|
(min visible-end slice-end))))
|
||||||
(ebox-scroll-lazy-prefix-lookahead-lines 0))
|
|
||||||
(ebox--scroll-state-ensure-prefix-lines
|
(ebox--scroll-state-ensure-prefix-lines
|
||||||
region-id state required-lines t)))))
|
region-id state required-lines t)))))
|
||||||
|
|
||||||
@ -3138,8 +3138,14 @@ inside the old prefix, so a line-slide never mixes two layout versions."
|
|||||||
(throw 'result (list :result 'pending)))
|
(throw 'result (list :result 'pending)))
|
||||||
(when (and (> delta 0)
|
(when (and (> delta 0)
|
||||||
(or prefix-budget-lines
|
(or prefix-budget-lines
|
||||||
(and (ebox--scroll-sync-prefix-render-p)
|
;; A small foreground intent already has an explicit
|
||||||
(<= delta (ebox--scroll-prefetch-slice-lines)))))
|
;; frame budget. Extend exactly that bounded prefix
|
||||||
|
;; before publication so Ebox remains the sole owner of
|
||||||
|
;; the visible scroll coordinate. Deferring this case
|
||||||
|
;; leaves the intent residual to Emacs buffer scrolling;
|
||||||
|
;; the following Ebox commit then snaps WINDOW-START
|
||||||
|
;; back, producing a cold-cache flicker.
|
||||||
|
(<= delta (ebox--scroll-prefetch-slice-lines))))
|
||||||
(setq state
|
(setq state
|
||||||
(ebox--scroll-ensure-bounded-prefix-for-offset
|
(ebox--scroll-ensure-bounded-prefix-for-offset
|
||||||
region-id state desired prefix-budget-lines)))
|
region-id state desired prefix-budget-lines)))
|
||||||
|
|||||||
@ -8309,8 +8309,8 @@
|
|||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-lazy-scroll-gui-cache-miss-returns-pending ()
|
(ert-deftest ebox-lazy-scroll-gui-small-cache-miss-stays-in-ebox-coordinate ()
|
||||||
"GUI scroll cache misses should not synchronously render missing content."
|
"A bounded GUI cache miss should extend and scroll in one Ebox commit."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let* ((layout
|
(let* ((layout
|
||||||
(ebox-test-build
|
(ebox-test-build
|
||||||
@ -8331,7 +8331,7 @@
|
|||||||
(ebox-viewport-height 20)
|
(ebox-viewport-height 20)
|
||||||
(ebox-scroll-step 4)
|
(ebox-scroll-step 4)
|
||||||
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||||
(ebox-scroll-lazy-prefix-lookahead-lines 0)
|
(ebox-scroll-lazy-prefix-lookahead-lines 8)
|
||||||
(noninteractive nil))
|
(noninteractive nil))
|
||||||
(cl-letf (((symbol-function 'display-graphic-p)
|
(cl-letf (((symbol-function 'display-graphic-p)
|
||||||
(lambda (&optional _display) t)))
|
(lambda (&optional _display) t)))
|
||||||
@ -8355,9 +8355,22 @@
|
|||||||
(should (= boundary-offset
|
(should (= boundary-offset
|
||||||
(ebox--scroll-region-by root-id
|
(ebox--scroll-region-by root-id
|
||||||
boundary-offset)))
|
boundary-offset)))
|
||||||
(should (eq 'pending
|
(should (= 4 (ebox--scroll-region-by root-id 4)))
|
||||||
(ebox--scroll-region-by root-id 4)))))
|
(should (= (+ boundary-offset 4)
|
||||||
(should (= prefix-count 0))))
|
(plist-get (ebox--scroll-get-state root-id)
|
||||||
|
:scroll-offset)))
|
||||||
|
(should (= (+ initial-lines 12)
|
||||||
|
(length
|
||||||
|
(plist-get (ebox--scroll-get-state root-id)
|
||||||
|
:content-lines))))
|
||||||
|
(should
|
||||||
|
(= content-height
|
||||||
|
(length
|
||||||
|
(plist-get
|
||||||
|
(plist-get (ebox--buffer-render-state buffer)
|
||||||
|
:surface-fragments)
|
||||||
|
:scroll-lines))))))
|
||||||
|
(should (= prefix-count 1))))
|
||||||
(when root-id
|
(when root-id
|
||||||
(ebox--smooth-scroll-stop root-id))
|
(ebox--smooth-scroll-stop root-id))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user