perf: retain validated native document input across commits

This commit is contained in:
Kinneyzhang 2026-09-05 07:31:01 +08:00
parent 32b4834322
commit 9767c30208
8 changed files with 651 additions and 69 deletions

View File

@ -104,6 +104,14 @@ Caller-owned Source Tree
- `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`. - `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`.
- Buffer coordinates belong to the generation that produced them and must be refreshed after mutation. - Buffer coordinates belong to the generation that produced them and must be refreshed after mutation.
- Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness. - Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness.
- A confirmed retained-native frame owns an immutable layout document together
with its exact document revision. Candidate-session forks share that document.
A synchronous frame may omit the document only when its base and target
document revisions are equal and match the confirmed document, and its TP
runtime revision matches the confirmed frame. A changed IR sends a complete
replacement document with the next document revision; confirmation promotes
the candidate document atomically, while failure or cancellation leaves the
parent session unchanged.
- A full declarative update continuing a committed native frame computes - A full declarative update continuing a committed native frame computes
selector-local styles for every candidate node before stable native selector-local styles for every candidate node before stable native
eligibility. The `:styles-prepared-p` certificate prevents replacement eligibility. The `:styles-prepared-p` certificate prevents replacement

View File

@ -103,6 +103,11 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch` - `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。 - Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。 - Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。
- 已确认的 retained-native frame 持有不可变布局文档及其精确文档 revision候选
session 的 fork 共享该文档。同步 frame 只有在文档 base/target revision 相等且
命中 confirmed 文档、同时 TP runtime revision 命中 confirmed frame 时,才可
省略文档。IR 变化必须携带下一 document revision 的完整 replacement document
confirm 原子晋升候选文档,失败或取消不修改 parent session。
- 只有继续已提交 native frame 的 full declarative update才会在 stable native - 只有继续已提交 native frame 的 full declarative update才会在 stable native
eligibility 前为全部 candidate node 计算 selector-local style生成的 eligibility 前为全部 candidate node 计算 selector-local style生成的
`:styles-prepared-p` certificate 防止 replacement runtime 丢失 computed style `:styles-prepared-p` certificate 防止 replacement runtime 丢失 computed style

View File

