Retain native node deltas across confirmed sessions
This commit is contained in:
parent
13e360a1a5
commit
c9131401ea
@ -9974,6 +9974,10 @@ role, and outside-complement compatibility before publication."
|
||||
(plist-put overrides :native-inherited-dirty-node-ids
|
||||
(plist-get candidate-state
|
||||
:native-inherited-dirty-node-ids)))
|
||||
(setq overrides
|
||||
(plist-put overrides :native-local-dirty-entries
|
||||
(plist-get candidate-state
|
||||
:native-local-dirty-entries)))
|
||||
(setq overrides
|
||||
(plist-put
|
||||
overrides :native-topology-stable-p
|
||||
@ -10963,9 +10967,10 @@ fresh by the mixed projection."
|
||||
((eq projection-kind 'native-frame)
|
||||
(append
|
||||
(ebox-incremental--viewport-state-overrides
|
||||
candidate-state t)
|
||||
candidate-state t)
|
||||
(list :native-topology-stable-p t
|
||||
:native-touched-node-ids nil
|
||||
:native-local-dirty-entries nil
|
||||
:native-removed-node-ids nil)))
|
||||
((memq projection-kind
|
||||
'(viewport-reflow viewport-reflow-mixed-scroll))
|
||||
|
||||
@ -339,6 +339,7 @@ new session with the same bounded configuration used by retained frames."
|
||||
(progn
|
||||
(plist-put candidate-state :native-topology-stable-p t)
|
||||
(plist-put candidate-state :native-touched-node-ids nil)
|
||||
(plist-put candidate-state :native-local-dirty-entries nil)
|
||||
(plist-put candidate-state :native-removed-node-ids nil)
|
||||
t)))
|
||||
|
||||
@ -375,6 +376,8 @@ new session with the same bounded configuration used by retained frames."
|
||||
old-state candidate-state))
|
||||
(plist-put candidate-state :native-topology-stable-p stable)
|
||||
(plist-put candidate-state :native-touched-node-ids layout-touched)
|
||||
(plist-put candidate-state :native-local-dirty-entries
|
||||
(copy-tree (plist-get prepared :dirty-set)))
|
||||
(plist-put candidate-state :native-inherited-dirty-node-ids
|
||||
(ebox-native-commit-inherited-dirty-node-ids prepared))
|
||||
(when context-axes-stable
|
||||
@ -612,6 +615,7 @@ publication transaction can still roll back."
|
||||
(1+ (or (plist-get state :runtime-revision) 0))))
|
||||
(plist-put state :native-frame-kind
|
||||
(if (plist-get frame :native-patch) 'patch 'full))
|
||||
(cl-remf state :native-local-dirty-entries)
|
||||
frame)))
|
||||
|
||||
(defconst ebox-native-commit--failed-render-state-keys
|
||||
@ -632,6 +636,7 @@ publication transaction can still roll back."
|
||||
:native-reuse-mount-projection-p
|
||||
:native-reuse-ownership-p
|
||||
:native-inherited-dirty-node-ids
|
||||
:native-local-dirty-entries
|
||||
:native-topology-stable-p
|
||||
:native-touched-node-ids
|
||||
:native-removed-node-ids)
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
|
||||
(defvar ebox--flex-content-min-width-table)
|
||||
|
||||
(defconst ebox-native-reflow-abi-version "11:7:12"
|
||||
(defconst ebox-native-reflow-abi-version "12:7:12"
|
||||
"Version tuple shared by the native module, layout IR, and render tape.")
|
||||
|
||||
(defconst ebox-native-reflow--minimum-rust-version "1.82.0"
|
||||
@ -222,7 +222,10 @@ module while loading the package."
|
||||
styles
|
||||
layout-package
|
||||
layout-fragment-cache
|
||||
layout-fragment-index
|
||||
layout-fragment-revision
|
||||
layout-style-index
|
||||
layout-property-template-index
|
||||
readiness-process
|
||||
readiness-preparation
|
||||
released-p)
|
||||
@ -286,9 +289,74 @@ module while loading the package."
|
||||
(defvar ebox-native-reflow--compile-styles nil
|
||||
"Dynamically bound ordered style registry for one Layout IR compile.")
|
||||
|
||||
(defvar ebox-native-reflow--compile-style-base-count 0
|
||||
"Number of retained styles preceding the current append batch.")
|
||||
|
||||
(defvar ebox-native-reflow--compile-style-index nil
|
||||
"Persistent retained style-to-id index for incremental compilation.")
|
||||
|
||||
(defvar ebox-native-reflow--compile-property-templates nil
|
||||
"Dynamically bound ordered opaque text-property registry.")
|
||||
|
||||
(defvar ebox-native-reflow--compile-property-template-base-count 0
|
||||
"Number of retained property templates preceding current additions.")
|
||||
|
||||
(defvar ebox-native-reflow--compile-property-template-index nil
|
||||
"Persistent retained property-template-to-id index.")
|
||||
|
||||
(defvar ebox-native-reflow--compile-retained-registry-p nil
|
||||
"Non-nil while style and property registries append incrementally.")
|
||||
|
||||
(defconst ebox-native-reflow--persistent-index-depth 16
|
||||
"Fixed number of four-bit branches in a retained native index path.")
|
||||
|
||||
(defun ebox-native-reflow--persistent-index-code (key)
|
||||
"Return a stable nonnegative retained-index code for KEY."
|
||||
(if (and (integerp key) (>= key 0))
|
||||
key
|
||||
(logand (sxhash-equal key) #xffffffffffffffff)))
|
||||
|
||||
(defun ebox-native-reflow--persistent-index-get (root key &optional missing)
|
||||
"Return KEY's value from persistent ROOT, or MISSING."
|
||||
(let ((node root)
|
||||
(code (ebox-native-reflow--persistent-index-code key))
|
||||
(depth 0))
|
||||
(while (and node (< depth ebox-native-reflow--persistent-index-depth))
|
||||
(setq node (and (vectorp node)
|
||||
(aref node (logand (ash code (* -4 depth)) 15)))
|
||||
depth (1+ depth)))
|
||||
(if (= depth ebox-native-reflow--persistent-index-depth)
|
||||
(let ((pair (cl-assoc key node :test #'equal)))
|
||||
(if pair (cdr pair) missing))
|
||||
missing)))
|
||||
|
||||
(defun ebox-native-reflow--persistent-index-put (root key value &optional depth)
|
||||
"Return persistent ROOT updated to map KEY to VALUE.
|
||||
Only the fixed-depth path for KEY is copied. DEPTH is for recursion."
|
||||
(let ((depth (or depth 0)))
|
||||
(if (= depth ebox-native-reflow--persistent-index-depth)
|
||||
(cons (cons key value)
|
||||
(cl-remove key root :key #'car :test #'equal))
|
||||
(let* ((code (ebox-native-reflow--persistent-index-code key))
|
||||
(branch (logand (ash code (* -4 depth)) 15))
|
||||
(copy (if (vectorp root)
|
||||
(copy-sequence root)
|
||||
(make-vector 16 nil))))
|
||||
(aset copy branch
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
(aref copy branch) key value (1+ depth)))
|
||||
copy))))
|
||||
|
||||
(defun ebox-native-reflow--persistent-index-from-sequence (sequence)
|
||||
"Return an immutable persistent index for ordered SEQUENCE values."
|
||||
(let (root)
|
||||
(cl-loop for value across (vconcat sequence)
|
||||
for id from 0
|
||||
do (setq root
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
root value id)))
|
||||
root))
|
||||
|
||||
(defvar ebox-native-reflow--compile-property-template-ids nil
|
||||
"Dynamically bound opaque property-template lookup table.")
|
||||
|
||||
@ -1338,14 +1406,20 @@ copied separately; no pending frame or registered Rust document is inherited."
|
||||
(condition-case err
|
||||
(progn
|
||||
(setf (ebox-native-reflow-session-styles fork)
|
||||
(copy-sequence (ebox-native-reflow-session-styles session))
|
||||
(ebox-native-reflow-session-styles session)
|
||||
(ebox-native-reflow-session-layout-package fork)
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
(ebox-native-reflow-session-layout-fragment-cache fork)
|
||||
(copy-hash-table
|
||||
(ebox-native-reflow-session-layout-fragment-cache session))
|
||||
(ebox-native-reflow-session-layout-fragment-cache session)
|
||||
(ebox-native-reflow-session-layout-fragment-index fork)
|
||||
(ebox-native-reflow-session-layout-fragment-index session)
|
||||
(ebox-native-reflow-session-layout-fragment-revision fork)
|
||||
(ebox-native-reflow-session-layout-fragment-revision session))
|
||||
(ebox-native-reflow-session-layout-fragment-revision session)
|
||||
(ebox-native-reflow-session-layout-style-index fork)
|
||||
(ebox-native-reflow-session-layout-style-index session)
|
||||
(ebox-native-reflow-session-layout-property-template-index fork)
|
||||
(ebox-native-reflow-session-layout-property-template-index
|
||||
session))
|
||||
fork)
|
||||
(error
|
||||
(ebox-native-reflow-release-session fork)
|
||||
@ -1409,7 +1483,11 @@ copied separately; no pending frame or registered Rust document is inherited."
|
||||
:properties owned-properties))
|
||||
(missing (make-symbol "missing-property-template"))
|
||||
(existing
|
||||
(if (hash-table-p
|
||||
(if ebox-native-reflow--compile-retained-registry-p
|
||||
(ebox-native-reflow--persistent-index-get
|
||||
ebox-native-reflow--compile-property-template-index
|
||||
entry missing)
|
||||
(if (hash-table-p
|
||||
ebox-native-reflow--compile-property-template-ids)
|
||||
(gethash
|
||||
entry ebox-native-reflow--compile-property-template-ids
|
||||
@ -1417,13 +1495,19 @@ copied separately; no pending frame or registered Rust document is inherited."
|
||||
(or (cl-position
|
||||
entry ebox-native-reflow--compile-property-templates
|
||||
:test #'equal)
|
||||
missing))))
|
||||
missing)))))
|
||||
(if (not (eq existing missing))
|
||||
existing
|
||||
(let ((id (length ebox-native-reflow--compile-property-templates)))
|
||||
(let ((id (+ ebox-native-reflow--compile-property-template-base-count
|
||||
(length ebox-native-reflow--compile-property-templates))))
|
||||
(setq ebox-native-reflow--compile-property-templates
|
||||
(append ebox-native-reflow--compile-property-templates
|
||||
(list entry)))
|
||||
(when ebox-native-reflow--compile-retained-registry-p
|
||||
(setq ebox-native-reflow--compile-property-template-index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
ebox-native-reflow--compile-property-template-index
|
||||
entry id)))
|
||||
(when (hash-table-p
|
||||
ebox-native-reflow--compile-property-template-ids)
|
||||
(puthash
|
||||
@ -1592,13 +1676,27 @@ When FORMATTED-P is non-nil, preserve exact display spaces produced by KP."
|
||||
(defun ebox-native-reflow--register-style (mode face)
|
||||
"Return the stable numeric id for a MODE and FACE operation."
|
||||
(let* ((entry (list :mode mode :face face))
|
||||
(existing (cl-position entry ebox-native-reflow--compile-styles
|
||||
:test #'equal)))
|
||||
(or existing
|
||||
(prog1 (length ebox-native-reflow--compile-styles)
|
||||
(missing (make-symbol "missing-native-style"))
|
||||
(existing
|
||||
(if ebox-native-reflow--compile-retained-registry-p
|
||||
(ebox-native-reflow--persistent-index-get
|
||||
ebox-native-reflow--compile-style-index entry missing)
|
||||
(or (cl-position entry ebox-native-reflow--compile-styles
|
||||
:test #'equal)
|
||||
missing))))
|
||||
(if (not (eq existing missing))
|
||||
existing
|
||||
(prog1 (+ ebox-native-reflow--compile-style-base-count
|
||||
(length ebox-native-reflow--compile-styles))
|
||||
(setq ebox-native-reflow--compile-styles
|
||||
(append ebox-native-reflow--compile-styles
|
||||
(list entry)))))))
|
||||
(list entry)))
|
||||
(when ebox-native-reflow--compile-retained-registry-p
|
||||
(setq ebox-native-reflow--compile-style-index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
ebox-native-reflow--compile-style-index entry
|
||||
(+ ebox-native-reflow--compile-style-base-count
|
||||
(1- (length ebox-native-reflow--compile-styles))))))))))
|
||||
|
||||
(defun ebox-native-reflow--compile-add-face-style (face)
|
||||
"Register additive FACE and return its id, or nil."
|
||||
@ -2286,6 +2384,16 @@ Otherwise return CANDIDATE with the next document revision."
|
||||
(plist-put replacement :document-revision
|
||||
(1+ (or (plist-get old :document-revision) 0)))))))
|
||||
|
||||
(defun ebox-native-reflow--confirmed-layout-package (package)
|
||||
"Return PACKAGE without one-shot retained input transport fields."
|
||||
(if (plist-get package :document-delta)
|
||||
(let ((confirmed (copy-sequence package)))
|
||||
(dolist (key '(:document-delta :native-fragment-index
|
||||
:native-fragment-revision :native-style-index))
|
||||
(cl-remf confirmed key))
|
||||
confirmed)
|
||||
package))
|
||||
|
||||
(defun ebox-native-reflow--buffer-display-signature (buffer)
|
||||
"Return BUFFER's live canonical Surface display capability."
|
||||
(with-current-buffer buffer
|
||||
@ -2491,9 +2599,9 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
(gethash child flex-content-min-widths)))
|
||||
(ebox-native-reflow--retained-layout-children node))))))
|
||||
|
||||
(defun ebox-native-reflow--compile-retained-layout-package
|
||||
(defun ebox-native-reflow--compile-retained-layout-package-full
|
||||
(session state node)
|
||||
"Compile NODE incrementally into retained native SESSION from STATE."
|
||||
"Compile a complete retained package for NODE in SESSION from STATE."
|
||||
(let ((postorder (plist-get state :native-node-postorder)))
|
||||
(unless (and (vectorp postorder) (> (length postorder) 0))
|
||||
(setq postorder
|
||||
@ -2632,7 +2740,9 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
(setq fragment
|
||||
(ebox-native-reflow--compile-scene-node
|
||||
current fragments flex-content-min-widths)
|
||||
fragment (plist-put fragment :node-id node-id)
|
||||
revision (1+ revision)
|
||||
fragment (plist-put fragment :node-revision revision)
|
||||
entry (list :signature (copy-tree signature)
|
||||
:fragment fragment
|
||||
:revision revision)))
|
||||
@ -2658,10 +2768,354 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
candidate)))
|
||||
(setf (ebox-native-reflow-session-layout-fragment-cache session)
|
||||
new-cache
|
||||
(ebox-native-reflow-session-layout-fragment-index session)
|
||||
(let (index)
|
||||
(maphash
|
||||
(lambda (node-id entry)
|
||||
(setq index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
index node-id entry)))
|
||||
new-cache)
|
||||
index)
|
||||
(ebox-native-reflow-session-layout-fragment-revision session)
|
||||
revision)
|
||||
revision
|
||||
(ebox-native-reflow-session-layout-style-index session)
|
||||
(ebox-native-reflow--persistent-index-from-sequence
|
||||
(plist-get package :styles))
|
||||
(ebox-native-reflow-session-layout-property-template-index
|
||||
session)
|
||||
(ebox-native-reflow--persistent-index-from-sequence
|
||||
(plist-get package :property-templates)))
|
||||
package)))))
|
||||
|
||||
(defconst ebox-native-reflow--delta-edge-fields
|
||||
'(:type :child :children :items :node-id :node-revision)
|
||||
"Fragment fields that a stable local node delta cannot replace.")
|
||||
|
||||
(defconst ebox-native-reflow--delta-flex-edge-properties
|
||||
'(:order :flex-grow :flex-shrink :flex-basis :align-self
|
||||
:flex-direction :cross-align)
|
||||
"Changed properties that alter retained Flex item metadata in N1.")
|
||||
|
||||
(defun ebox-native-reflow--delta-flex-edge-change-p (state)
|
||||
"Return non-nil when STATE changes unsupported Flex edge metadata."
|
||||
(cl-some
|
||||
(lambda (dirty)
|
||||
(let* ((keys (plist-get dirty :changed-keys))
|
||||
(parents (plist-get state :parent-table))
|
||||
(nodes (plist-get state :node-table))
|
||||
(dirty-id (plist-get dirty :node-id))
|
||||
(dirty-node (and nodes (gethash dirty-id nodes)))
|
||||
(raw-parent-id (and parents (gethash dirty-id parents)))
|
||||
(raw-parent (and nodes (gethash raw-parent-id nodes)))
|
||||
(owner-id
|
||||
(if (and (eq (plist-get dirty-node :ebox-kind) 'text)
|
||||
(eq (plist-get raw-parent :ebox-kind) 'box)
|
||||
(when-let* ((layout
|
||||
(plist-get raw-parent
|
||||
:ebox-layout-config)))
|
||||
(eq (ebox-layout-config-kind layout) 'normal)))
|
||||
raw-parent-id
|
||||
dirty-id))
|
||||
(edge-parent-id (and parents (gethash owner-id parents)))
|
||||
(edge-parent (and nodes (gethash edge-parent-id nodes)))
|
||||
(config
|
||||
(and edge-parent
|
||||
(plist-get edge-parent :ebox-layout-config))))
|
||||
(or
|
||||
(cl-some (lambda (key)
|
||||
(memq key ebox-native-reflow--delta-flex-edge-properties))
|
||||
keys)
|
||||
;; A child-local edit can change content-min-width in a Flex item or
|
||||
;; derived align-self in typed axis lowering. N1 has no edge patch,
|
||||
;; so retain correctness through full input until exact edge facts are
|
||||
;; independently addressable.
|
||||
(or (eq (plist-get edge-parent :ebox-type) 'flex)
|
||||
(and config
|
||||
(let ((kind (ebox-layout-config-kind config))
|
||||
(props (ebox-layout-config-props config)))
|
||||
(or (eq kind 'flex)
|
||||
(and (memq kind '(row column))
|
||||
(not (and (equal (plist-get props :item-gap) 0)
|
||||
(eq (plist-get props :cross-align)
|
||||
'stretch)))))))))))
|
||||
(plist-get state :native-local-dirty-entries)))
|
||||
|
||||
(defun ebox-native-reflow--fragment-local (fragment)
|
||||
"Return FRAGMENT's scalar and local-content fields."
|
||||
(let ((tail fragment) local)
|
||||
(while tail
|
||||
(let ((key (pop tail))
|
||||
(value (pop tail)))
|
||||
(unless (memq key ebox-native-reflow--delta-edge-fields)
|
||||
(setq local (nconc local (list key value))))))
|
||||
local))
|
||||
|
||||
(defun ebox-native-reflow--local-difference (old new)
|
||||
"Return NEW local fields whose values differ from OLD."
|
||||
(let ((tail new) difference)
|
||||
(while tail
|
||||
(let ((key (pop tail))
|
||||
(value (pop tail)))
|
||||
(unless (equal value (plist-get old key))
|
||||
(setq difference (nconc difference (list key value))))))
|
||||
difference))
|
||||
|
||||
(defun ebox-native-reflow--apply-local-difference (fragment difference)
|
||||
"Return FRAGMENT with scalar fields from DIFFERENCE replaced."
|
||||
(let ((copy (copy-sequence fragment))
|
||||
(tail difference))
|
||||
(while tail
|
||||
(setq copy (plist-put copy (pop tail) (pop tail))))
|
||||
copy))
|
||||
|
||||
(defun ebox-native-reflow--delta-owner-id (state node-id index)
|
||||
"Return addressable owner for NODE-ID in STATE and retained INDEX."
|
||||
(let ((parents (plist-get state :parent-table))
|
||||
(nodes (plist-get state :node-table))
|
||||
(current node-id))
|
||||
(when-let* ((source (and nodes (gethash current nodes)))
|
||||
(parent-id (and parents (gethash current parents)))
|
||||
(parent (and nodes (gethash parent-id nodes))))
|
||||
(when (and (eq (plist-get source :ebox-kind) 'text)
|
||||
(eq (plist-get parent :ebox-kind) 'box)
|
||||
(eq (ebox-layout-config-kind
|
||||
(plist-get parent :ebox-layout-config))
|
||||
'normal))
|
||||
(setq current parent-id)))
|
||||
(while (and current
|
||||
(null (ebox-native-reflow--persistent-index-get
|
||||
index current)))
|
||||
(setq current (and parents (gethash current parents))))
|
||||
current))
|
||||
|
||||
(defun ebox-native-reflow--compile-delta-slots (node old-fragment)
|
||||
"Compile NODE local slots while retaining OLD-FRAGMENT edges."
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('box
|
||||
(if (eq (plist-get node :ebox-kind) 'text)
|
||||
(vector (ebox-native-reflow--compile-text-node node))
|
||||
(let* ((config (and (eq (plist-get node :ebox-kind) 'box)
|
||||
(plist-get node :ebox-layout-config)))
|
||||
(kind (and config (ebox-layout-config-kind config)))
|
||||
(old-child (plist-get old-fragment :child))
|
||||
(fused-p (and (eq kind 'normal)
|
||||
(let ((child (car (ebox-tree-node-children node))))
|
||||
(and child
|
||||
(eq (plist-get child :ebox-kind) 'text)))))
|
||||
(inner
|
||||
(pcase kind
|
||||
((or 'row 'column)
|
||||
(ebox-native-reflow--compile-typed-axis-children
|
||||
kind config nil))
|
||||
('flex
|
||||
(ebox-native-reflow--compile-flex-inner
|
||||
(copy-sequence (ebox-layout-config-props config)) nil))
|
||||
(_ nil)))
|
||||
(child (cond (fused-p
|
||||
(ebox-native-reflow--compile-text-node
|
||||
(car (ebox-tree-node-children node))))
|
||||
(inner inner)
|
||||
(t old-child)))
|
||||
(outer (ebox-native-reflow--compile-box node child nil t)))
|
||||
(if inner (vector outer inner) (vector outer)))))
|
||||
('flex
|
||||
(let* ((wrapper (plist-get node :box))
|
||||
(props (ebox--flex-container-content-props
|
||||
(plist-get node :props) (plist-get node :raw-props)
|
||||
wrapper)))
|
||||
(when wrapper
|
||||
(plist-put props :width nil)
|
||||
(when (plist-member (plist-get node :raw-props) :height)
|
||||
(plist-put props :height 'viewport-height)))
|
||||
(let ((inner (ebox-native-reflow--compile-flex-inner props nil)))
|
||||
(if wrapper
|
||||
(vector (ebox-native-reflow--compile-box wrapper inner t) inner)
|
||||
(vector inner)))))
|
||||
(_ (vector old-fragment))))
|
||||
|
||||
(defun ebox-native-reflow--delta-direct-owner-ids (state index)
|
||||
"Return owners for STATE's direct dirty entries found in INDEX."
|
||||
(let ((seen (make-hash-table :test 'equal)) result)
|
||||
(dolist (dirty (plist-get state :native-local-dirty-entries))
|
||||
(when-let* ((owner
|
||||
(ebox-native-reflow--delta-owner-id
|
||||
state (plist-get dirty :node-id) index)))
|
||||
(unless (gethash owner seen)
|
||||
(puthash owner t seen)
|
||||
(push owner result))))
|
||||
(nreverse result)))
|
||||
|
||||
(defun ebox-native-reflow--compile-retained-layout-delta
|
||||
(session state node)
|
||||
"Return SESSION's local delta for NODE under STATE, or nil for fallback."
|
||||
(let* ((old-package (ebox-native-reflow-session-layout-package session))
|
||||
(old-index (ebox-native-reflow-session-layout-fragment-index session))
|
||||
(node-table (plist-get state :node-table))
|
||||
(touched (plist-get state :native-touched-node-ids)))
|
||||
(when (and old-package old-index (hash-table-p node-table)
|
||||
(plist-get state :native-topology-stable-p)
|
||||
(plist-member state :native-local-dirty-entries)
|
||||
(not (ebox-native-reflow--delta-flex-edge-change-p state))
|
||||
;; Descendant inherited-style expansion needs its own bounded
|
||||
;; affected-node index. Until then it is an explicit full
|
||||
;; input fallback rather than a partial semantic update.
|
||||
(null (plist-get state :native-inherited-dirty-node-ids)))
|
||||
(if (null touched)
|
||||
old-package
|
||||
(let* ((style-base (length (plist-get old-package :styles)))
|
||||
(template-base
|
||||
(length (plist-get old-package :property-templates)))
|
||||
(ebox-native-reflow--compile-styles nil)
|
||||
(ebox-native-reflow--compile-style-base-count style-base)
|
||||
(ebox-native-reflow--compile-style-index
|
||||
(ebox-native-reflow-session-layout-style-index session))
|
||||
(ebox-native-reflow--compile-property-templates nil)
|
||||
(ebox-native-reflow--compile-property-template-base-count
|
||||
template-base)
|
||||
(ebox-native-reflow--compile-property-template-index
|
||||
(ebox-native-reflow-session-layout-property-template-index
|
||||
session))
|
||||
(ebox-native-reflow--compile-retained-registry-p t)
|
||||
(ebox-native-reflow--compile-display-signature
|
||||
(ebox--current-display-signature))
|
||||
(ebox-native-reflow--compile-root-node node)
|
||||
(ebox--render-string-pixel-width-cache
|
||||
(make-hash-table :test 'equal))
|
||||
(ebox--render-string-max-pixel-width-cache
|
||||
(make-hash-table :test 'eq))
|
||||
(direct (ebox-native-reflow--delta-direct-owner-ids
|
||||
state old-index))
|
||||
(direct-set (make-hash-table :test 'equal))
|
||||
(entry-ids nil)
|
||||
(seen (make-hash-table :test 'equal))
|
||||
(new-index old-index)
|
||||
(revision
|
||||
(or (ebox-native-reflow-session-layout-fragment-revision
|
||||
session) 0))
|
||||
entries unsupported)
|
||||
(dolist (id direct) (puthash id t direct-set))
|
||||
(dolist (id touched)
|
||||
(when-let* ((owner
|
||||
(ebox-native-reflow--delta-owner-id state id old-index)))
|
||||
(unless (gethash owner seen)
|
||||
(puthash owner t seen)
|
||||
(push owner entry-ids))))
|
||||
(dolist (owner-id (nreverse entry-ids))
|
||||
(let* ((cached (ebox-native-reflow--persistent-index-get
|
||||
old-index owner-id))
|
||||
(old-fragment (plist-get cached :fragment))
|
||||
(source (gethash owner-id node-table))
|
||||
(expected (plist-get cached :revision))
|
||||
patches new-fragment)
|
||||
(unless (and cached source (integerp expected))
|
||||
(setq unsupported t))
|
||||
(when (and (not unsupported) (gethash owner-id direct-set))
|
||||
(let ((slots
|
||||
(ebox-native-reflow--compile-delta-slots
|
||||
source old-fragment)))
|
||||
(unless (equal (plist-get (aref slots 0) :type)
|
||||
(plist-get old-fragment :type))
|
||||
(setq unsupported t))
|
||||
(unless unsupported
|
||||
(let ((local
|
||||
(ebox-native-reflow--local-difference
|
||||
(ebox-native-reflow--fragment-local old-fragment)
|
||||
(ebox-native-reflow--fragment-local
|
||||
(aref slots 0)))))
|
||||
(when local
|
||||
(push (list :slot 0 :local local) patches)
|
||||
(setq new-fragment
|
||||
(ebox-native-reflow--apply-local-difference
|
||||
old-fragment local))))
|
||||
(when (> (length slots) 1)
|
||||
(let ((old-inner (plist-get old-fragment :child))
|
||||
(new-inner (aref slots 1)))
|
||||
(unless (equal (plist-get old-inner :type)
|
||||
(plist-get new-inner :type))
|
||||
(setq unsupported t))
|
||||
(unless unsupported
|
||||
(let ((local
|
||||
(ebox-native-reflow--local-difference
|
||||
(ebox-native-reflow--fragment-local old-inner)
|
||||
(ebox-native-reflow--fragment-local new-inner))))
|
||||
(when local
|
||||
(push (list :slot 1 :local local) patches)
|
||||
(let ((outer (or new-fragment
|
||||
(copy-sequence old-fragment))))
|
||||
(plist-put
|
||||
outer :child
|
||||
(ebox-native-reflow--apply-local-difference
|
||||
old-inner local))
|
||||
(setq new-fragment outer))))))))))
|
||||
(unless unsupported
|
||||
(setq revision (1+ revision))
|
||||
(unless new-fragment
|
||||
(setq new-fragment (copy-sequence old-fragment)))
|
||||
(plist-put new-fragment :node-id owner-id)
|
||||
(plist-put new-fragment :node-revision revision)
|
||||
(let* ((entry
|
||||
(append
|
||||
(list :node-id owner-id
|
||||
:expected-revision expected
|
||||
:target-revision revision)
|
||||
(when patches
|
||||
(list :slot-patches (vconcat (nreverse patches))))))
|
||||
(new-cache-entry
|
||||
(list :signature nil
|
||||
:fragment (or new-fragment old-fragment)
|
||||
:revision revision)))
|
||||
(push entry entries)
|
||||
(setq new-index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
new-index owner-id new-cache-entry))))))
|
||||
(when (and (not unsupported)
|
||||
;; N1 can authenticate retained templates, but cannot
|
||||
;; append their Emacs-owned values to Rust atomically.
|
||||
(= template-base
|
||||
(+ template-base
|
||||
(length ebox-native-reflow--compile-property-templates))))
|
||||
(let* ((styles-append ebox-native-reflow--compile-styles)
|
||||
(styles
|
||||
(if styles-append
|
||||
;; Materializing the global style output is explicitly
|
||||
;; outside retained input work; the common no-append
|
||||
;; path preserves the old vector by identity.
|
||||
(vconcat (plist-get old-package :styles) styles-append)
|
||||
(plist-get old-package :styles)))
|
||||
(base (or (plist-get old-package :document-revision) 0))
|
||||
(package (copy-sequence old-package)))
|
||||
(plist-put package :document-revision (1+ base))
|
||||
(plist-put package :styles styles)
|
||||
(plist-put
|
||||
package :document-delta
|
||||
(list :style-base-count style-base
|
||||
:styles-append
|
||||
(vconcat
|
||||
(mapcar
|
||||
(lambda (descriptor)
|
||||
(list :mode (symbol-name (plist-get descriptor :mode))
|
||||
:face
|
||||
(list :lisp
|
||||
(prin1-to-string
|
||||
(plist-get descriptor :face)))))
|
||||
styles-append))
|
||||
:property-template-base-count template-base
|
||||
:property-template-target-count template-base
|
||||
:entries (vconcat (nreverse entries))))
|
||||
(plist-put package :native-fragment-index new-index)
|
||||
(plist-put package :native-fragment-revision revision)
|
||||
(plist-put package :native-style-index
|
||||
ebox-native-reflow--compile-style-index)
|
||||
package)))))))
|
||||
|
||||
(defun ebox-native-reflow--compile-retained-layout-package
|
||||
(session state node)
|
||||
"Compile NODE for SESSION using a delta when STATE proves it supported."
|
||||
(or (ebox-native-reflow--compile-retained-layout-delta session state node)
|
||||
(ebox-native-reflow--compile-retained-layout-package-full
|
||||
session state node)))
|
||||
|
||||
(defun ebox-native-reflow-compile-layout-ir (node)
|
||||
"Compile NODE to one complete versioned native layout document."
|
||||
(plist-get (ebox-native-reflow--compile-layout-package node) :document))
|
||||
@ -2682,9 +3136,10 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
|
||||
(defun ebox-native-reflow--layout-control-json
|
||||
(document frames &optional document-base-revision
|
||||
document-target-revision)
|
||||
document-target-revision document-delta)
|
||||
"Return strict control JSON for optional layout DOCUMENT and FRAMES.
|
||||
DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input."
|
||||
DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input.
|
||||
DOCUMENT-DELTA is the stable-topology local replacement batch."
|
||||
(unless (and (listp frames) frames)
|
||||
(error "Native reflow requires at least one layout frame"))
|
||||
(let ((control
|
||||
@ -2792,6 +3247,8 @@ DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input."
|
||||
document-target-revision)))
|
||||
(when document
|
||||
(setq control (plist-put control :document document)))
|
||||
(when document-delta
|
||||
(setq control (plist-put control :document-delta document-delta)))
|
||||
(ebox-native-reflow--serialize-layout-control control)))
|
||||
|
||||
(defun ebox-native-reflow-submit-layout
|
||||
@ -2807,9 +3264,12 @@ LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE."
|
||||
node
|
||||
(ebox-native-reflow-session-styles session)
|
||||
(plist-get old-package :property-templates))))
|
||||
(document-delta (plist-get candidate-package :document-delta))
|
||||
(package
|
||||
(ebox-native-reflow--reuse-exact-layout-package
|
||||
old-package candidate-package))
|
||||
(if document-delta
|
||||
candidate-package
|
||||
(ebox-native-reflow--reuse-exact-layout-package
|
||||
old-package candidate-package)))
|
||||
(register-layout-p
|
||||
(not (eq package
|
||||
old-package)))
|
||||
@ -2821,12 +3281,14 @@ LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE."
|
||||
(ebox-native-reflow--live-handle session)
|
||||
generation
|
||||
(ebox-native-reflow--layout-control-json
|
||||
(and register-layout-p (plist-get package :document)) frames
|
||||
base-revision target-revision))))
|
||||
(and register-layout-p (not document-delta)
|
||||
(plist-get package :document))
|
||||
frames base-revision target-revision document-delta))))
|
||||
(setf (ebox-native-reflow-session-generation session) generation)
|
||||
(setf (ebox-native-reflow-session-styles session)
|
||||
(plist-get package :styles))
|
||||
(setf (ebox-native-reflow-session-layout-package session) package)
|
||||
(setf (ebox-native-reflow-session-layout-package session)
|
||||
(ebox-native-reflow--confirmed-layout-package package))
|
||||
accepted))
|
||||
|
||||
(defun ebox-native-reflow--tape-cursor (payload &optional position)
|
||||
@ -3968,13 +4430,16 @@ root effect metadata required by a thin host commit."
|
||||
(plist-get
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
:property-templates)))))
|
||||
(document-delta (plist-get candidate-package :document-delta))
|
||||
(package
|
||||
(ebox-native-reflow--reuse-exact-layout-package
|
||||
old-package candidate-package))
|
||||
(if document-delta
|
||||
candidate-package
|
||||
(ebox-native-reflow--reuse-exact-layout-package
|
||||
old-package candidate-package)))
|
||||
(generation (1+ (ebox-native-reflow-session-generation session)))
|
||||
(control-frame (copy-sequence frame))
|
||||
(native-key (or (plist-get control-frame :key) 1))
|
||||
(reuse-document-p (eq package old-package))
|
||||
(reuse-document-p (or document-delta (eq package old-package)))
|
||||
(base-revision (or (plist-get old-package :document-revision) 0))
|
||||
(target-revision (or (plist-get package :document-revision)
|
||||
(1+ base-revision))))
|
||||
@ -3986,12 +4451,22 @@ root effect metadata required by a thin host commit."
|
||||
generation
|
||||
(ebox-native-reflow--layout-control-json
|
||||
(unless reuse-document-p (plist-get package :document))
|
||||
(list control-frame) base-revision target-revision))
|
||||
(list control-frame) base-revision target-revision
|
||||
document-delta))
|
||||
package control-frame generation)))
|
||||
(setf (ebox-native-reflow-session-generation session) generation
|
||||
(ebox-native-reflow-session-layout-package session) package
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
(ebox-native-reflow--confirmed-layout-package package)
|
||||
(ebox-native-reflow-session-styles session)
|
||||
(plist-get package :styles))
|
||||
(when document-delta
|
||||
(setf (ebox-native-reflow-session-layout-fragment-cache session) nil
|
||||
(ebox-native-reflow-session-layout-fragment-index session)
|
||||
(plist-get package :native-fragment-index)
|
||||
(ebox-native-reflow-session-layout-fragment-revision session)
|
||||
(plist-get package :native-fragment-revision)
|
||||
(ebox-native-reflow-session-layout-style-index session)
|
||||
(plist-get package :native-style-index)))
|
||||
result)))
|
||||
|
||||
(defun ebox-native-reflow-render-proof-sync
|
||||
|
||||
@ -278,6 +278,12 @@ rather than relying on an implicit default.")
|
||||
(:symbol ebox-native-reflow--compile-property-template-ids
|
||||
:reason dynamically-bound-compile-local-scratch
|
||||
:evidence let-bound-per-compile-and-never-committed)
|
||||
(:symbol ebox-native-reflow--compile-property-template-index
|
||||
:reason dynamically-bound-compile-local-scratch
|
||||
:evidence let-bound-per-compile-and-published-only-through-session-root)
|
||||
(:symbol ebox-native-reflow--compile-style-index
|
||||
:reason dynamically-bound-compile-local-scratch
|
||||
:evidence let-bound-per-compile-and-published-only-through-session-root)
|
||||
(:symbol ebox--render-source-generations
|
||||
:reason dynamically-bound-render-proof-input
|
||||
:evidence let-bound-to-candidate-and-prior-generations-for-one-plan)
|
||||
|
||||
@ -164,7 +164,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
|
||||
(void) nargs;
|
||||
(void) args;
|
||||
(void) data;
|
||||
static const char version[] = "11:7:12";
|
||||
static const char version[] = "12:7:12";
|
||||
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
|
||||
}
|
||||
|
||||
|
||||
1717
native/src/layout.rs
1717
native/src/layout.rs
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,8 @@
|
||||
mod layout;
|
||||
|
||||
use layout::{
|
||||
encode_error_tape, LayoutContext, LayoutDocument, LayoutTape, TapeIdentity, TapeOutputOptions,
|
||||
MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES,
|
||||
encode_error_tape, DocumentDelta, LayoutContext, LayoutDocument, LayoutTape, RetainedDocument,
|
||||
TapeIdentity, TapeOutputOptions, MAX_LAYOUT_DIMENSION, MIN_TAPE_BYTES,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
@ -324,6 +324,8 @@ struct ControlBatch {
|
||||
version: u32,
|
||||
#[serde(default)]
|
||||
document: Option<LayoutDocument>,
|
||||
#[serde(default, rename = "document-delta")]
|
||||
document_delta: Option<DocumentDelta>,
|
||||
#[serde(default, rename = "document-base-revision")]
|
||||
document_base_revision: Option<u64>,
|
||||
#[serde(default, rename = "document-target-revision")]
|
||||
@ -375,7 +377,7 @@ struct ControlFrame {
|
||||
enum JobPayload {
|
||||
Echo(Vec<u8>),
|
||||
Layout {
|
||||
document: Arc<LayoutDocument>,
|
||||
document: LayoutSource,
|
||||
context: LayoutContext,
|
||||
root_width: i64,
|
||||
root_width_override: bool,
|
||||
@ -388,9 +390,43 @@ enum JobPayload {
|
||||
root_metadata: bool,
|
||||
document_base_revision: u64,
|
||||
document_target_revision: u64,
|
||||
validation_resolver_lookups: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum LayoutSource {
|
||||
Full(Arc<LayoutDocument>),
|
||||
Retained(Arc<RetainedDocument>),
|
||||
}
|
||||
|
||||
impl LayoutSource {
|
||||
fn validate_context(&self, context: LayoutContext) -> Result<(), String> {
|
||||
match self {
|
||||
Self::Full(document) => document.validate_context(context),
|
||||
Self::Retained(document) => document.validate_context(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_tape(
|
||||
&self,
|
||||
context: LayoutContext,
|
||||
root_width: Option<i64>,
|
||||
) -> Result<LayoutTape, String> {
|
||||
match self {
|
||||
Self::Full(document) => document.layout_tape(context, root_width),
|
||||
Self::Retained(document) => document.layout_tape(context, root_width),
|
||||
}
|
||||
}
|
||||
|
||||
fn styles(&self) -> Result<Vec<layout::StyleTemplate>, String> {
|
||||
match self {
|
||||
Self::Full(document) => Ok(document.styles.clone()),
|
||||
Self::Retained(document) => document.styles(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BaselineIdentity {
|
||||
context: LayoutContext,
|
||||
@ -404,7 +440,7 @@ struct BaselineIdentity {
|
||||
#[derive(Clone, Debug)]
|
||||
struct ConfirmedBaseline {
|
||||
identity: BaselineIdentity,
|
||||
document: Arc<LayoutDocument>,
|
||||
document: LayoutSource,
|
||||
document_revision: u64,
|
||||
tape: LayoutTape,
|
||||
styles: Vec<layout::StyleTemplate>,
|
||||
@ -413,7 +449,7 @@ struct ConfirmedBaseline {
|
||||
#[derive(Debug)]
|
||||
struct PendingBaseline {
|
||||
confirmed_identity: BaselineIdentity,
|
||||
document: Arc<LayoutDocument>,
|
||||
document: LayoutSource,
|
||||
document_revision: u64,
|
||||
tape: LayoutTape,
|
||||
styles: Vec<layout::StyleTemplate>,
|
||||
@ -431,6 +467,20 @@ struct RenderedJob {
|
||||
baseline_hit: bool,
|
||||
base_renders: u64,
|
||||
target_renders: u64,
|
||||
resolver_lookups: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DocumentInputStats {
|
||||
parses: u64,
|
||||
validations: u64,
|
||||
reuses: u64,
|
||||
full_input_bytes: u64,
|
||||
delta_input_bytes: u64,
|
||||
full_nodes_parsed: u64,
|
||||
delta_entries_parsed: u64,
|
||||
delta_entries_validated: u64,
|
||||
trie_path_nodes_copied: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -463,6 +513,13 @@ struct RuntimeState {
|
||||
document_parses: u64,
|
||||
document_validations: u64,
|
||||
document_reuses: u64,
|
||||
document_full_input_bytes: u64,
|
||||
document_delta_input_bytes: u64,
|
||||
document_full_nodes_parsed: u64,
|
||||
document_delta_entries_parsed: u64,
|
||||
document_delta_entries_validated: u64,
|
||||
document_trie_path_nodes_copied: u64,
|
||||
document_resolver_lookups: u64,
|
||||
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
||||
}
|
||||
|
||||
@ -491,7 +548,7 @@ struct Shared {
|
||||
struct Session {
|
||||
shared: Arc<Shared>,
|
||||
workers: Mutex<Option<Vec<JoinHandle<()>>>>,
|
||||
layout_document: Mutex<Option<Arc<LayoutDocument>>>,
|
||||
layout_document: Mutex<Option<LayoutSource>>,
|
||||
worker_count: usize,
|
||||
}
|
||||
|
||||
@ -516,6 +573,13 @@ struct SessionStats {
|
||||
document_parses: u64,
|
||||
document_validations: u64,
|
||||
document_reuses: u64,
|
||||
document_full_input_bytes: u64,
|
||||
document_delta_input_bytes: u64,
|
||||
document_full_nodes_parsed: u64,
|
||||
document_delta_entries_parsed: u64,
|
||||
document_delta_entries_validated: u64,
|
||||
document_trie_path_nodes_copied: u64,
|
||||
document_resolver_lookups: u64,
|
||||
pending_baselines: usize,
|
||||
confirmed_baseline: bool,
|
||||
confirmed_baseline_bytes: usize,
|
||||
@ -581,6 +645,13 @@ impl Session {
|
||||
document_parses: 0,
|
||||
document_validations: 0,
|
||||
document_reuses: 0,
|
||||
document_full_input_bytes: 0,
|
||||
document_delta_input_bytes: 0,
|
||||
document_full_nodes_parsed: 0,
|
||||
document_delta_entries_parsed: 0,
|
||||
document_delta_entries_validated: 0,
|
||||
document_trie_path_nodes_copied: 0,
|
||||
document_resolver_lookups: 0,
|
||||
confirmed_baseline,
|
||||
}),
|
||||
readiness_channel: Mutex::new(None),
|
||||
@ -611,7 +682,7 @@ impl Session {
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.confirmed_baseline
|
||||
.as_ref()
|
||||
.map(|baseline| Arc::clone(&baseline.document));
|
||||
.map(|baseline| baseline.document.clone());
|
||||
Ok(Box::new(Self {
|
||||
shared,
|
||||
workers: Mutex::new(Some(handles)),
|
||||
@ -643,6 +714,9 @@ impl Session {
|
||||
|
||||
fn submit(&self, generation: u64, payload: &[u8]) -> Result<usize, String> {
|
||||
let batch = parse_control_batch(payload)?;
|
||||
if batch.document_delta.is_some() {
|
||||
return Err("Native async reflow does not accept retained document deltas".to_owned());
|
||||
}
|
||||
if !self.shared.alive.load(Ordering::Acquire) {
|
||||
return Err("Native reflow session is closed".to_owned());
|
||||
}
|
||||
@ -672,12 +746,12 @@ impl Session {
|
||||
let document = match batch.document {
|
||||
Some(document) => {
|
||||
document.validate()?;
|
||||
Some(Arc::new(document))
|
||||
Some(LayoutSource::Full(Arc::new(document)))
|
||||
}
|
||||
None if layout_requested => Some(
|
||||
confirmed_baseline
|
||||
.as_ref()
|
||||
.map(|baseline| Arc::clone(&baseline.document))
|
||||
.map(|baseline| baseline.document.clone())
|
||||
.or_else(|| {
|
||||
self.layout_document
|
||||
.lock()
|
||||
@ -852,10 +926,13 @@ impl Session {
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.confirmed_baseline
|
||||
.clone();
|
||||
let (document, document_parses, document_validations, document_reuses) = match batch
|
||||
.document
|
||||
{
|
||||
Some(document) => {
|
||||
let (document, input_stats) = match (batch.document, batch.document_delta) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(
|
||||
"Native retained render cannot combine document and document-delta".to_owned(),
|
||||
);
|
||||
}
|
||||
(Some(document), None) => {
|
||||
if document_base_revision.checked_add(1) != Some(document_target_revision) {
|
||||
return Err(
|
||||
"Native retained document replacement must advance one revision".to_owned(),
|
||||
@ -876,10 +953,60 @@ impl Session {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
document.validate()?;
|
||||
(Arc::new(document), 1, 1, 0)
|
||||
let retained = document.retained_root_p();
|
||||
let (source, node_count) = if retained {
|
||||
let (document, node_count) = RetainedDocument::bootstrap(document)?;
|
||||
(LayoutSource::Retained(document), node_count)
|
||||
} else {
|
||||
document.validate()?;
|
||||
let node_count = document.input_node_count();
|
||||
(LayoutSource::Full(Arc::new(document)), node_count)
|
||||
};
|
||||
(
|
||||
source,
|
||||
DocumentInputStats {
|
||||
parses: 1,
|
||||
validations: 1,
|
||||
full_input_bytes: payload.len() as u64,
|
||||
full_nodes_parsed: node_count,
|
||||
..DocumentInputStats::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
None => {
|
||||
(None, Some(delta)) => {
|
||||
if document_base_revision.checked_add(1) != Some(document_target_revision) {
|
||||
return Err(
|
||||
"Native retained document delta must advance one revision".to_owned()
|
||||
);
|
||||
}
|
||||
let baseline = confirmed.as_ref().ok_or_else(|| {
|
||||
"Native retained document delta requires a confirmed baseline".to_owned()
|
||||
})?;
|
||||
if baseline.document_revision != document_base_revision {
|
||||
return Err(
|
||||
"Native retained document delta base revision does not match confirmed state"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
let LayoutSource::Retained(document) = &baseline.document else {
|
||||
return Err(
|
||||
"Native retained document delta requires an identified bootstrap"
|
||||
.to_owned(),
|
||||
);
|
||||
};
|
||||
let (document, parsed, copied) = document.apply_delta(delta)?;
|
||||
(
|
||||
LayoutSource::Retained(document),
|
||||
DocumentInputStats {
|
||||
delta_input_bytes: payload.len() as u64,
|
||||
delta_entries_parsed: parsed,
|
||||
delta_entries_validated: parsed,
|
||||
trie_path_nodes_copied: copied,
|
||||
..DocumentInputStats::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
(None, None) => {
|
||||
if document_base_revision != document_target_revision {
|
||||
return Err(
|
||||
"Native retained document reuse requires equal base and target revisions"
|
||||
@ -902,7 +1029,13 @@ impl Session {
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
(Arc::clone(&baseline.document), 0, 0, 1)
|
||||
(
|
||||
baseline.document.clone(),
|
||||
DocumentInputStats {
|
||||
reuses: 1,
|
||||
..DocumentInputStats::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
let frame = batch
|
||||
@ -943,9 +1076,16 @@ impl Session {
|
||||
}
|
||||
state.base_renders += output.base_renders;
|
||||
state.target_renders += output.target_renders;
|
||||
state.document_parses += document_parses;
|
||||
state.document_validations += document_validations;
|
||||
state.document_reuses += document_reuses;
|
||||
state.document_resolver_lookups += output.resolver_lookups;
|
||||
state.document_parses += input_stats.parses;
|
||||
state.document_validations += input_stats.validations;
|
||||
state.document_reuses += input_stats.reuses;
|
||||
state.document_full_input_bytes += input_stats.full_input_bytes;
|
||||
state.document_delta_input_bytes += input_stats.delta_input_bytes;
|
||||
state.document_full_nodes_parsed += input_stats.full_nodes_parsed;
|
||||
state.document_delta_entries_parsed += input_stats.delta_entries_parsed;
|
||||
state.document_delta_entries_validated += input_stats.delta_entries_validated;
|
||||
state.document_trie_path_nodes_copied += input_stats.trie_path_nodes_copied;
|
||||
state.pending_baselines.clear();
|
||||
if let Some(pending) = output.pending {
|
||||
state
|
||||
@ -1078,6 +1218,13 @@ impl Session {
|
||||
document_parses: state.document_parses,
|
||||
document_validations: state.document_validations,
|
||||
document_reuses: state.document_reuses,
|
||||
document_full_input_bytes: state.document_full_input_bytes,
|
||||
document_delta_input_bytes: state.document_delta_input_bytes,
|
||||
document_full_nodes_parsed: state.document_full_nodes_parsed,
|
||||
document_delta_entries_parsed: state.document_delta_entries_parsed,
|
||||
document_delta_entries_validated: state.document_delta_entries_validated,
|
||||
document_trie_path_nodes_copied: state.document_trie_path_nodes_copied,
|
||||
document_resolver_lookups: state.document_resolver_lookups,
|
||||
pending_baselines,
|
||||
confirmed_baseline: state.confirmed_baseline.is_some(),
|
||||
confirmed_baseline_bytes,
|
||||
@ -1129,7 +1276,7 @@ fn parse_control_batch(payload: &[u8]) -> Result<ControlBatch, String> {
|
||||
}
|
||||
|
||||
fn checked_layout_context(
|
||||
document: &LayoutDocument,
|
||||
document: &LayoutSource,
|
||||
frame_key: i64,
|
||||
viewport_width: i64,
|
||||
viewport_width_known: bool,
|
||||
@ -1179,11 +1326,12 @@ fn confirmed_baseline_matches(
|
||||
}
|
||||
|
||||
fn prepare_layout_job(
|
||||
document: &Arc<LayoutDocument>,
|
||||
document: &LayoutSource,
|
||||
frame: ControlFrame,
|
||||
document_base_revision: u64,
|
||||
document_target_revision: u64,
|
||||
) -> Result<PreparedJob, String> {
|
||||
layout::reset_resolver_lookups();
|
||||
if frame.payload.is_some() {
|
||||
return Err(format!(
|
||||
"Native layout frame {} cannot contain an echo payload",
|
||||
@ -1197,7 +1345,7 @@ fn prepare_layout_job(
|
||||
.viewport_height
|
||||
.ok_or_else(|| format!("Native layout frame {} requires viewport-height", frame.key))?;
|
||||
let context = checked_layout_context(
|
||||
document.as_ref(),
|
||||
document,
|
||||
frame.key,
|
||||
viewport_width,
|
||||
frame.viewport_width_known,
|
||||
@ -1223,7 +1371,7 @@ fn prepare_layout_job(
|
||||
)
|
||||
})?;
|
||||
let base_context = checked_layout_context(
|
||||
document.as_ref(),
|
||||
document,
|
||||
frame.key,
|
||||
base_viewport_width,
|
||||
frame.base_viewport_width_known,
|
||||
@ -1254,7 +1402,7 @@ fn prepare_layout_job(
|
||||
key: frame.key,
|
||||
delay_ms: frame.delay_ms,
|
||||
payload: JobPayload::Layout {
|
||||
document: Arc::clone(document),
|
||||
document: document.clone(),
|
||||
context,
|
||||
root_width,
|
||||
root_width_override: frame.root_width_override,
|
||||
@ -1267,6 +1415,7 @@ fn prepare_layout_job(
|
||||
root_metadata: frame.root_metadata,
|
||||
document_base_revision,
|
||||
document_target_revision,
|
||||
validation_resolver_lookups: layout::resolver_lookups(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -1287,6 +1436,7 @@ fn render_layout_payload(
|
||||
baseline_hit: false,
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
resolver_lookups: 0,
|
||||
},
|
||||
JobPayload::Layout {
|
||||
document,
|
||||
@ -1302,6 +1452,7 @@ fn render_layout_payload(
|
||||
root_metadata,
|
||||
document_base_revision,
|
||||
document_target_revision,
|
||||
validation_resolver_lookups,
|
||||
} => {
|
||||
let identity = TapeIdentity {
|
||||
session_id,
|
||||
@ -1331,8 +1482,20 @@ fn render_layout_payload(
|
||||
// job with neither result nor error tape, so the Elisp ready
|
||||
// watcher would poll forever. Convert panics into the same
|
||||
// error-tape channel ordinary layout failures use.
|
||||
type LayoutRenderOutcome = Result<(Vec<u8>, LayoutTape, bool, u64, u64), String>;
|
||||
type LayoutRenderOutcome = Result<
|
||||
(
|
||||
Vec<u8>,
|
||||
LayoutTape,
|
||||
Vec<layout::StyleTemplate>,
|
||||
bool,
|
||||
u64,
|
||||
u64,
|
||||
),
|
||||
String,
|
||||
>;
|
||||
layout::reset_resolver_lookups();
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
|
||||
let target_styles = document.styles()?;
|
||||
if let Some(base_context) = base_context {
|
||||
let base_identity = BaselineIdentity {
|
||||
context: base_context,
|
||||
@ -1351,7 +1514,7 @@ fn render_layout_payload(
|
||||
&base_identity,
|
||||
) && layout::style_registry_extends_exact_prefix(
|
||||
&baseline.styles,
|
||||
&document.styles,
|
||||
&target_styles,
|
||||
)
|
||||
})
|
||||
.cloned();
|
||||
@ -1360,12 +1523,12 @@ fn render_layout_payload(
|
||||
.layout_tape(context, root_width_override.then_some(root_width))?;
|
||||
let bytes = layout::encode_layout_tape(
|
||||
target.clone(),
|
||||
&document.styles,
|
||||
&target_styles,
|
||||
identity,
|
||||
output.root_metadata,
|
||||
output.max_bytes,
|
||||
)?;
|
||||
return Ok((bytes, target, false, 0, 1));
|
||||
return Ok((bytes, target, target_styles, false, 0, 1));
|
||||
}
|
||||
let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
|
||||
(baseline.tape.clone(), true, 0)
|
||||
@ -1384,45 +1547,51 @@ fn render_layout_payload(
|
||||
let bytes = layout::encode_layout_patch_tape(
|
||||
old,
|
||||
target.clone(),
|
||||
&document.styles,
|
||||
&target_styles,
|
||||
identity,
|
||||
output.root_metadata,
|
||||
output.max_bytes,
|
||||
)?;
|
||||
Ok((bytes, target, baseline_hit, base_renders, 1))
|
||||
Ok((bytes, target, target_styles, baseline_hit, base_renders, 1))
|
||||
} else {
|
||||
let target =
|
||||
document.layout_tape(context, root_width_override.then_some(root_width))?;
|
||||
let bytes = layout::encode_layout_tape(
|
||||
target.clone(),
|
||||
&document.styles,
|
||||
&target_styles,
|
||||
identity,
|
||||
output.root_metadata,
|
||||
output.max_bytes,
|
||||
)?;
|
||||
Ok((bytes, target, false, 0, 1))
|
||||
Ok((bytes, target, target_styles, false, 0, 1))
|
||||
}
|
||||
}));
|
||||
let resolver_lookups =
|
||||
validation_resolver_lookups.saturating_add(layout::resolver_lookups());
|
||||
match result {
|
||||
Ok(Ok((bytes, tape, baseline_hit, base_renders, target_renders))) => RenderedJob {
|
||||
bytes,
|
||||
pending: Some(PendingBaseline {
|
||||
confirmed_identity: pending_identity,
|
||||
document: Arc::clone(&document),
|
||||
document_revision: document_target_revision,
|
||||
tape,
|
||||
styles: document.styles.clone(),
|
||||
}),
|
||||
baseline_hit,
|
||||
base_renders,
|
||||
target_renders,
|
||||
},
|
||||
Ok(Ok((bytes, tape, styles, baseline_hit, base_renders, target_renders))) => {
|
||||
RenderedJob {
|
||||
bytes,
|
||||
pending: Some(PendingBaseline {
|
||||
confirmed_identity: pending_identity,
|
||||
document: document.clone(),
|
||||
document_revision: document_target_revision,
|
||||
tape,
|
||||
styles,
|
||||
}),
|
||||
baseline_hit,
|
||||
base_renders,
|
||||
target_renders,
|
||||
resolver_lookups,
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => RenderedJob {
|
||||
bytes: encode_error_tape(identity, &error, max_result_bytes),
|
||||
pending: None,
|
||||
baseline_hit: false,
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
resolver_lookups,
|
||||
},
|
||||
Err(_) => RenderedJob {
|
||||
bytes: encode_error_tape(identity, "native layout panicked", max_result_bytes),
|
||||
@ -1430,6 +1599,7 @@ fn render_layout_payload(
|
||||
baseline_hit: false,
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
resolver_lookups,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1464,7 +1634,8 @@ fn render_proof(payload: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if frame.delay_ms != 0 {
|
||||
return Err("Native proof render cannot contain delay-ms".to_owned());
|
||||
}
|
||||
let prepared = prepare_layout_job(&Arc::new(document), frame, 0, 1)?;
|
||||
let document = LayoutSource::Full(Arc::new(document));
|
||||
let prepared = prepare_layout_job(&document, frame, 0, 1)?;
|
||||
let output = render_layout_payload(
|
||||
prepared.payload,
|
||||
SYNC_RENDER_SESSION_ID,
|
||||
@ -1577,6 +1748,7 @@ fn worker_loop(shared: Arc<Shared>) {
|
||||
}
|
||||
state.base_renders += output.base_renders;
|
||||
state.target_renders += output.target_renders;
|
||||
state.document_resolver_lookups += output.resolver_lookups;
|
||||
state.results.insert(
|
||||
(job.generation, job.key),
|
||||
ResultEntry {
|
||||
@ -1994,6 +2166,14 @@ mod tests {
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
fn identified_proof_layout_payload(frames: &str) -> Vec<u8> {
|
||||
let mut payload: serde_json::Value =
|
||||
serde_json::from_slice(&proof_layout_payload(frames)).unwrap();
|
||||
payload["document"]["root"]["node-id"] = serde_json::json!(1);
|
||||
payload["document"]["root"]["node-revision"] = serde_json::json!(7);
|
||||
serde_json::to_vec(&payload).unwrap()
|
||||
}
|
||||
|
||||
fn replacement_layout_payload(
|
||||
document_base_revision: u64,
|
||||
document_target_revision: u64,
|
||||
@ -2336,6 +2516,79 @@ mod tests {
|
||||
grandchild.stop(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonzero_document_delta_renders_and_promotes_without_full_reparse() {
|
||||
let session = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||
let first = identified_proof_layout_payload(
|
||||
r#"[{"key":1,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":0,"context-hash":77}]"#,
|
||||
);
|
||||
let first_tape = session.render_sync(1, &first).unwrap();
|
||||
assert!(!tape_patch_p(&first_tape));
|
||||
assert!(session.confirm(1, 1, 1).unwrap());
|
||||
|
||||
let delta = serde_json::to_vec(&serde_json::json!({
|
||||
"version": 1,
|
||||
"document-base-revision": 1,
|
||||
"document-target-revision": 2,
|
||||
"document-delta": {
|
||||
"style-base-count": 0,
|
||||
"styles-append": [],
|
||||
"property-template-base-count": 0,
|
||||
"property-template-target-count": 0,
|
||||
"entries": [{
|
||||
"node-id": 1,
|
||||
"expected-revision": 7,
|
||||
"target-revision": 21,
|
||||
"slot-patches": [{
|
||||
"slot": 0,
|
||||
"local": {
|
||||
"content": {"lines": [{"clusters": [{
|
||||
"text": "y", "width": 8, "cjk": false, "space": false
|
||||
}]}]}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
},
|
||||
"frames": [{
|
||||
"key": 2,
|
||||
"viewport-width": 80,
|
||||
"viewport-height": 10,
|
||||
"root-width": 80,
|
||||
"patch": true,
|
||||
"base-viewport-width": 80,
|
||||
"base-viewport-height": 10,
|
||||
"base-root-width": 80,
|
||||
"runtime-revision": 1,
|
||||
"context-hash": 77
|
||||
}]
|
||||
}))
|
||||
.unwrap();
|
||||
let delta_tape = session.render_sync(2, &delta).unwrap();
|
||||
assert!(tape_patch_p(&delta_tape));
|
||||
let stats = session.stats();
|
||||
assert_eq!(stats.document_parses, 1);
|
||||
assert_eq!(stats.document_validations, 1);
|
||||
assert_eq!(stats.document_full_nodes_parsed, 1);
|
||||
assert_eq!(stats.document_delta_entries_parsed, 1);
|
||||
assert_eq!(stats.document_delta_entries_validated, 1);
|
||||
assert_eq!(stats.document_trie_path_nodes_copied, 17);
|
||||
assert_eq!(stats.document_delta_input_bytes, delta.len() as u64);
|
||||
assert!(
|
||||
stats.document_resolver_lookups >= 3,
|
||||
"target/base validation and target render lookups must all remain visible"
|
||||
);
|
||||
assert!(session.confirm(2, 2, 2).unwrap());
|
||||
|
||||
let child = session.fork_confirmed().unwrap();
|
||||
let retained = retained_layout_payload(
|
||||
2,
|
||||
r#"[{"key":3,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":2,"context-hash":77}]"#,
|
||||
);
|
||||
assert!(!child.render_sync(1, &retained).unwrap().is_empty());
|
||||
session.stop(true);
|
||||
child.stop(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
|
||||
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||
|
||||
@ -2372,6 +2372,14 @@ remain retained identities."
|
||||
session initial-state root))
|
||||
(next (copy-tree root))
|
||||
(next-child (car (ebox-tree-node-children next))))
|
||||
(let ((bootstrap-root
|
||||
(plist-get (plist-get initial-package :document) :root)))
|
||||
(should (= (plist-get root :node-id)
|
||||
(plist-get bootstrap-root :node-id)))
|
||||
(should (integerp (plist-get bootstrap-root :node-revision)))
|
||||
;; The Text is fused into its owner and therefore has no addressable
|
||||
;; child entry in the full wire document.
|
||||
(should (eq :null (plist-get bootstrap-root :child))))
|
||||
(setf (ebox-native-reflow-session-styles session)
|
||||
(plist-get initial-package :styles)
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
@ -2567,6 +2575,434 @@ remain retained identities."
|
||||
(should (= (plist-get (car controls) :document-base-revision) 10))
|
||||
(should (= (plist-get (car controls) :document-target-revision) 11)))))
|
||||
|
||||
(ert-deftest ebox-native-persistent-index-path-copies-without-changing-base ()
|
||||
"A retained index update shares the base and leaves its values immutable."
|
||||
(require 'ebox-native-reflow)
|
||||
(let* ((base (ebox-native-reflow--persistent-index-put nil 1 'one))
|
||||
(next (ebox-native-reflow--persistent-index-put base 17 'seventeen)))
|
||||
(should (eq 'one (ebox-native-reflow--persistent-index-get base 1)))
|
||||
(should-not (ebox-native-reflow--persistent-index-get base 17))
|
||||
(should (eq 'one (ebox-native-reflow--persistent-index-get next 1)))
|
||||
(should (eq 'seventeen
|
||||
(ebox-native-reflow--persistent-index-get next 17)))
|
||||
(should-not (eq base next))))
|
||||
|
||||
(ert-deftest ebox-native-session-fork-shares-immutable-compiler-roots ()
|
||||
"Forking does not clone the retained fragment map or persistent indexes."
|
||||
(require 'ebox-native-reflow)
|
||||
(let* ((cache (make-hash-table :test 'equal))
|
||||
(index (ebox-native-reflow--persistent-index-put nil 1 'entry))
|
||||
(styles (vector 'style))
|
||||
(session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'parent :generation 4 :styles styles
|
||||
:layout-package 'package :layout-fragment-cache cache
|
||||
:layout-fragment-index index :layout-fragment-revision 8
|
||||
:layout-style-index index :layout-property-template-index index)))
|
||||
(cl-letf (((symbol-function 'ebox-native--module-fork-confirmed)
|
||||
(lambda (_handle) 'child)))
|
||||
(let ((fork (ebox-native-reflow-fork-session session)))
|
||||
(should (eq cache
|
||||
(ebox-native-reflow-session-layout-fragment-cache fork)))
|
||||
(should (eq index
|
||||
(ebox-native-reflow-session-layout-fragment-index fork)))
|
||||
(should (eq styles (ebox-native-reflow-session-styles fork)))
|
||||
(should (= 8
|
||||
(ebox-native-reflow-session-layout-fragment-revision
|
||||
fork)))))))
|
||||
|
||||
(ert-deftest ebox-native-failed-delta-keeps-fork-and-parent-indexes-unchanged ()
|
||||
"A rejected candidate cannot publish its path-copied compiler index."
|
||||
(require 'ebox-native-reflow)
|
||||
(let* ((cache (make-hash-table :test 'equal))
|
||||
(base-index (ebox-native-reflow--persistent-index-put nil 1 'old))
|
||||
(next-index
|
||||
(ebox-native-reflow--persistent-index-put base-index 1 'new))
|
||||
(old (list :document '(:version 2) :document-revision 4
|
||||
:styles [] :property-templates []))
|
||||
(next (copy-sequence old))
|
||||
(parent
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'parent :layout-package old :styles []
|
||||
:layout-fragment-cache cache :layout-fragment-index base-index)))
|
||||
(plist-put next :document-revision 5)
|
||||
(plist-put next :document-delta
|
||||
'(:style-base-count 0 :styles-append []
|
||||
:property-template-base-count 0
|
||||
:property-template-target-count 0 :entries []))
|
||||
(plist-put next :native-fragment-index next-index)
|
||||
(cl-letf (((symbol-function 'ebox-native--module-fork-confirmed)
|
||||
(lambda (_handle) 'child)))
|
||||
(let ((fork (ebox-native-reflow-fork-session parent)))
|
||||
(cl-letf (((symbol-function
|
||||
'ebox-native-reflow--compile-retained-layout-package)
|
||||
(lambda (&rest _) next))
|
||||
((symbol-function 'ebox-native--module-render-session-frame)
|
||||
(lambda (&rest _) (error "reject delta"))))
|
||||
(should-error
|
||||
(ebox-native-reflow-execute-session-sync
|
||||
fork 'node '(:key 1 :viewport-width 80 :viewport-height 10)
|
||||
nil 'state))
|
||||
(should (eq base-index
|
||||
(ebox-native-reflow-session-layout-fragment-index fork)))
|
||||
(should (eq base-index
|
||||
(ebox-native-reflow-session-layout-fragment-index parent)))
|
||||
(should (eq cache
|
||||
(ebox-native-reflow-session-layout-fragment-cache fork))))))))
|
||||
|
||||
(ert-deftest ebox-native-accepted-delta-invalidates-stale-full-cache ()
|
||||
"A later full fallback cannot resurrect pre-delta legacy fragments."
|
||||
(require 'ebox-native-reflow)
|
||||
(let* ((cache (make-hash-table :test 'equal))
|
||||
(base-index (ebox-native-reflow--persistent-index-put nil 1 'old))
|
||||
(next-index
|
||||
(ebox-native-reflow--persistent-index-put base-index 1 'new))
|
||||
(old (list :document '(:version 2) :document-revision 4
|
||||
:styles [] :property-templates []))
|
||||
(next (copy-sequence old))
|
||||
(session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'test :generation 0 :layout-package old :styles []
|
||||
:layout-fragment-cache cache :layout-fragment-index base-index)))
|
||||
(puthash 1 'stale-fragment cache)
|
||||
(plist-put next :document-revision 5)
|
||||
(plist-put next :document-delta
|
||||
'(:style-base-count 0 :styles-append []
|
||||
:property-template-base-count 0
|
||||
:property-template-target-count 0 :entries []))
|
||||
(plist-put next :native-fragment-index next-index)
|
||||
(plist-put next :native-fragment-revision 9)
|
||||
(cl-letf (((symbol-function
|
||||
'ebox-native-reflow--compile-retained-layout-package)
|
||||
(lambda (&rest _) next))
|
||||
((symbol-function 'ebox-native--module-render-session-frame)
|
||||
(lambda (&rest _) 'native-frame))
|
||||
((symbol-function 'ebox-native-reflow--materialize-module-frame)
|
||||
(lambda (&rest _) '(:rendered "ok"))))
|
||||
(ebox-native-reflow-execute-session-sync
|
||||
session 'node '(:key 1 :viewport-width 80 :viewport-height 10)
|
||||
nil 'state)
|
||||
(should-not
|
||||
(ebox-native-reflow-session-layout-fragment-cache session))
|
||||
(should (eq next-index
|
||||
(ebox-native-reflow-session-layout-fragment-index session)))
|
||||
(should (= 9
|
||||
(ebox-native-reflow-session-layout-fragment-revision
|
||||
session)))
|
||||
;; Model the next unsupported B update. The current source contains A's
|
||||
;; accepted value while the persistent index is deliberately stale; the
|
||||
;; invalidated legacy cache forces a complete compile from current state.
|
||||
(let* ((input
|
||||
(ebox-test-box (ebox-test-text "A") :bgcolor "#00ff00"))
|
||||
(root (ebox-test-root input))
|
||||
(_ids (ebox--runtime-node-ids root))
|
||||
(full
|
||||
(ebox-native-reflow--compile-retained-layout-package-full
|
||||
session
|
||||
(list :native-node-postorder
|
||||
(ebox-native-reflow--retained-layout-postorder root)
|
||||
:native-topology-stable-p nil
|
||||
:source-index (ebox-test-source-index input))
|
||||
root))
|
||||
(document-root (plist-get (plist-get full :document) :root))
|
||||
(style-id (plist-get document-root :background-style)))
|
||||
(should
|
||||
(equal '(:background "#00ff00")
|
||||
(plist-get (aref (plist-get full :styles) style-id)
|
||||
:face)))))))
|
||||
|
||||
(ert-deftest ebox-native-node-delta-is-local-and-bumps-ancestors ()
|
||||
"A local change patches one owner and only revises its retained ancestor."
|
||||
(require 'ebox-native-reflow)
|
||||
(let* ((leaf-fragment
|
||||
'(:type "box" :background-style :null :child :null
|
||||
:node-id 2 :node-revision 3))
|
||||
(root-fragment
|
||||
(list :type "box" :background-style :null :child leaf-fragment
|
||||
:node-id 1 :node-revision 4))
|
||||
(leaf-entry (list :fragment leaf-fragment :revision 3))
|
||||
(root-entry (list :fragment root-fragment :revision 4))
|
||||
(index (ebox-native-reflow--persistent-index-put nil 1 root-entry))
|
||||
(_index (setq index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
index 2 leaf-entry)))
|
||||
(style-index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
nil '(:mode add :face (:background "red")) 0))
|
||||
(package
|
||||
(list :document '(:version 2) :document-revision 7
|
||||
:styles [] :property-templates []))
|
||||
(session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'test :generation 0 :styles [] :layout-package package
|
||||
:layout-fragment-index index :layout-fragment-revision 4
|
||||
:layout-style-index style-index))
|
||||
(nodes (make-hash-table :test 'equal))
|
||||
(parents (make-hash-table :test 'equal))
|
||||
(state
|
||||
(list :node-table nodes :parent-table parents
|
||||
:native-topology-stable-p t
|
||||
:native-touched-node-ids '(2 1)
|
||||
:native-local-dirty-entries
|
||||
'((:node-id 2 :dirty-kind paint
|
||||
:changed-keys (:background-color))))))
|
||||
(puthash 1 '(:node-id 1) nodes)
|
||||
(puthash 2 '(:node-id 2) nodes)
|
||||
(puthash 2 1 parents)
|
||||
(cl-letf (((symbol-function 'ebox--current-display-signature)
|
||||
(lambda () 'display))
|
||||
((symbol-function 'ebox-native-reflow--compile-delta-slots)
|
||||
(lambda (_node _old)
|
||||
(vector
|
||||
'(:type "box" :background-style 0 :child :null)))))
|
||||
(let* ((next
|
||||
(ebox-native-reflow--compile-retained-layout-delta
|
||||
session state 'root))
|
||||
(delta (plist-get next :document-delta))
|
||||
(entries (plist-get delta :entries))
|
||||
(leaf (aref entries 0))
|
||||
(root (aref entries 1)))
|
||||
(should (= 8 (plist-get next :document-revision)))
|
||||
(should (= 0 (plist-get delta :style-base-count)))
|
||||
(should (= 2 (length entries)))
|
||||
(should (equal [(:slot 0 :local (:background-style 0))]
|
||||
(plist-get leaf :slot-patches)))
|
||||
(should-not (plist-member root :slot-patches))
|
||||
(should (= 5 (plist-get leaf :target-revision)))
|
||||
(should (= 6 (plist-get root :target-revision)))))))
|
||||
|
||||
(ert-deftest ebox-native-node-delta-deduplicates-multiple-leaf-closures ()
|
||||
"Multiple changed leaves produce one entry each and one shared ancestor."
|
||||
(require 'ebox-native-reflow)
|
||||
(let ((index nil)
|
||||
(nodes (make-hash-table :test 'equal))
|
||||
(parents (make-hash-table :test 'equal)))
|
||||
(dolist (pair '((1 . 3) (2 . 1) (3 . 2)))
|
||||
(setq index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
index (car pair)
|
||||
(list :fragment
|
||||
'(:type "box" :background-style :null :child :null)
|
||||
:revision (cdr pair))))
|
||||
(puthash (car pair) (list :node-id (car pair)) nodes))
|
||||
(puthash 2 1 parents)
|
||||
(puthash 3 1 parents)
|
||||
(let* ((package (list :document '(:version 2) :document-revision 2
|
||||
:styles [] :property-templates []))
|
||||
(session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'test :layout-package package
|
||||
:layout-fragment-index index :layout-fragment-revision 3))
|
||||
(state
|
||||
(list :node-table nodes :parent-table parents
|
||||
:native-topology-stable-p t
|
||||
:native-touched-node-ids '(2 1 3 1)
|
||||
:native-local-dirty-entries
|
||||
'((:node-id 2 :changed-keys (:background-color))
|
||||
(:node-id 3 :changed-keys (:background-color))))))
|
||||
(cl-letf (((symbol-function 'ebox--current-display-signature)
|
||||
(lambda () 'display))
|
||||
((symbol-function 'ebox-native-reflow--compile-delta-slots)
|
||||
(lambda (node _old)
|
||||
(vector
|
||||
(list :type "box" :background-style
|
||||
(plist-get node :node-id) :child :null)))))
|
||||
(let* ((next
|
||||
(ebox-native-reflow--compile-retained-layout-delta
|
||||
session state 'root))
|
||||
(entries
|
||||
(plist-get (plist-get next :document-delta) :entries)))
|
||||
(should (= 3 (length entries)))
|
||||
(should (equal '(2 1 3)
|
||||
(mapcar (lambda (entry)
|
||||
(plist-get entry :node-id))
|
||||
(append entries nil))))
|
||||
(should (plist-member (aref entries 0) :slot-patches))
|
||||
(should-not (plist-member (aref entries 1) :slot-patches))
|
||||
(should (plist-member (aref entries 2) :slot-patches)))))))
|
||||
|
||||
(ert-deftest ebox-native-fused-text-delta-resolves-to-box-owner ()
|
||||
"A fused text id addresses its containing Box rather than a hidden node."
|
||||
(require 'ebox-native-reflow)
|
||||
(let ((nodes (make-hash-table :test 'equal))
|
||||
(parents (make-hash-table :test 'equal))
|
||||
(index (ebox-native-reflow--persistent-index-put nil 10 'owner)))
|
||||
(puthash 10
|
||||
(list :node-id 10 :ebox-kind 'box
|
||||
:ebox-layout-config
|
||||
(ebox-normal-layout-create))
|
||||
nodes)
|
||||
(puthash 11 '(:node-id 11 :ebox-kind text) nodes)
|
||||
(puthash 11 10 parents)
|
||||
(should (= 10
|
||||
(ebox-native-reflow--delta-owner-id
|
||||
(list :node-table nodes :parent-table parents) 11 index)))))
|
||||
|
||||
(ert-deftest ebox-native-flex-item-metadata-change-uses-full-input ()
|
||||
"N1 does not misrepresent Flex item metadata as a local scalar patch."
|
||||
(require 'ebox-native-reflow)
|
||||
(let ((session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'test :layout-package 'old
|
||||
:layout-fragment-index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
nil 1 '(:fragment (:type "box" :child :null) :revision 1))))
|
||||
(state
|
||||
'(:native-topology-stable-p t :native-touched-node-ids (1)
|
||||
:native-local-dirty-entries
|
||||
((:node-id 1 :dirty-kind geometry
|
||||
:changed-keys (:flex-grow)))))
|
||||
full-called)
|
||||
(cl-letf (((symbol-function
|
||||
'ebox-native-reflow--compile-retained-layout-package-full)
|
||||
(lambda (&rest _)
|
||||
(setq full-called t)
|
||||
'full))
|
||||
((symbol-function 'ebox-native-reflow--compile-delta-slots)
|
||||
(lambda (&rest _)
|
||||
(ert-fail "Flex item metadata reached local delta"))))
|
||||
(should (eq 'full
|
||||
(ebox-native-reflow--compile-retained-layout-package
|
||||
session state 'node)))
|
||||
(should full-called))))
|
||||
|
||||
(ert-deftest ebox-native-flex-child-content-change-uses-full-input ()
|
||||
"Child content may alter retained Flex edge measurement in N1."
|
||||
(require 'ebox-native-reflow)
|
||||
(let ((nodes (make-hash-table :test 'equal))
|
||||
(parents (make-hash-table :test 'equal))
|
||||
(index
|
||||
(ebox-native-reflow--persistent-index-put
|
||||
nil 2 '(:fragment (:type "box" :content [] :child :null)
|
||||
:revision 3)))
|
||||
full-called)
|
||||
(puthash 1 '(:node-id 1 :ebox-type flex) nodes)
|
||||
(puthash 2
|
||||
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
|
||||
:ebox-layout-config (ebox-normal-layout-create))
|
||||
nodes)
|
||||
(puthash 3 '(:node-id 3 :ebox-type box :ebox-kind text) nodes)
|
||||
(puthash 2 1 parents)
|
||||
(puthash 3 2 parents)
|
||||
(let ((session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'test :layout-package 'old
|
||||
:layout-fragment-index index))
|
||||
(state
|
||||
(list :node-table nodes :parent-table parents
|
||||
:native-topology-stable-p t
|
||||
:native-touched-node-ids '(3 2 1)
|
||||
:native-local-dirty-entries
|
||||
'((:node-id 3 :dirty-kind content
|
||||
:changed-keys (:content))))))
|
||||
(cl-letf (((symbol-function
|
||||
'ebox-native-reflow--compile-retained-layout-package-full)
|
||||
(lambda (&rest _)
|
||||
(setq full-called t)
|
||||
'full))
|
||||
((symbol-function 'ebox-native-reflow--compile-delta-slots)
|
||||
(lambda (&rest _)
|
||||
(ert-fail "Flex child content reached local delta"))))
|
||||
(should (eq 'full
|
||||
(ebox-native-reflow--compile-retained-layout-package
|
||||
session state 'node)))
|
||||
(should full-called)))))
|
||||
|
||||
(ert-deftest ebox-native-default-axis-child-change-keeps-local-input ()
|
||||
"Default Row/Column edges carry no derived Flex item metadata."
|
||||
(require 'ebox-native-reflow)
|
||||
(let ((nodes (make-hash-table :test 'equal))
|
||||
(parents (make-hash-table :test 'equal)))
|
||||
(puthash 1
|
||||
(list :node-id 1 :ebox-type 'box :ebox-kind 'box
|
||||
:ebox-layout-config (ebox-column-layout-create))
|
||||
nodes)
|
||||
(puthash 2
|
||||
(list :node-id 2 :ebox-type 'box :ebox-kind 'box
|
||||
:ebox-layout-config (ebox-normal-layout-create))
|
||||
nodes)
|
||||
(puthash 3 '(:node-id 3 :ebox-type box :ebox-kind text) nodes)
|
||||
(puthash 2 1 parents)
|
||||
(puthash 3 2 parents)
|
||||
(should-not
|
||||
(ebox-native-reflow--delta-flex-edge-change-p
|
||||
(list :node-table nodes :parent-table parents
|
||||
:native-local-dirty-entries
|
||||
'((:node-id 3 :dirty-kind geometry
|
||||
:changed-keys (:content))))))))
|
||||
|
||||
(ert-deftest ebox-native-surface-overrides-carry-local-dirty-entries ()
|
||||
"The incremental producer preserves exact local dirtiness to native input."
|
||||
(let* ((dirty '((:node-id 7 :dirty-kind paint
|
||||
:changed-keys (:background-color))))
|
||||
(candidate (list :runtime-revision 3
|
||||
:native-topology-stable-p t
|
||||
:native-touched-node-ids '(7 1)
|
||||
:native-local-dirty-entries dirty))
|
||||
(overrides
|
||||
(ebox-incremental--surface-state-overrides
|
||||
nil '(:display-signature display) candidate 'native-frame)))
|
||||
(should (equal dirty
|
||||
(plist-get overrides :native-local-dirty-entries)))))
|
||||
|
||||
(ert-deftest ebox-native-wide-owner-local-compile-does-not-enumerate-children ()
|
||||
"A scalar slot update never walks a stable owner's unchanged children."
|
||||
(require 'ebox-native-reflow)
|
||||
(let ((node (list :ebox-type 'box :ebox-kind 'box
|
||||
:ebox-layout-config (ebox-flex-layout-create)))
|
||||
(old '(:type "box" :child (:type "flex" :items [a b c]))))
|
||||
(cl-letf (((symbol-function 'ebox-tree-node-children)
|
||||
(lambda (&rest _)
|
||||
(ert-fail "stable delta enumerated unchanged children")))
|
||||
((symbol-function 'ebox-native-reflow--compile-flex-inner)
|
||||
(lambda (_props items &rest _)
|
||||
(should-not items)
|
||||
'(:type "flex" :items [])))
|
||||
((symbol-function 'ebox-native-reflow--compile-box)
|
||||
(lambda (_box child &rest _)
|
||||
(list :type "box" :child child))))
|
||||
(let ((slots (ebox-native-reflow--compile-delta-slots node old)))
|
||||
(should (= 2 (length slots)))
|
||||
(should (equal [] (plist-get (aref slots 1) :items)))))))
|
||||
|
||||
(ert-deftest ebox-native-retained-sync-sends-document-delta-without-document ()
|
||||
"A supported retained compile sends only its node delta and revisions."
|
||||
(require 'ebox-native-reflow)
|
||||
(let* ((old (list :document '(:version 2) :document-revision 4
|
||||
:styles [] :property-templates []))
|
||||
(delta '(:style-base-count 0 :styles-append []
|
||||
:property-template-base-count 0
|
||||
:property-template-target-count 0 :entries []))
|
||||
(next (copy-sequence old))
|
||||
(session
|
||||
(ebox-native-reflow--make-session
|
||||
:handle 'test :generation 0 :styles [] :layout-package old))
|
||||
control)
|
||||
(plist-put next :document-revision 5)
|
||||
(plist-put next :document-delta delta)
|
||||
(cl-letf (((symbol-function
|
||||
'ebox-native-reflow--compile-retained-layout-package)
|
||||
(lambda (&rest _) next))
|
||||
((symbol-function 'ebox-native--module-render-session-frame)
|
||||
(lambda (_handle _generation payload)
|
||||
(setq control
|
||||
(json-parse-string payload :object-type 'plist
|
||||
:array-type 'array))
|
||||
'native-frame))
|
||||
((symbol-function 'ebox-native-reflow--materialize-module-frame)
|
||||
(lambda (&rest _) '(:rendered "ok"))))
|
||||
(ebox-native-reflow-execute-session-sync
|
||||
session 'node
|
||||
'(:key 1 :viewport-width 80 :viewport-height 10) nil 'state)
|
||||
(should-not (plist-member control :document))
|
||||
(should (equal delta (plist-get control :document-delta)))
|
||||
(should (= 4 (plist-get control :document-base-revision)))
|
||||
(should (= 5 (plist-get control :document-target-revision)))
|
||||
(should-not
|
||||
(plist-member
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
:document-delta)))))
|
||||
|
||||
(provide 'ebox-commit-tests)
|
||||
|
||||
;;; ebox-commit-tests.el ends here
|
||||
|
||||
@ -411,6 +411,55 @@
|
||||
:authority ebox :lifetime candidate-until-final-accept
|
||||
:rollback private-session-discard :rebuild native-frame-stage
|
||||
:cleanup confirm-or-rollback)
|
||||
(:id candidate/native-local-dirty-entries
|
||||
:storage (:state-key :native-local-dirty-entries)
|
||||
:proposed-category generation-fact :owner ebox-native-commit
|
||||
:authority ebox :lifetime candidate-until-native-input
|
||||
:rollback candidate-discard :rebuild prepared-dirty-set
|
||||
:cleanup native-frame-consume-or-downgrade)
|
||||
(:id native-session/layout-fragment-index
|
||||
:storage (:struct-slot ebox-native-reflow-session layout-fragment-index)
|
||||
:proposed-category generation-bound-mutable :owner ebox-native-reflow
|
||||
:authority ebox :lifetime private-native-session
|
||||
:rollback private-session-discard :rebuild full-bootstrap-or-path-copy
|
||||
:cleanup native-session-release)
|
||||
(:id native-session/layout-style-index
|
||||
:storage (:struct-slot ebox-native-reflow-session layout-style-index)
|
||||
:proposed-category generation-bound-mutable :owner ebox-native-reflow
|
||||
:authority ebox :lifetime private-native-session
|
||||
:rollback private-session-discard :rebuild full-bootstrap-or-style-append
|
||||
:cleanup native-session-release)
|
||||
(:id native-session/layout-property-template-index
|
||||
:storage
|
||||
(:struct-slot ebox-native-reflow-session layout-property-template-index)
|
||||
:proposed-category generation-bound-mutable :owner ebox-native-reflow
|
||||
:authority ebox :lifetime private-native-session
|
||||
:rollback private-session-discard :rebuild full-bootstrap
|
||||
:cleanup native-session-release)
|
||||
(:id native-compile/style-index
|
||||
:storage (:global ebox-native-reflow--compile-style-index)
|
||||
:proposed-category generation-fact :owner ebox-native-reflow
|
||||
:authority ebox :lifetime dynamic-native-compile
|
||||
:rollback dynamic-binding-unwind :rebuild retained-session-root
|
||||
:cleanup dynamic-binding-unwind)
|
||||
(:id native-compile/property-template-index
|
||||
:storage (:global ebox-native-reflow--compile-property-template-index)
|
||||
:proposed-category generation-fact :owner ebox-native-reflow
|
||||
:authority ebox :lifetime dynamic-native-compile
|
||||
:rollback dynamic-binding-unwind :rebuild retained-session-root
|
||||
:cleanup dynamic-binding-unwind)
|
||||
(:id native-package/fragment-index
|
||||
:storage (:state-key :native-fragment-index)
|
||||
:proposed-category generation-fact :owner ebox-native-reflow
|
||||
:authority ebox :lifetime native-call-until-success
|
||||
:rollback package-discard :rebuild path-copied-session-root
|
||||
:cleanup strip-from-confirmed-package)
|
||||
(:id native-package/style-index
|
||||
:storage (:state-key :native-style-index)
|
||||
:proposed-category generation-fact :owner ebox-native-reflow
|
||||
:authority ebox :lifetime native-call-until-success
|
||||
:rollback package-discard :rebuild style-append-path-copy
|
||||
:cleanup strip-from-confirmed-package)
|
||||
(:id process/scroll-global-state
|
||||
:storage (:global ebox--scroll-global-state)
|
||||
:proposed-category generation-bound-mutable :owner ebox-incremental
|
||||
|
||||
@ -466,18 +466,20 @@ exclusion freshness, but only retained/container evidence is coverage-gated."
|
||||
'generation-bound-mutable-authority))))
|
||||
|
||||
(ert-deftest ebox-state-contract-excludes-native-compile-scratch-from-authority ()
|
||||
"Compile-local template ids are not mislabeled as native session authority."
|
||||
(should-not
|
||||
(memq 'ebox-native-reflow--compile-property-template-ids
|
||||
"Compile-local indexes are not mislabeled as native session authority."
|
||||
(let ((authority-storage
|
||||
(plist-get (ebox-state-contract-record 'native-runtime-authority)
|
||||
:storage)))
|
||||
(let ((exclusion
|
||||
(cl-find 'ebox-native-reflow--compile-property-template-ids
|
||||
ebox-state-contract-source-scan-exclusions
|
||||
:key (lambda (entry) (plist-get entry :symbol)))))
|
||||
(should exclusion)
|
||||
(should (eq (plist-get exclusion :reason)
|
||||
'dynamically-bound-compile-local-scratch))))
|
||||
(dolist (symbol '(ebox-native-reflow--compile-property-template-ids
|
||||
ebox-native-reflow--compile-property-template-index
|
||||
ebox-native-reflow--compile-style-index))
|
||||
(should-not (memq symbol authority-storage))
|
||||
(let ((exclusion
|
||||
(cl-find symbol ebox-state-contract-source-scan-exclusions
|
||||
:key (lambda (entry) (plist-get entry :symbol)))))
|
||||
(should exclusion)
|
||||
(should (eq (plist-get exclusion :reason)
|
||||
'dynamically-bound-compile-local-scratch))))))
|
||||
|
||||
(ert-deftest ebox-state-contract-tp-custody-is-opaque ()
|
||||
"The inventory separates current whole-state custody from its M2a target."
|
||||
|
||||
@ -1656,17 +1656,25 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(plist-get (tp-surface-client-state surface)
|
||||
:native-sync-session))
|
||||
(should (eq (plist-get report :projection-kind) 'native-frame))
|
||||
;; A style-table change cannot consume the prior confirmed
|
||||
;; document and therefore remains a truthful full frame.
|
||||
(should (eq (plist-get report :native-frame-kind) 'full))
|
||||
;; A root paint delta and appended styles preserve the old
|
||||
;; registry prefix and publish against the confirmed tape.
|
||||
(should (eq (plist-get report :native-frame-kind) 'patch))
|
||||
(should next-session)
|
||||
(should-not (eq next-session winning-session))
|
||||
(should (ebox-native-reflow-session-released-p winning-session))
|
||||
(should-not (ebox-native-reflow-session-released-p
|
||||
next-session))
|
||||
(should
|
||||
(equal-including-properties
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max)))
|
||||
(ebox-surface-test--render-runtime
|
||||
(tp-surface-client-state surface))))
|
||||
(let ((stats (ebox-native-reflow-stats next-session)))
|
||||
(should (= (plist-get stats :document-parses) 1))
|
||||
(should (= (plist-get stats :document-validations) 1))
|
||||
(should (= (plist-get stats :document-parses) 0))
|
||||
(should (= (plist-get stats :document-validations) 0))
|
||||
(should (= (plist-get stats :document-delta-entries-parsed) 1))
|
||||
(should (= (plist-get stats :document-delta-entries-validated) 1))
|
||||
(should (= (plist-get stats :document-reuses) 0))))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer))
|
||||
@ -1725,6 +1733,108 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-native-node-delta-survives-viewport-and-repeated-commits ()
|
||||
"Real local commits preserve earlier edits across viewport document reuse."
|
||||
(skip-unless (ebox-native-reflow-layout-ready-p))
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let ((buffer (generate-new-buffer " *ebox-native-delta-history*"))
|
||||
(ebox-viewport-width 320)
|
||||
(ebox-viewport-height 40)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(unwind-protect
|
||||
(cl-labels
|
||||
((item
|
||||
(index text)
|
||||
(ebox-test-box
|
||||
:key index :source-identity index :width '(8) :height 1
|
||||
(ebox-test-text text :key (+ 100 index)
|
||||
:source-identity (+ 100 index))))
|
||||
(source
|
||||
(count)
|
||||
(apply #'ebox-test-column
|
||||
(append (list :key 'delta-history
|
||||
:source-identity 'delta-history
|
||||
:width '(viewport))
|
||||
(cl-loop for index below count
|
||||
collect (item index "A")))))
|
||||
(ids
|
||||
(state)
|
||||
(let (result)
|
||||
(ebox-surface-test--walk-runtime
|
||||
(plist-get state :root-node)
|
||||
(lambda (node) (push (plist-get node :node-id) result)))
|
||||
(nreverse result))))
|
||||
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||
(lambda () nil)))
|
||||
(ebox-render-to-buffer buffer (source 15)))
|
||||
(ebox-commit buffer (source 16))
|
||||
(let* ((surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(baseline-ids (ids (tp-surface-client-state surface)))
|
||||
(original-send
|
||||
(symbol-function 'ebox-native--module-render-session-frame))
|
||||
(expected-texts (make-vector 16 "A"))
|
||||
previous-document-revision events)
|
||||
(should (plist-get (tp-surface-client-state surface)
|
||||
:native-sync-confirmed-p))
|
||||
(cl-letf
|
||||
(((symbol-function 'ebox-native--module-render-session-frame)
|
||||
(lambda (handle generation control)
|
||||
(push (json-parse-string control :object-type 'plist
|
||||
:array-type 'array)
|
||||
events)
|
||||
(funcall original-send handle generation control))))
|
||||
(dolist (step '((4 "B") viewport (12 "C") (4 "D")))
|
||||
(setq events nil)
|
||||
(if (eq step 'viewport)
|
||||
(progn
|
||||
(setq ebox-viewport-width 360)
|
||||
(ebox-rerender-buffer-with-context buffer 360 40))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate (car step) (item (car step) (cadr step)))
|
||||
(ebox-commit buffer candidate)
|
||||
(aset expected-texts (car step) (cadr step))))
|
||||
(ert-info ((format "Native delta history step %S" step))
|
||||
(should (= 1 (length events))))
|
||||
(let* ((control (car events))
|
||||
(base (plist-get control :document-base-revision))
|
||||
(target (plist-get control :document-target-revision))
|
||||
(state (tp-surface-client-state surface))
|
||||
(session (plist-get state :native-sync-session)))
|
||||
(should-not (plist-get control :document))
|
||||
(when previous-document-revision
|
||||
(should (= base previous-document-revision)))
|
||||
(if (eq step 'viewport)
|
||||
(progn
|
||||
(should-not (plist-get control :document-delta))
|
||||
(should (= base target)))
|
||||
(should (plist-get control :document-delta))
|
||||
(should (= (1+ base) target)))
|
||||
(setq previous-document-revision target)
|
||||
(should (equal baseline-ids (ids state)))
|
||||
(should
|
||||
(equal (mapconcat #'identity expected-texts "")
|
||||
(with-current-buffer buffer
|
||||
(replace-regexp-in-string
|
||||
"[[:space:]]" ""
|
||||
(buffer-substring-no-properties
|
||||
(point-min) (point-max))))))
|
||||
(should (plist-get state :native-sync-confirmed-p))
|
||||
(should-not (plist-get state :native-sync-pending))
|
||||
(should-not
|
||||
(plist-get
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
:document-delta))
|
||||
(should
|
||||
(equal-including-properties
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max)))
|
||||
(ebox-surface-test--render-runtime state))))))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-viewport-reflow-retains-final-sized-flex-child-fragments ()
|
||||
"Viewport reflow should reuse final-sized Flex child fragments exactly."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user