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)))
|
||||
(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)
|
||||
"Copy retained style state from PREVIOUS-STATE into a weak table."
|
||||
(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))))
|
||||
(if (and visible (cadr visible))
|
||||
(let* ((lines (cadr visible))
|
||||
(output (ebox-lines-join lines))
|
||||
(fragment-data
|
||||
(ebox-surface--scroll-fragment-data lines output)))
|
||||
(when (and fragment-data
|
||||
(output (ebox-lines-join lines)))
|
||||
(when (and (ebox-surface--scroll-lines-owned-p lines)
|
||||
(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)
|
||||
(add-text-properties
|
||||
0 (length output)
|
||||
@ -3745,50 +3752,83 @@ fragment's complete paint-owner ancestor chain."
|
||||
(push copy metadata)
|
||||
(setq offset (+ offset length))))))
|
||||
|
||||
(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.
|
||||
(defun ebox-surface--owned-ranges-from-runs
|
||||
(context leaf state node-objects region-objects attach-p visit-runs)
|
||||
"Attach merged ownership produced by VISIT-RUNS 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."
|
||||
(let ((active (make-hash-table :test #'eq))
|
||||
(owner-cache (make-hash-table :test #'equal))
|
||||
(offset 0)
|
||||
ranges)
|
||||
(dolist (fragment fragments)
|
||||
(let* ((role-ids (plist-get fragment :role-ids))
|
||||
(owners (gethash role-ids owner-cache 'ebox/no-owners))
|
||||
(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))))))
|
||||
(when (eq owners 'ebox/no-owners)
|
||||
(setq owners
|
||||
(ebox-surface--fragment-owners
|
||||
role-ids state node-objects region-objects))
|
||||
(puthash role-ids owners owner-cache))
|
||||
(dolist (owner owners)
|
||||
(let* ((object (nth 0 owner))
|
||||
(tags (nth 1 owner))
|
||||
(tag-ranges (or (gethash object active)
|
||||
(let ((table (make-hash-table :test #'equal)))
|
||||
(puthash object table active)
|
||||
table)))
|
||||
(current (gethash tags tag-ranges)))
|
||||
(if (and current (= (plist-get current :end) start))
|
||||
(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)))))))
|
||||
(funcall
|
||||
visit-runs
|
||||
(lambda (role-ids start end)
|
||||
(let ((owners (gethash role-ids owner-cache 'ebox/no-owners)))
|
||||
(when (eq owners 'ebox/no-owners)
|
||||
(setq owners
|
||||
(ebox-surface--fragment-owners
|
||||
role-ids state node-objects region-objects))
|
||||
(puthash role-ids owners owner-cache))
|
||||
(dolist (owner owners)
|
||||
(let* ((object (nth 0 owner))
|
||||
(tags (nth 1 owner))
|
||||
(tag-ranges
|
||||
(or (gethash object active)
|
||||
(let ((table (make-hash-table :test #'equal)))
|
||||
(puthash object table active)
|
||||
table)))
|
||||
(current (gethash tags tag-ranges)))
|
||||
(if (and current (= (plist-get current :end) start))
|
||||
(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 ranges (nreverse ranges))
|
||||
(when attach-p
|
||||
(tp-object-attach-content-ranges-owned context leaf 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
|
||||
(context leaf template state node-objects region-objects attach-p)
|
||||
"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)
|
||||
"Return STATE's paint ledger, expanding a native frame only on demand."
|
||||
(let ((ledger (plist-get state :surface-fragments)))
|
||||
(if-let* ((frame (and (listp ledger)
|
||||
(plist-get ledger :native-frame))))
|
||||
(ebox-native-reflow-frame-fragments frame)
|
||||
ledger)))
|
||||
(let* ((ledger (plist-get state :surface-fragments))
|
||||
(frame (and (listp ledger) (plist-get ledger :native-frame)))
|
||||
(lines (and (listp ledger) (plist-get ledger :scroll-lines))))
|
||||
(cond
|
||||
(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
|
||||
(context leaf ranges state node-objects)
|
||||
@ -3875,24 +3921,26 @@ all descendant ranges remain unchanged."
|
||||
&optional transfer-text-p)
|
||||
"Return one shared-text TP plan for OUTPUT and STATE.
|
||||
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
|
||||
ebox-surface--fragments-key 'ebox/fragments))
|
||||
(fragment-data
|
||||
(or (and transfer-text-p
|
||||
(plist-get state :retained-scroll-content-p)
|
||||
(plist-get state :scroll-patch-fragment-data))
|
||||
(and (eq output (plist-get state :native-render-output))
|
||||
(plist-get state :native-render-fragment-template))
|
||||
(and (eq (plist-get state :projection-kind)
|
||||
'mixed-owner-reflow)
|
||||
(plist-get state :mixed-owner-fragment-data))
|
||||
(and (memq (plist-get state :projection-kind)
|
||||
'(span-patch owner-scoped))
|
||||
(plist-get state :span-patch-fragment-data))
|
||||
(if (stringp output)
|
||||
(ebox-surface--rendered-fragments output)
|
||||
output)))
|
||||
(unless scroll-lines
|
||||
(or (and (eq output (plist-get state :native-render-output))
|
||||
(plist-get state :native-render-fragment-template))
|
||||
(and (eq (plist-get state :projection-kind)
|
||||
'mixed-owner-reflow)
|
||||
(plist-get state :mixed-owner-fragment-data))
|
||||
(and (memq (plist-get state :projection-kind)
|
||||
'(span-patch owner-scoped))
|
||||
(plist-get state :span-patch-fragment-data))
|
||||
(if (stringp output)
|
||||
(ebox-surface--rendered-fragments output)
|
||||
output))))
|
||||
(rendered
|
||||
(if (stringp output)
|
||||
output
|
||||
@ -3922,6 +3970,10 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
||||
(eq output (plist-get state :native-render-output))))
|
||||
(owned-ranges
|
||||
(cond
|
||||
(scroll-lines
|
||||
(ebox-surface--scroll-owned-ranges
|
||||
context text-leaf scroll-lines state node-objects
|
||||
region-objects attach-p))
|
||||
((and native-template-p
|
||||
(plist-get state :native-reuse-ownership-p))
|
||||
(let ((ranges
|
||||
@ -4009,6 +4061,8 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
|
||||
(cond
|
||||
((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)
|
||||
(plist-get state :span-patch-content-p))
|
||||
;; 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 :span-patch-fragment-data)
|
||||
(cl-remf state :span-patch-content-p)
|
||||
(cl-remf state :scroll-patch-lines)
|
||||
(let ((rendered-length (length rendered)))
|
||||
(list plan rendered owned-ranges
|
||||
(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
|
||||
only replace cached line strings after those strings exist. When the user
|
||||
reaches the current lazy prefix boundary, render at most one configured
|
||||
interactive slice instead of pausing for an idle timer or materializing the
|
||||
entire source. BUDGET-LINES caps how many new source lines this call may
|
||||
request; callers that represent GUI animation ticks pass their tick size."
|
||||
reaches the current lazy prefix boundary, render one configured interactive
|
||||
slice plus the fixed lazy lookahead instead of pausing for an idle timer or
|
||||
materializing the entire source. The lookahead amortizes composite source
|
||||
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))
|
||||
(content-height (or (plist-get state :content-height) 0))
|
||||
(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-end (+ current-lines slice-lines))
|
||||
(required-lines (max current-lines
|
||||
(min visible-end slice-end)))
|
||||
(ebox-scroll-lazy-prefix-lookahead-lines 0))
|
||||
(min visible-end slice-end))))
|
||||
(ebox--scroll-state-ensure-prefix-lines
|
||||
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)))
|
||||
(when (and (> delta 0)
|
||||
(or prefix-budget-lines
|
||||
(and (ebox--scroll-sync-prefix-render-p)
|
||||
(<= delta (ebox--scroll-prefetch-slice-lines)))))
|
||||
;; A small foreground intent already has an explicit
|
||||
;; 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
|
||||
(ebox--scroll-ensure-bounded-prefix-for-offset
|
||||
region-id state desired prefix-budget-lines)))
|
||||
|
||||
@ -8309,8 +8309,8 @@
|
||||
(when (and buffer (buffer-live-p buffer))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-lazy-scroll-gui-cache-miss-returns-pending ()
|
||||
"GUI scroll cache misses should not synchronously render missing content."
|
||||
(ert-deftest ebox-lazy-scroll-gui-small-cache-miss-stays-in-ebox-coordinate ()
|
||||
"A bounded GUI cache miss should extend and scroll in one Ebox commit."
|
||||
(ebox-test--reset-runtime-state)
|
||||
(let* ((layout
|
||||
(ebox-test-build
|
||||
@ -8331,7 +8331,7 @@
|
||||
(ebox-viewport-height 20)
|
||||
(ebox-scroll-step 4)
|
||||
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||
(ebox-scroll-lazy-prefix-lookahead-lines 0)
|
||||
(ebox-scroll-lazy-prefix-lookahead-lines 8)
|
||||
(noninteractive nil))
|
||||
(cl-letf (((symbol-function 'display-graphic-p)
|
||||
(lambda (&optional _display) t)))
|
||||
@ -8355,9 +8355,22 @@
|
||||
(should (= boundary-offset
|
||||
(ebox--scroll-region-by root-id
|
||||
boundary-offset)))
|
||||
(should (eq 'pending
|
||||
(ebox--scroll-region-by root-id 4)))))
|
||||
(should (= prefix-count 0))))
|
||||
(should (= 4 (ebox--scroll-region-by root-id 4)))
|
||||
(should (= (+ boundary-offset 4)
|
||||
(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
|
||||
(ebox--smooth-scroll-stop root-id))
|
||||
(when (and buffer (buffer-live-p buffer))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user