@ -22,6 +22,8 @@
"ebox-native-reflow" (&rest arguments)) "ebox-native-reflow" (&rest arguments))
(declare-function ebox-native-reflow-fork-session (declare-function ebox-native-reflow-fork-session
"ebox-native-reflow" (session &rest options)) "ebox-native-reflow" (session &rest options))
(declare-function ebox-native-reflow-release-session
"ebox-native-reflow" (session))
(declare-function ebox-native-reflow-confirm-native-frame (declare-function ebox-native-reflow-confirm-native-frame
"ebox-native-reflow" "ebox-native-reflow"
(session generation key confirmed-revision)) (session generation key confirmed-revision))
@ -637,8 +639,17 @@ publication transaction can still roll back."
(defun ebox-native-commit--downgrade-candidate (state reason) (defun ebox-native-commit--downgrade-candidate (state reason)
"Downgrade STATE after native rendering fails for diagnostic REASON." "Downgrade STATE after native rendering fails for diagnostic REASON."
(let ((session (plist-get state :native-sync-session)))
(dolist (key ebox-native-commit--failed-render-state-keys) (dolist (key ebox-native-commit--failed-render-state-keys)
(cl-remf state key)) (cl-remf state key))
;; Once native execution begins STATE owns an isolated candidate session.
;; Release it before removing the only reference, while containing cleanup
;; faults so the ordinary semantic fallback remains the primary result.
(when session
(let ((inhibit-quit t) (quit-flag nil))
(condition-case nil
(ebox-native-reflow-release-session session)
((error quit) nil)))))
(plist-put state :projection-kind nil) (plist-put state :projection-kind nil)
;; Surface consumes this once into the transaction report, then removes it ;; Surface consumes this once into the transaction report, then removes it
;; from runtime state. It never participates in projection eligibility. ;; from runtime state. It never participates in projection eligibility.

View File

@ -95,7 +95,7 @@
(defvar ebox--flex-content-min-width-table) (defvar ebox--flex-content-min-width-table)
(defconst ebox-native-reflow-abi-version "10:7:12" (defconst ebox-native-reflow-abi-version "11:7:12"
"Version tuple shared by the native module, layout IR, and render tape.") "Version tuple shared by the native module, layout IR, and render tape.")
(defconst ebox-native-reflow--minimum-rust-version "1.82.0" (defconst ebox-native-reflow--minimum-rust-version "1.82.0"
@ -2222,9 +2222,10 @@ backend has a matching two-dimensional layout contract."
(and (listp node) (plist-get node :ebox-type)))))) (and (listp node) (plist-get node :ebox-type))))))
(defun ebox-native-reflow--layout-package-from-root (defun ebox-native-reflow--layout-package-from-root
(root styles &optional property-templates) (root styles &optional property-templates document-revision)
"Return one immutable native package from compiled ROOT and STYLES. "Return one immutable native package from compiled ROOT and STYLES.
PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids." PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids.
DOCUMENT-REVISION is the exact retained-input revision, defaulting to one."
(list :document (list :document
(list :version 2 (list :version 2
:space-width (ebox--space-pixel-width) :space-width (ebox--space-pixel-width)
@ -2246,9 +2247,45 @@ PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids."
(plist-get descriptor :face))))) (plist-get descriptor :face)))))
styles)) styles))
:root root) :root root)
:document-revision (or document-revision 1)
:styles (vconcat styles) :styles (vconcat styles)
:property-templates (vconcat property-templates))) :property-templates (vconcat property-templates)))
(defun ebox-native-reflow--vector-elements-eq-p (left right)
"Return non-nil when LEFT and RIGHT contain the same objects in order."
(and (vectorp left)
(vectorp right)
(= (length left) (length right))
(cl-loop for index below (length left)
always (eq (aref left index) (aref right index)))))
(defun ebox-native-reflow--reuse-exact-layout-package (old candidate)
"Return OLD when CANDIDATE has exactly the same immutable layout input.
Otherwise return CANDIDATE with the next document revision."
(let ((old-document (plist-get old :document))
(candidate-document (plist-get candidate :document)))
(if (and old old-document candidate-document
(eq (plist-get old-document :root)
(plist-get candidate-document :root))
(equal (plist-get old-document :version)
(plist-get candidate-document :version))
(equal (plist-get old-document :space-width)
(plist-get candidate-document :space-width))
(equal (plist-get old-document :style-count)
(plist-get candidate-document :style-count))
(equal (plist-get old-document :property-template-count)
(plist-get candidate-document
:property-template-count))
(ebox-native-reflow--vector-elements-eq-p
(plist-get old :styles) (plist-get candidate :styles))
(ebox-native-reflow--vector-elements-eq-p
(plist-get old :property-templates)
(plist-get candidate :property-templates)))
old
(let ((replacement (copy-sequence candidate)))
(plist-put replacement :document-revision
(1+ (or (plist-get old :document-revision) 0)))))))
(defun ebox-native-reflow--buffer-display-signature (buffer) (defun ebox-native-reflow--buffer-display-signature (buffer)
"Return BUFFER's live canonical Surface display capability." "Return BUFFER's live canonical Surface display capability."
(with-current-buffer buffer (with-current-buffer buffer
@ -2610,11 +2647,15 @@ per call, and no call recursively visits the captured Ebox tree."
(compile-current current)))) (compile-current current))))
(when retained-fast-p (when retained-fast-p
(seed-fragment node))) (seed-fragment node)))
(let ((package (let* ((candidate
(ebox-native-reflow--layout-package-from-root (ebox-native-reflow--layout-package-from-root
(ebox-native-reflow--compiled-scene-child fragments node) (ebox-native-reflow--compiled-scene-child fragments node)
ebox-native-reflow--compile-styles ebox-native-reflow--compile-styles
ebox-native-reflow--compile-property-templates))) ebox-native-reflow--compile-property-templates))
(package
(ebox-native-reflow--reuse-exact-layout-package
(ebox-native-reflow-session-layout-package session)
candidate)))
(setf (ebox-native-reflow-session-layout-fragment-cache session) (setf (ebox-native-reflow-session-layout-fragment-cache session)
new-cache new-cache
(ebox-native-reflow-session-layout-fragment-revision session) (ebox-native-reflow-session-layout-fragment-revision session)
@ -2639,8 +2680,11 @@ per call, and no call recursively visits the captured Ebox tree."
(encode-coding-string (encode-coding-string
(json-encode control) 'utf-8-unix))))) (json-encode control) 'utf-8-unix)))))
(defun ebox-native-reflow--layout-control-json (document frames) (defun ebox-native-reflow--layout-control-json
"Return strict control JSON for optional layout DOCUMENT and frame contexts." (document frames &optional document-base-revision
document-target-revision)
"Return strict control JSON for optional layout DOCUMENT and FRAMES.
DOCUMENT-BASE-REVISION and DOCUMENT-TARGET-REVISION bind retained input."
(unless (and (listp frames) frames) (unless (and (listp frames) frames)
(error "Native reflow requires at least one layout frame")) (error "Native reflow requires at least one layout frame"))
(let ((control (let ((control
@ -2738,6 +2782,14 @@ per call, and no call recursively visits the captured Ebox tree."
:base-root-width-override :base-root-width-override
(if base-root-width-override-p t :false)))))) (if base-root-width-override-p t :false))))))
frames))))) frames)))))
(when document-base-revision
(setq control
(plist-put control :document-base-revision
document-base-revision)))
(when document-target-revision
(setq control
(plist-put control :document-target-revision
document-target-revision)))
(when document (when document
(setq control (plist-put control :document document))) (setq control (plist-put control :document document)))
(ebox-native-reflow--serialize-layout-control control))) (ebox-native-reflow--serialize-layout-control control)))
@ -2748,22 +2800,29 @@ per call, and no call recursively visits the captured Ebox tree."
LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE." LAYOUT-PACKAGE may reuse a caller-validated document for the same NODE."
(unless (and (integerp generation) (>= generation 0)) (unless (and (integerp generation) (>= generation 0))
(error "Native reflow generation must be a nonnegative integer")) (error "Native reflow generation must be a nonnegative integer"))
(let* ((package (or layout-package (let* ((old-package (ebox-native-reflow-session-layout-package session))
(candidate-package
(or layout-package
(ebox-native-reflow--compile-layout-package (ebox-native-reflow--compile-layout-package
node node
(ebox-native-reflow-session-styles session) (ebox-native-reflow-session-styles session)
(plist-get (plist-get old-package :property-templates))))
(ebox-native-reflow-session-layout-package session) (package
:property-templates)))) (ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package))
(register-layout-p (register-layout-p
(not (eq package (not (eq package
(ebox-native-reflow-session-layout-package session)))) old-package)))
(base-revision (or (plist-get old-package :document-revision) 0))
(target-revision (or (plist-get package :document-revision)
(1+ base-revision)))
(accepted (accepted
(ebox-native--module-submit (ebox-native--module-submit
(ebox-native-reflow--live-handle session) (ebox-native-reflow--live-handle session)
generation generation
(ebox-native-reflow--layout-control-json (ebox-native-reflow--layout-control-json
(and register-layout-p (plist-get package :document)) frames)))) (and register-layout-p (plist-get package :document)) frames
base-revision target-revision))))
(setf (ebox-native-reflow-session-generation session) generation) (setf (ebox-native-reflow-session-generation session) generation)
(setf (ebox-native-reflow-session-styles session) (setf (ebox-native-reflow-session-styles session)
(plist-get package :styles)) (plist-get package :styles))
@ -3896,7 +3955,9 @@ root effect metadata required by a thin host commit."
(unless (and (ebox-native-reflow-session-p session) (unless (and (ebox-native-reflow-session-p session)
(fboundp 'ebox-native--module-render-session-frame)) (fboundp 'ebox-native--module-render-session-frame))
(error "Native retained frame execution is unavailable")) (error "Native retained frame execution is unavailable"))
(let* ((package (let* ((old-package
(ebox-native-reflow-session-layout-package session))
(candidate-package
(or layout-package (or layout-package
(if state (if state
(ebox-native-reflow--compile-retained-layout-package (ebox-native-reflow--compile-retained-layout-package
@ -3907,9 +3968,16 @@ root effect metadata required by a thin host commit."
(plist-get (plist-get
(ebox-native-reflow-session-layout-package session) (ebox-native-reflow-session-layout-package session)
:property-templates))))) :property-templates)))))
(package
(ebox-native-reflow--reuse-exact-layout-package
old-package candidate-package))
(generation (1+ (ebox-native-reflow-session-generation session))) (generation (1+ (ebox-native-reflow-session-generation session)))
(control-frame (copy-sequence frame)) (control-frame (copy-sequence frame))
(native-key (or (plist-get control-frame :key) 1))) (native-key (or (plist-get control-frame :key) 1))
(reuse-document-p (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))))
(setq control-frame (plist-put control-frame :key native-key)) (setq control-frame (plist-put control-frame :key native-key))
(let ((result (let ((result
(ebox-native-reflow--materialize-module-frame (ebox-native-reflow--materialize-module-frame
@ -3917,7 +3985,8 @@ root effect metadata required by a thin host commit."
(ebox-native-reflow--live-handle session) (ebox-native-reflow--live-handle session)
generation generation
(ebox-native-reflow--layout-control-json (ebox-native-reflow--layout-control-json
(plist-get package :document) (list control-frame))) (unless reuse-document-p (plist-get package :document))
(list control-frame) base-revision target-revision))
package control-frame generation))) package control-frame generation)))
(setf (ebox-native-reflow-session-generation session) generation (setf (ebox-native-reflow-session-generation session) generation
(ebox-native-reflow-session-layout-package session) package (ebox-native-reflow-session-layout-package session) package

View File

@ -164,7 +164,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
(void) nargs; (void) nargs;
(void) args; (void) args;
(void) data; (void) data;
static const char version[] = "10:7:12"; static const char version[] = "11:7:12";
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1)); return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
} }

View File

@ -324,6 +324,10 @@ struct ControlBatch {
version: u32, version: u32,
#[serde(default)] #[serde(default)]
document: Option<LayoutDocument>, document: Option<LayoutDocument>,
#[serde(default, rename = "document-base-revision")]
document_base_revision: Option<u64>,
#[serde(default, rename = "document-target-revision")]
document_target_revision: Option<u64>,
frames: Vec<ControlFrame>, frames: Vec<ControlFrame>,
} }
@ -382,6 +386,8 @@ enum JobPayload {
context_hash: i64, context_hash: i64,
complete: bool, complete: bool,
root_metadata: bool, root_metadata: bool,
document_base_revision: u64,
document_target_revision: u64,
}, },
} }
@ -398,6 +404,8 @@ struct BaselineIdentity {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct ConfirmedBaseline { struct ConfirmedBaseline {
identity: BaselineIdentity, identity: BaselineIdentity,
document: Arc<LayoutDocument>,
document_revision: u64,
tape: LayoutTape, tape: LayoutTape,
styles: Vec<layout::StyleTemplate>, styles: Vec<layout::StyleTemplate>,
} }
@ -405,6 +413,8 @@ struct ConfirmedBaseline {
#[derive(Debug)] #[derive(Debug)]
struct PendingBaseline { struct PendingBaseline {
confirmed_identity: BaselineIdentity, confirmed_identity: BaselineIdentity,
document: Arc<LayoutDocument>,
document_revision: u64,
tape: LayoutTape, tape: LayoutTape,
styles: Vec<layout::StyleTemplate>, styles: Vec<layout::StyleTemplate>,
} }
@ -450,6 +460,9 @@ struct RuntimeState {
baseline_misses: u64, baseline_misses: u64,
base_renders: u64, base_renders: u64,
target_renders: u64, target_renders: u64,
document_parses: u64,
document_validations: u64,
document_reuses: u64,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>, confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
} }
@ -500,6 +513,9 @@ struct SessionStats {
baseline_misses: u64, baseline_misses: u64,
base_renders: u64, base_renders: u64,
target_renders: u64, target_renders: u64,
document_parses: u64,
document_validations: u64,
document_reuses: u64,
pending_baselines: usize, pending_baselines: usize,
confirmed_baseline: bool, confirmed_baseline: bool,
confirmed_baseline_bytes: usize, confirmed_baseline_bytes: usize,
@ -562,6 +578,9 @@ impl Session {
baseline_misses: 0, baseline_misses: 0,
base_renders: 0, base_renders: 0,
target_renders: 0, target_renders: 0,
document_parses: 0,
document_validations: 0,
document_reuses: 0,
confirmed_baseline, confirmed_baseline,
}), }),
readiness_channel: Mutex::new(None), readiness_channel: Mutex::new(None),
@ -586,10 +605,17 @@ impl Session {
} }
} }
} }
let layout_document = shared
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.as_ref()
.map(|baseline| Arc::clone(&baseline.document));
Ok(Box::new(Self { Ok(Box::new(Self {
shared, shared,
workers: Mutex::new(Some(handles)), workers: Mutex::new(Some(handles)),
layout_document: Mutex::new(None), layout_document: Mutex::new(layout_document),
worker_count, worker_count,
})) }))
} }
@ -623,16 +649,41 @@ impl Session {
let layout_requested = let layout_requested =
batch.document.is_some() || batch.frames.iter().any(|frame| frame.payload.is_none()); batch.document.is_some() || batch.frames.iter().any(|frame| frame.payload.is_none());
let register_document = batch.document.is_some(); let register_document = batch.document.is_some();
let confirmed_baseline = self
.shared
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.clone();
let confirmed_document_revision = confirmed_baseline
.as_ref()
.map_or(0, |baseline| baseline.document_revision);
let document_base_revision = batch
.document_base_revision
.unwrap_or(confirmed_document_revision);
let document_target_revision = match batch.document_target_revision {
Some(revision) => revision,
None if register_document => document_base_revision
.checked_add(1)
.ok_or_else(|| "Native layout document revision overflow".to_owned())?,
None => document_base_revision,
};
let document = match batch.document { let document = match batch.document {
Some(document) => { Some(document) => {
document.validate()?; document.validate()?;
Some(Arc::new(document)) Some(Arc::new(document))
} }
None if layout_requested => Some( None if layout_requested => Some(
confirmed_baseline
.as_ref()
.map(|baseline| Arc::clone(&baseline.document))
.or_else(|| {
self.layout_document self.layout_document
.lock() .lock()
.unwrap_or_else(|poison| poison.into_inner()) .unwrap_or_else(|poison| poison.into_inner())
.clone() .clone()
})
.ok_or_else(|| { .ok_or_else(|| {
"Native layout frames require a registered document".to_owned() "Native layout frames require a registered document".to_owned()
})?, })?,
@ -648,7 +699,12 @@ impl Session {
let mut prepared = Vec::with_capacity(batch.frames.len()); let mut prepared = Vec::with_capacity(batch.frames.len());
for frame in batch.frames { for frame in batch.frames {
let job = if let Some(document) = &document { let job = if let Some(document) = &document {
prepare_layout_job(document, frame)? prepare_layout_job(
document,
frame,
document_base_revision,
document_target_revision,
)?
} else { } else {
let payload = frame let payload = frame
.payload .payload
@ -780,13 +836,75 @@ impl Session {
return Err("Native reflow session is closed".to_owned()); return Err("Native reflow session is closed".to_owned());
} }
let batch = parse_control_batch(payload)?; let batch = parse_control_batch(payload)?;
let document = batch
.document
.ok_or_else(|| "Native retained render requires an inline document".to_owned())?;
document.validate()?;
if batch.frames.len() != 1 { if batch.frames.len() != 1 {
return Err("Native retained render requires exactly one frame".to_owned()); return Err("Native retained render requires exactly one frame".to_owned());
} }
let document_base_revision = batch
.document_base_revision
.ok_or_else(|| "Native retained render requires exact document revisions".to_owned())?;
let document_target_revision = batch
.document_target_revision
.ok_or_else(|| "Native retained render requires exact document revisions".to_owned())?;
let confirmed = self
.shared
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.clone();
let (document, document_parses, document_validations, document_reuses) = match batch
.document
{
Some(document) => {
if document_base_revision.checked_add(1) != Some(document_target_revision) {
return Err(
"Native retained document replacement must advance one revision".to_owned(),
);
}
match &confirmed {
Some(baseline) if baseline.document_revision != document_base_revision => {
return Err(
"Native retained document base revision does not match confirmed state"
.to_owned(),
);
}
None if document_base_revision != 0 => {
return Err(
"Native retained document bootstrap must start at revision zero"
.to_owned(),
);
}
_ => {}
}
document.validate()?;
(Arc::new(document), 1, 1, 0)
}
None => {
if document_base_revision != document_target_revision {
return Err(
"Native retained document reuse requires equal base and target revisions"
.to_owned(),
);
}
let baseline = confirmed.as_ref().ok_or_else(|| {
"Native retained document reuse requires a confirmed baseline".to_owned()
})?;
if baseline.document_revision != document_target_revision {
return Err(
"Native retained document revision does not match confirmed state"
.to_owned(),
);
}
let frame = batch.frames.first().expect("checked retained frame");
if frame.runtime_revision != baseline.identity.runtime_revision {
return Err(
"Native retained runtime revision does not match confirmed state"
.to_owned(),
);
}
(Arc::clone(&baseline.document), 0, 0, 1)
}
};
let frame = batch let frame = batch
.frames .frames
.into_iter() .into_iter()
@ -795,15 +913,12 @@ impl Session {
if !frame.complete || frame.delay_ms != 0 { if !frame.complete || frame.delay_ms != 0 {
return Err("Native retained render requires one immediate complete frame".to_owned()); return Err("Native retained render requires one immediate complete frame".to_owned());
} }
let document = Arc::new(document); let prepared = prepare_layout_job(
let prepared = prepare_layout_job(&document, frame)?; &document,
let confirmed = self frame,
.shared document_base_revision,
.state document_target_revision,
.lock() )?;
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.clone();
let output = render_layout_payload( let output = render_layout_payload(
prepared.payload, prepared.payload,
self.shared.id, self.shared.id,
@ -828,6 +943,9 @@ impl Session {
} }
state.base_renders += output.base_renders; state.base_renders += output.base_renders;
state.target_renders += output.target_renders; state.target_renders += output.target_renders;
state.document_parses += document_parses;
state.document_validations += document_validations;
state.document_reuses += document_reuses;
state.pending_baselines.clear(); state.pending_baselines.clear();
if let Some(pending) = output.pending { if let Some(pending) = output.pending {
state state
@ -857,6 +975,8 @@ impl Session {
identity.runtime_revision = confirmed_revision; identity.runtime_revision = confirmed_revision;
state.confirmed_baseline = Some(Arc::new(ConfirmedBaseline { state.confirmed_baseline = Some(Arc::new(ConfirmedBaseline {
identity, identity,
document: pending.document,
document_revision: pending.document_revision,
tape: pending.tape, tape: pending.tape,
styles: pending.styles, styles: pending.styles,
})); }));
@ -955,6 +1075,9 @@ impl Session {
baseline_misses: state.baseline_misses, baseline_misses: state.baseline_misses,
base_renders: state.base_renders, base_renders: state.base_renders,
target_renders: state.target_renders, target_renders: state.target_renders,
document_parses: state.document_parses,
document_validations: state.document_validations,
document_reuses: state.document_reuses,
pending_baselines, pending_baselines,
confirmed_baseline: state.confirmed_baseline.is_some(), confirmed_baseline: state.confirmed_baseline.is_some(),
confirmed_baseline_bytes, confirmed_baseline_bytes,
@ -1043,24 +1166,23 @@ fn checked_root_width(frame_key: i64, root_width: i64, label: &str) -> Result<i6
fn confirmed_baseline_matches( fn confirmed_baseline_matches(
baseline: &ConfirmedBaseline, baseline: &ConfirmedBaseline,
context: LayoutContext, document_base_revision: u64,
root_width: i64, identity: &BaselineIdentity,
root_width_override: bool,
runtime_revision: u64,
context_hash: i64,
complete: bool,
) -> bool { ) -> bool {
baseline.identity.context == context baseline.document_revision == document_base_revision
&& baseline.identity.root_width == root_width && baseline.identity.context == identity.context
&& baseline.identity.root_width_override == root_width_override && baseline.identity.root_width == identity.root_width
&& baseline.identity.runtime_revision == runtime_revision && baseline.identity.root_width_override == identity.root_width_override
&& baseline.identity.context_hash == context_hash && baseline.identity.runtime_revision == identity.runtime_revision
&& baseline.identity.complete == complete && baseline.identity.context_hash == identity.context_hash
&& baseline.identity.complete == identity.complete
} }
fn prepare_layout_job( fn prepare_layout_job(
document: &Arc<LayoutDocument>, document: &Arc<LayoutDocument>,
frame: ControlFrame, frame: ControlFrame,
document_base_revision: u64,
document_target_revision: u64,
) -> Result<PreparedJob, String> { ) -> Result<PreparedJob, String> {
if frame.payload.is_some() { if frame.payload.is_some() {
return Err(format!( return Err(format!(
@ -1143,6 +1265,8 @@ fn prepare_layout_job(
context_hash: frame.context_hash, context_hash: frame.context_hash,
complete: frame.complete, complete: frame.complete,
root_metadata: frame.root_metadata, root_metadata: frame.root_metadata,
document_base_revision,
document_target_revision,
}, },
}) })
} }
@ -1176,6 +1300,8 @@ fn render_layout_payload(
context_hash, context_hash,
complete, complete,
root_metadata, root_metadata,
document_base_revision,
document_target_revision,
} => { } => {
let identity = TapeIdentity { let identity = TapeIdentity {
session_id, session_id,
@ -1208,17 +1334,21 @@ fn render_layout_payload(
type LayoutRenderOutcome = Result<(Vec<u8>, LayoutTape, bool, u64, u64), String>; type LayoutRenderOutcome = Result<(Vec<u8>, LayoutTape, bool, u64, u64), String>;
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome { let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
if let Some(base_context) = base_context { if let Some(base_context) = base_context {
let base_identity = BaselineIdentity {
context: base_context,
root_width: base_root_width,
root_width_override: base_root_width_override,
runtime_revision,
context_hash,
complete,
};
let base_hit = confirmed_baseline let base_hit = confirmed_baseline
.as_ref() .as_ref()
.filter(|baseline| { .filter(|baseline| {
confirmed_baseline_matches( confirmed_baseline_matches(
baseline, baseline,
base_context, document_base_revision,
base_root_width, &base_identity,
base_root_width_override,
runtime_revision,
context_hash,
complete,
) && baseline.styles == document.styles ) && baseline.styles == document.styles
}) })
.cloned(); .cloned();
@ -1275,6 +1405,8 @@ fn render_layout_payload(
bytes, bytes,
pending: Some(PendingBaseline { pending: Some(PendingBaseline {
confirmed_identity: pending_identity, confirmed_identity: pending_identity,
document: Arc::clone(&document),
document_revision: document_target_revision,
tape, tape,
styles: document.styles.clone(), styles: document.styles.clone(),
}), }),
@ -1329,7 +1461,7 @@ fn render_proof(payload: &[u8]) -> Result<Vec<u8>, String> {
if frame.delay_ms != 0 { if frame.delay_ms != 0 {
return Err("Native proof render cannot contain delay-ms".to_owned()); return Err("Native proof render cannot contain delay-ms".to_owned());
} }
let prepared = prepare_layout_job(&Arc::new(document), frame)?; let prepared = prepare_layout_job(&Arc::new(document), frame, 0, 1)?;
let output = render_layout_payload( let output = render_layout_payload(
prepared.payload, prepared.payload,
SYNC_RENDER_SESSION_ID, SYNC_RENDER_SESSION_ID,
@ -1847,14 +1979,41 @@ mod tests {
fn proof_layout_payload(frames: &str) -> Vec<u8> { fn proof_layout_payload(frames: &str) -> Vec<u8> {
format!( format!(
r#"{{"version":1,"document":{{"version":2,"space-width":8,"style-count":0,"styles":[],"root":{{"type":"box","region-id":1,"content":{{"lines":[{{"clusters":[{{"text":"x","width":8,"cjk":false,"space":false}}]}}]}},"child":null,"content-width-exact":false,"width":{{"kind":"viewport"}},"min-width":{{"kind":"pixels","value":0}},"max-width":{{"kind":"none"}},"height":{{"kind":"auto"}},"min-height":{{"kind":"lines","value":0}},"max-height":{{"kind":"none"}},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}}}},"frames":{frames}}}"# r#"{{"version":1,"document-base-revision":0,"document-target-revision":1,"document":{{"version":2,"space-width":8,"style-count":0,"styles":[],"root":{{"type":"box","region-id":1,"content":{{"lines":[{{"clusters":[{{"text":"x","width":8,"cjk":false,"space":false}}]}}]}},"child":null,"content-width-exact":false,"width":{{"kind":"viewport"}},"min-width":{{"kind":"pixels","value":0}},"max-width":{{"kind":"none"}},"height":{{"kind":"auto"}},"min-height":{{"kind":"lines","value":0}},"max-height":{{"kind":"none"}},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}}}},"frames":{frames}}}"#
)
.into_bytes()
}
fn retained_layout_payload(document_revision: u64, frames: &str) -> Vec<u8> {
format!(
r#"{{"version":1,"document-base-revision":{document_revision},"document-target-revision":{document_revision},"frames":{frames}}}"#
)
.into_bytes()
}
fn replacement_layout_payload(
document_base_revision: u64,
document_target_revision: u64,
frames: &str,
) -> Vec<u8> {
let payload = String::from_utf8(proof_layout_payload(frames)).unwrap();
payload
.replacen(
r#""document-base-revision":0"#,
&format!(r#""document-base-revision":{document_base_revision}"#),
1,
)
.replacen(
r#""document-target-revision":1"#,
&format!(r#""document-target-revision":{document_target_revision}"#),
1,
) )
.into_bytes() .into_bytes()
} }
fn column_proof_layout_payload(frames: &str) -> Vec<u8> { fn column_proof_layout_payload(frames: &str) -> Vec<u8> {
format!( format!(
r#"{{"version":1,"document":{{"version":2,"space-width":8,"style-count":0,"styles":[],"root":{{"type":"column","children":[{{"type":"box","region-id":1,"content":{{"lines":[{{"clusters":[{{"text":"x","width":8,"cjk":false,"space":false}}]}}]}},"child":null,"content-width-exact":false,"width":{{"kind":"content"}},"min-width":{{"kind":"pixels","value":0}},"max-width":{{"kind":"none"}},"height":{{"kind":"auto"}},"min-height":{{"kind":"lines","value":0}},"max-height":{{"kind":"none"}},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}},{{"type":"box","region-id":2,"content":{{"lines":[{{"clusters":[{{"text":"yy","width":16,"cjk":false,"space":false}}]}}]}},"child":null,"content-width-exact":false,"width":{{"kind":"content"}},"min-width":{{"kind":"pixels","value":0}},"max-width":{{"kind":"none"}},"height":{{"kind":"auto"}},"min-height":{{"kind":"lines","value":0}},"max-height":{{"kind":"none"}},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}}]}}}},"frames":{frames}}}"# r#"{{"version":1,"document-base-revision":0,"document-target-revision":1,"document":{{"version":2,"space-width":8,"style-count":0,"styles":[],"root":{{"type":"column","children":[{{"type":"box","region-id":1,"content":{{"lines":[{{"clusters":[{{"text":"x","width":8,"cjk":false,"space":false}}]}}]}},"child":null,"content-width-exact":false,"width":{{"kind":"content"}},"min-width":{{"kind":"pixels","value":0}},"max-width":{{"kind":"none"}},"height":{{"kind":"auto"}},"min-height":{{"kind":"lines","value":0}},"max-height":{{"kind":"none"}},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}},{{"type":"box","region-id":2,"content":{{"lines":[{{"clusters":[{{"text":"yy","width":16,"cjk":false,"space":false}}]}}]}},"child":null,"content-width-exact":false,"width":{{"kind":"content"}},"min-width":{{"kind":"pixels","value":0}},"max-width":{{"kind":"none"}},"height":{{"kind":"auto"}},"min-height":{{"kind":"lines","value":0}},"max-height":{{"kind":"none"}},"box-sizing":"border-box","padding-left":0,"padding-right":0,"padding-top":0,"padding-bottom":0,"margin-left":0,"margin-right":0,"margin-top":0,"margin-bottom":0,"border-left":0,"border-right":0,"foreground-style":null,"background-style":null,"border-left-style":null,"border-right-style":null,"border-top-style":null,"border-bottom-style":null,"text-align":"left","vertical-align":"top","overflow":"scroll","wrap-mode":"word","scroll-offset":0}}]}}}},"frames":{frames}}}"#
) )
.into_bytes() .into_bytes()
} }
@ -2083,10 +2242,11 @@ mod tests {
assert_eq!(child_initial.ready_results, 0); assert_eq!(child_initial.ready_results, 0);
assert_eq!(child_initial.pending_baselines, 0); assert_eq!(child_initial.pending_baselines, 0);
assert!(child_initial.confirmed_baseline); assert!(child_initial.confirmed_baseline);
assert!(!child_initial.layout_registered); assert!(child_initial.layout_registered);
parent.stop(true); parent.stop(true);
let second = proof_layout_payload( let second = retained_layout_payload(
1,
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":1,"context-hash":77}]"#, r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":1,"context-hash":77}]"#,
); );
let second_tape = child.render_sync(1, &second).unwrap(); let second_tape = child.render_sync(1, &second).unwrap();
@ -2110,6 +2270,152 @@ mod tests {
child.stop(true); child.stop(true);
} }
#[test]
fn confirmed_fork_reuses_retained_document_without_reparse_or_revalidation() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = proof_layout_payload(
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"root-width":120,"runtime-revision":0,"context-hash":77}]"#,
);
assert!(!parent.render_sync(1, &first).unwrap().is_empty());
assert!(parent.confirm(1, 1, 1).unwrap());
let child = parent.fork_confirmed().unwrap();
let retained = retained_layout_payload(
1,
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":1,"context-hash":77}]"#,
);
let tape = child.render_sync(1, &retained).unwrap();
assert!(tape_patch_p(&tape));
let stats = child.stats();
assert_eq!(stats.document_parses, 0);
assert_eq!(stats.document_validations, 0);
assert_eq!(stats.document_reuses, 1);
assert!(child.confirm(1, 2, 2).unwrap());
let grandchild = child.fork_confirmed().unwrap();
let next = retained_layout_payload(
1,
r#"[{"key":3,"viewport-width":100,"viewport-height":10,"root-width":100,"patch":true,"base-viewport-width":80,"base-viewport-height":10,"base-root-width":80,"runtime-revision":2,"context-hash":77}]"#,
);
assert!(tape_patch_p(&grandchild.render_sync(1, &next).unwrap()));
assert_eq!(grandchild.stats().document_reuses, 1);
parent.stop(true);
child.stop(true);
grandchild.stop(true);
}
#[test]
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = proof_layout_payload(
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"runtime-revision":0}]"#,
);
parent.render_sync(1, &first).unwrap();
assert!(parent.confirm(1, 1, 1).unwrap());
let child = parent.fork_confirmed().unwrap();
let wrong_document = retained_layout_payload(
2,
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"runtime-revision":1}]"#,
);
assert!(child
.render_sync(1, &wrong_document)
.unwrap_err()
.contains("document revision"));
let wrong_runtime = retained_layout_payload(
1,
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"runtime-revision":0}]"#,
);
assert!(child
.render_sync(1, &wrong_runtime)
.unwrap_err()
.contains("runtime revision"));
let ambiguous = br#"{"version":1,"frames":[{"key":2,"viewport-width":80,"viewport-height":10,"runtime-revision":1}]}"#;
assert!(child
.render_sync(1, ambiguous)
.unwrap_err()
.contains("document revisions"));
assert_eq!(child.stats().document_reuses, 0);
assert!(parent.stats().confirmed_baseline);
parent.stop(true);
child.stop(true);
}
#[test]
fn cancelled_replacement_does_not_promote_or_contaminate_its_parent() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = proof_layout_payload(
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"runtime-revision":0}]"#,
);
parent.render_sync(1, &first).unwrap();
assert!(parent.confirm(1, 1, 1).unwrap());
let child = parent.fork_confirmed().unwrap();
let replacement = replacement_layout_payload(
1,
2,
r#"[{"key":2,"viewport-width":100,"viewport-height":10,"runtime-revision":1}]"#,
);
assert!(!child.render_sync(1, &replacement).unwrap().is_empty());
assert_eq!(child.stats().pending_baselines, 1);
child.cancel(1);
assert_eq!(child.stats().pending_baselines, 0);
let retry = child.fork_confirmed().unwrap();
let retained = retained_layout_payload(
1,
r#"[{"key":3,"viewport-width":80,"viewport-height":10,"runtime-revision":1}]"#,
);
assert!(!retry.render_sync(1, &retained).unwrap().is_empty());
assert_eq!(retry.stats().document_reuses, 1);
assert_eq!(parent.stats().document_parses, 1);
assert_eq!(parent.stats().document_reuses, 0);
parent.stop(true);
child.stop(true);
retry.stop(true);
}
#[test]
fn async_omit_uses_the_latest_synchronously_confirmed_document() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = proof_layout_payload(
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"runtime-revision":0}]"#,
);
let first_count = full_tape_character_count(&parent.render_sync(1, &first).unwrap());
assert!(parent.confirm(1, 1, 1).unwrap());
let parent_omit =
batch(r#"[{"key":2,"viewport-width":120,"viewport-height":10,"runtime-revision":1}]"#);
assert_eq!(parent.submit(2, &parent_omit).unwrap(), 1);
wait_until(|| parent.ready(2, 2));
assert_eq!(
full_tape_character_count(&parent.take(2, 2).unwrap()),
first_count
);
let child = parent.fork_confirmed().unwrap();
let replacement = String::from_utf8(replacement_layout_payload(
1,
2,
r#"[{"key":3,"viewport-width":120,"viewport-height":10,"runtime-revision":1}]"#,
))
.unwrap()
.replacen(r#""text":"x","width":8"#, r#""text":"yy","width":16"#, 1)
.into_bytes();
let replacement_count =
full_tape_character_count(&child.render_sync(1, &replacement).unwrap());
assert_ne!(replacement_count, first_count);
assert!(child.confirm(1, 3, 2).unwrap());
let child_omit =
batch(r#"[{"key":4,"viewport-width":120,"viewport-height":10,"runtime-revision":2}]"#);
assert_eq!(child.submit(2, &child_omit).unwrap(), 1);
wait_until(|| child.ready(2, 4));
assert_eq!(
full_tape_character_count(&child.take(2, 4).unwrap()),
replacement_count
);
parent.stop(true);
child.stop(true);
}
#[test] #[test]
fn confirmed_fork_falls_back_to_full_frame_on_identity_mismatch() { fn confirmed_fork_falls_back_to_full_frame_on_identity_mismatch() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap(); let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
@ -2120,7 +2426,9 @@ mod tests {
assert!(parent.confirm(1, 1, 1).unwrap()); assert!(parent.confirm(1, 1, 1).unwrap());
let child = parent.fork_confirmed().unwrap(); let child = parent.fork_confirmed().unwrap();
let mismatched = proof_layout_payload( let mismatched = replacement_layout_payload(
1,
2,
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":2,"context-hash":77}]"#, r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":2,"context-hash":77}]"#,
); );
let tape = child.render_sync(1, &mismatched).unwrap(); let tape = child.render_sync(1, &mismatched).unwrap();

View File

@ -2399,6 +2399,174 @@ remain retained identities."
(equal '(:foreground "#222222") (equal '(:foreground "#222222")
(plist-get (aref styles style-id) :face)))))) (plist-get (aref styles style-id) :face))))))
(ert-deftest ebox-native-retained-package-reuse-requires-exact-ir-identity ()
"Only an identity-preserving compile may reuse the confirmed package."
(let* ((root (list :type "row" :children []))
(style (list :mode 'face :face '(:foreground "red")))
(template (list :mouse-face 'highlight))
(old
(list :document
(list :version 2 :space-width 8 :style-count 1
:property-template-count 1
:styles (vector style) :root root)
:document-revision 7
:styles (vector style)
:property-templates (vector template)))
(same
(list :document
(list :version 2 :space-width 8 :style-count 1
:property-template-count 1
:styles (vector style) :root root)
:styles (vector style)
:property-templates (vector template))))
(should (eq old
(ebox-native-reflow--reuse-exact-layout-package old same)))
(dolist (candidate
(list
(copy-tree same)
(let ((copy (copy-tree same)))
(plist-put (plist-get copy :document) :space-width 9)
copy)
(let ((copy (copy-tree same)))
(plist-put (plist-get copy :document) :root (copy-tree root))
copy)
(let ((copy (copy-tree same)))
(plist-put copy :styles
(vector (copy-tree style)))
copy)
(let ((copy (copy-tree same)))
(plist-put copy :property-templates
(vector (copy-tree template)))
copy)))
;; COPY-TREE deliberately destroys the required root/style/template
;; object identity even when values remain equal.
(should-not
(eq old (ebox-native-reflow--reuse-exact-layout-package old candidate))))))
(ert-deftest ebox-native-retained-sync-omits-an-exactly-reused-document ()
"A retained frame sends only revisions and context for unchanged IR."
(let* ((document (list :version 2 :space-width 8 :style-count 0
:property-template-count 0 :styles []
:root (list :type "row" :children [])))
(package (list :document document :document-revision 4
:styles [] :property-templates []))
(session
(ebox-native-reflow--make-session
:handle 'test :generation 3 :styles [] :layout-package package
:layout-fragment-cache (make-hash-table :test 'equal)
:layout-fragment-revision 0))
control)
(cl-letf (((symbol-function
'ebox-native-reflow--compile-retained-layout-package)
(lambda (&rest _) package))
((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"))))
(should
(equal
'(:rendered "ok")
(ebox-native-reflow-execute-session-sync
session 'node
'(:key 1 :viewport-width 80 :viewport-height 10
:runtime-revision 9)
nil 'state)))
(should-not (plist-member control :document))
(should (= (plist-get control :document-base-revision) 4))
(should (= (plist-get control :document-target-revision) 4)))))
(ert-deftest ebox-native-session-input-normalizes-every-replacement-revision ()
"Compiled and explicit replacement packages both advance at the boundary."
(let* ((styles [])
(templates [])
(old
(list :document
(list :version 2 :space-width 8 :style-count 0
:property-template-count 0 :styles []
:root (list :type "row" :children []))
:document-revision 4
:styles styles :property-templates templates))
(compiled
(list :document
(list :version 2 :space-width 8 :style-count 0
:property-template-count 0 :styles []
:root (list :type "column" :children []))
:document-revision 1
:styles styles :property-templates templates))
(explicit
(list :document
(list :version 2 :space-width 8 :style-count 0
:property-template-count 0 :styles []
:root (list :type "box"))
:document-revision 1
:styles styles :property-templates templates))
(session
(ebox-native-reflow--make-session
:handle 'test :generation 0 :styles styles :layout-package old
:layout-fragment-cache (make-hash-table :test 'equal)
:layout-fragment-revision 0))
(other-old (copy-tree old))
(other-session
(progn
(plist-put other-old :document-revision 10)
(ebox-native-reflow--make-session
:handle 'other :generation 0 :styles styles
:layout-package other-old
:layout-fragment-cache (make-hash-table :test 'equal)
:layout-fragment-revision 0)))
controls)
(cl-letf (((symbol-function 'ebox-native-reflow--compile-layout-package)
(lambda (&rest _) compiled))
((symbol-function 'ebox-native--module-render-session-frame)
(lambda (_handle _generation payload)
(push (json-parse-string payload :object-type 'plist
:array-type 'array)
controls)
'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
:runtime-revision 9))
(should (= (plist-get (ebox-native-reflow-session-layout-package session)
:document-revision)
5))
(ebox-native-reflow-execute-session-sync
session 'node
'(:key 1 :viewport-width 90 :viewport-height 10
:runtime-revision 10)
explicit)
(setq controls (nreverse controls))
(should (plist-member (car controls) :document))
(should (= (plist-get (car controls) :document-base-revision) 4))
(should (= (plist-get (car controls) :document-target-revision) 5))
(should (plist-member (cadr controls) :document))
(should (= (plist-get (cadr controls) :document-base-revision) 5))
(should (= (plist-get (cadr controls) :document-target-revision) 6))
(should (= (plist-get (ebox-native-reflow-session-layout-package session)
:document-revision)
6))
(ebox-native-reflow-execute-session-sync
other-session 'node
'(:key 1 :viewport-width 100 :viewport-height 10
:runtime-revision 20)
explicit)
(should (= (plist-get explicit :document-revision) 1))
(should (= (plist-get (ebox-native-reflow-session-layout-package session)
:document-revision)
6))
(should (= (plist-get
(ebox-native-reflow-session-layout-package other-session)
:document-revision)
11))
(should (= (plist-get (car controls) :document-base-revision) 10))
(should (= (plist-get (car controls) :document-target-revision) 11)))))
(provide 'ebox-commit-tests) (provide 'ebox-commit-tests)
;;; ebox-commit-tests.el ends here ;;; ebox-commit-tests.el ends here

View File

@ -1500,6 +1500,11 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should (ebox-native-reflow-session-released-p session)) (should (ebox-native-reflow-session-released-p session))
(should-not (ebox-native-reflow-session-released-p (should-not (ebox-native-reflow-session-released-p
committed-session)) committed-session))
(let ((stats
(ebox-native-reflow-stats committed-session)))
(should (= (plist-get stats :document-parses) 0))
(should (= (plist-get stats :document-validations) 0))
(should (= (plist-get stats :document-reuses) 1)))
(should (plist-get (ebox--buffer-render-state buffer) (should (plist-get (ebox--buffer-render-state buffer)
:native-sync-confirmed-p)) :native-sync-confirmed-p))
(should-not (plist-get (ebox--buffer-render-state buffer) (should-not (plist-get (ebox--buffer-render-state buffer)
@ -1640,6 +1645,10 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should (stringp (should (stringp
(plist-get (tp-surface-client-state surface) (plist-get (tp-surface-client-state surface)
:native-committed-rendered))) :native-committed-rendered)))
(let ((stats (ebox-native-reflow-stats winning-session)))
(should (= (plist-get stats :document-parses) 1))
(should (= (plist-get stats :document-validations) 1))
(should (= (plist-get stats :document-reuses) 0)))
(should-not (ebox-native-reflow-session-released-p (should-not (ebox-native-reflow-session-released-p
winning-session))) winning-session)))
(let ((report (ebox-commit buffer updated))) (let ((report (ebox-commit buffer updated)))
@ -1654,7 +1663,11 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should-not (eq next-session winning-session)) (should-not (eq next-session winning-session))
(should (ebox-native-reflow-session-released-p winning-session)) (should (ebox-native-reflow-session-released-p winning-session))
(should-not (ebox-native-reflow-session-released-p (should-not (ebox-native-reflow-session-released-p
next-session))))) next-session))
(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-reuses) 0))))))
(when (buffer-live-p buffer) (when (buffer-live-p buffer)
(kill-buffer buffer)) (kill-buffer buffer))
(when next-session (when next-session