perf: retain validated native document input across commits
This commit is contained in:
parent
32b4834322
commit
76bce0bb8d
@ -104,6 +104,14 @@ Caller-owned Source Tree
|
||||
- `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.
|
||||
- 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
|
||||
selector-local styles for every candidate node before stable native
|
||||
eligibility. The `:styles-prepared-p` certificate prevents replacement
|
||||
|
||||
@ -103,6 +103,11 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
||||
- `owner-rerender` 范围大于 `span-patch`,`span-patch` 大于 `paint-patch`。
|
||||
- Buffer 坐标属于生成它的 generation,变更后必须重新获取。
|
||||
- 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
|
||||
eligibility 前为全部 candidate node 计算 selector-local style;生成的
|
||||
`:styles-prepared-p` certificate 防止 replacement runtime 丢失 computed style,
|
||||
|
||||
@ -22,6 +22,8 @@
|
||||
"ebox-native-reflow" (&rest arguments))
|
||||
(declare-function ebox-native-reflow-fork-session
|
||||
"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
|
||||
"ebox-native-reflow"
|
||||
(session generation key confirmed-revision))
|
||||
@ -637,8 +639,17 @@ publication transaction can still roll back."
|
||||
|
||||
(defun ebox-native-commit--downgrade-candidate (state reason)
|
||||
"Downgrade STATE after native rendering fails for diagnostic REASON."
|
||||
(dolist (key ebox-native-commit--failed-render-state-keys)
|
||||
(cl-remf state key))
|
||||
(let ((session (plist-get state :native-sync-session)))
|
||||
(dolist (key ebox-native-commit--failed-render-state-keys)
|
||||
(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)
|
||||
;; Surface consumes this once into the transaction report, then removes it
|
||||
;; from runtime state. It never participates in projection eligibility.
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
|
||||
(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.")
|
||||
|
||||
(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))))))
|
||||
|
||||
(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.
|
||||
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 :version 2
|
||||
: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)))))
|
||||
styles))
|
||||
:root root)
|
||||
:document-revision (or document-revision 1)
|
||||
:styles (vconcat styles)
|
||||
: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)
|
||||
"Return BUFFER's live canonical Surface display capability."
|
||||
(with-current-buffer buffer
|
||||
@ -2610,11 +2647,15 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
(compile-current current))))
|
||||
(when retained-fast-p
|
||||
(seed-fragment node)))
|
||||
(let ((package
|
||||
(ebox-native-reflow--layout-package-from-root
|
||||
(ebox-native-reflow--compiled-scene-child fragments node)
|
||||
ebox-native-reflow--compile-styles
|
||||
ebox-native-reflow--compile-property-templates)))
|
||||
(let* ((candidate
|
||||
(ebox-native-reflow--layout-package-from-root
|
||||
(ebox-native-reflow--compiled-scene-child fragments node)
|
||||
ebox-native-reflow--compile-styles
|
||||
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)
|
||||
new-cache
|
||||
(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
|
||||
(json-encode control) 'utf-8-unix)))))
|
||||
|
||||
(defun ebox-native-reflow--layout-control-json (document frames)
|
||||
"Return strict control JSON for optional layout DOCUMENT and frame contexts."
|
||||
(defun ebox-native-reflow--layout-control-json
|
||||
(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)
|
||||
(error "Native reflow requires at least one layout frame"))
|
||||
(let ((control
|
||||
@ -2738,6 +2782,14 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
:base-root-width-override
|
||||
(if base-root-width-override-p t :false))))))
|
||||
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
|
||||
(setq control (plist-put control :document document)))
|
||||
(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."
|
||||
(unless (and (integerp generation) (>= generation 0))
|
||||
(error "Native reflow generation must be a nonnegative integer"))
|
||||
(let* ((package (or layout-package
|
||||
(ebox-native-reflow--compile-layout-package
|
||||
node
|
||||
(ebox-native-reflow-session-styles session)
|
||||
(plist-get
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
:property-templates))))
|
||||
(let* ((old-package (ebox-native-reflow-session-layout-package session))
|
||||
(candidate-package
|
||||
(or layout-package
|
||||
(ebox-native-reflow--compile-layout-package
|
||||
node
|
||||
(ebox-native-reflow-session-styles session)
|
||||
(plist-get old-package :property-templates))))
|
||||
(package
|
||||
(ebox-native-reflow--reuse-exact-layout-package
|
||||
old-package candidate-package))
|
||||
(register-layout-p
|
||||
(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
|
||||
(ebox-native--module-submit
|
||||
(ebox-native-reflow--live-handle session)
|
||||
generation
|
||||
(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-styles session)
|
||||
(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)
|
||||
(fboundp 'ebox-native--module-render-session-frame))
|
||||
(error "Native retained frame execution is unavailable"))
|
||||
(let* ((package
|
||||
(let* ((old-package
|
||||
(ebox-native-reflow-session-layout-package session))
|
||||
(candidate-package
|
||||
(or layout-package
|
||||
(if state
|
||||
(ebox-native-reflow--compile-retained-layout-package
|
||||
@ -3907,9 +3968,16 @@ root effect metadata required by a thin host commit."
|
||||
(plist-get
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
:property-templates)))))
|
||||
(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)))
|
||||
(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))
|
||||
(let ((result
|
||||
(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)
|
||||
generation
|
||||
(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)))
|
||||
(setf (ebox-native-reflow-session-generation session) generation
|
||||
(ebox-native-reflow-session-layout-package session) package
|
||||
|
||||
@ -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[] = "10:7:12";
|
||||
static const char version[] = "11:7:12";
|
||||
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
|
||||
}
|
||||
|
||||
|
||||
@ -324,6 +324,10 @@ struct ControlBatch {
|
||||
version: u32,
|
||||
#[serde(default)]
|
||||
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>,
|
||||
}
|
||||
|
||||
@ -382,6 +386,8 @@ enum JobPayload {
|
||||
context_hash: i64,
|
||||
complete: bool,
|
||||
root_metadata: bool,
|
||||
document_base_revision: u64,
|
||||
document_target_revision: u64,
|
||||
},
|
||||
}
|
||||
|
||||
@ -398,6 +404,8 @@ struct BaselineIdentity {
|
||||
#[derive(Clone, Debug)]
|
||||
struct ConfirmedBaseline {
|
||||
identity: BaselineIdentity,
|
||||
document: Arc<LayoutDocument>,
|
||||
document_revision: u64,
|
||||
tape: LayoutTape,
|
||||
styles: Vec<layout::StyleTemplate>,
|
||||
}
|
||||
@ -405,6 +413,8 @@ struct ConfirmedBaseline {
|
||||
#[derive(Debug)]
|
||||
struct PendingBaseline {
|
||||
confirmed_identity: BaselineIdentity,
|
||||
document: Arc<LayoutDocument>,
|
||||
document_revision: u64,
|
||||
tape: LayoutTape,
|
||||
styles: Vec<layout::StyleTemplate>,
|
||||
}
|
||||
@ -450,6 +460,9 @@ struct RuntimeState {
|
||||
baseline_misses: u64,
|
||||
base_renders: u64,
|
||||
target_renders: u64,
|
||||
document_parses: u64,
|
||||
document_validations: u64,
|
||||
document_reuses: u64,
|
||||
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
||||
}
|
||||
|
||||
@ -500,6 +513,9 @@ struct SessionStats {
|
||||
baseline_misses: u64,
|
||||
base_renders: u64,
|
||||
target_renders: u64,
|
||||
document_parses: u64,
|
||||
document_validations: u64,
|
||||
document_reuses: u64,
|
||||
pending_baselines: usize,
|
||||
confirmed_baseline: bool,
|
||||
confirmed_baseline_bytes: usize,
|
||||
@ -562,6 +578,9 @@ impl Session {
|
||||
baseline_misses: 0,
|
||||
base_renders: 0,
|
||||
target_renders: 0,
|
||||
document_parses: 0,
|
||||
document_validations: 0,
|
||||
document_reuses: 0,
|
||||
confirmed_baseline,
|
||||
}),
|
||||
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 {
|
||||
shared,
|
||||
workers: Mutex::new(Some(handles)),
|
||||
layout_document: Mutex::new(None),
|
||||
layout_document: Mutex::new(layout_document),
|
||||
worker_count,
|
||||
}))
|
||||
}
|
||||
@ -623,16 +649,41 @@ impl Session {
|
||||
let layout_requested =
|
||||
batch.document.is_some() || batch.frames.iter().any(|frame| frame.payload.is_none());
|
||||
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 {
|
||||
Some(document) => {
|
||||
document.validate()?;
|
||||
Some(Arc::new(document))
|
||||
}
|
||||
None if layout_requested => Some(
|
||||
self.layout_document
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.clone()
|
||||
confirmed_baseline
|
||||
.as_ref()
|
||||
.map(|baseline| Arc::clone(&baseline.document))
|
||||
.or_else(|| {
|
||||
self.layout_document
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.clone()
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
"Native layout frames require a registered document".to_owned()
|
||||
})?,
|
||||
@ -648,7 +699,12 @@ impl Session {
|
||||
let mut prepared = Vec::with_capacity(batch.frames.len());
|
||||
for frame in batch.frames {
|
||||
let job = if let Some(document) = &document {
|
||||
prepare_layout_job(document, frame)?
|
||||
prepare_layout_job(
|
||||
document,
|
||||
frame,
|
||||
document_base_revision,
|
||||
document_target_revision,
|
||||
)?
|
||||
} else {
|
||||
let payload = frame
|
||||
.payload
|
||||
@ -780,13 +836,75 @@ impl Session {
|
||||
return Err("Native reflow session is closed".to_owned());
|
||||
}
|
||||
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 {
|
||||
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
|
||||
.frames
|
||||
.into_iter()
|
||||
@ -795,15 +913,12 @@ impl Session {
|
||||
if !frame.complete || frame.delay_ms != 0 {
|
||||
return Err("Native retained render requires one immediate complete frame".to_owned());
|
||||
}
|
||||
let document = Arc::new(document);
|
||||
let prepared = prepare_layout_job(&document, frame)?;
|
||||
let confirmed = self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner())
|
||||
.confirmed_baseline
|
||||
.clone();
|
||||
let prepared = prepare_layout_job(
|
||||
&document,
|
||||
frame,
|
||||
document_base_revision,
|
||||
document_target_revision,
|
||||
)?;
|
||||
let output = render_layout_payload(
|
||||
prepared.payload,
|
||||
self.shared.id,
|
||||
@ -828,6 +943,9 @@ 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.pending_baselines.clear();
|
||||
if let Some(pending) = output.pending {
|
||||
state
|
||||
@ -857,6 +975,8 @@ impl Session {
|
||||
identity.runtime_revision = confirmed_revision;
|
||||
state.confirmed_baseline = Some(Arc::new(ConfirmedBaseline {
|
||||
identity,
|
||||
document: pending.document,
|
||||
document_revision: pending.document_revision,
|
||||
tape: pending.tape,
|
||||
styles: pending.styles,
|
||||
}));
|
||||
@ -955,6 +1075,9 @@ impl Session {
|
||||
baseline_misses: state.baseline_misses,
|
||||
base_renders: state.base_renders,
|
||||
target_renders: state.target_renders,
|
||||
document_parses: state.document_parses,
|
||||
document_validations: state.document_validations,
|
||||
document_reuses: state.document_reuses,
|
||||
pending_baselines,
|
||||
confirmed_baseline: state.confirmed_baseline.is_some(),
|
||||
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(
|
||||
baseline: &ConfirmedBaseline,
|
||||
context: LayoutContext,
|
||||
root_width: i64,
|
||||
root_width_override: bool,
|
||||
runtime_revision: u64,
|
||||
context_hash: i64,
|
||||
complete: bool,
|
||||
document_base_revision: u64,
|
||||
identity: &BaselineIdentity,
|
||||
) -> bool {
|
||||
baseline.identity.context == context
|
||||
&& baseline.identity.root_width == root_width
|
||||
&& baseline.identity.root_width_override == root_width_override
|
||||
&& baseline.identity.runtime_revision == runtime_revision
|
||||
&& baseline.identity.context_hash == context_hash
|
||||
&& baseline.identity.complete == complete
|
||||
baseline.document_revision == document_base_revision
|
||||
&& baseline.identity.context == identity.context
|
||||
&& baseline.identity.root_width == identity.root_width
|
||||
&& baseline.identity.root_width_override == identity.root_width_override
|
||||
&& baseline.identity.runtime_revision == identity.runtime_revision
|
||||
&& baseline.identity.context_hash == identity.context_hash
|
||||
&& baseline.identity.complete == identity.complete
|
||||
}
|
||||
|
||||
fn prepare_layout_job(
|
||||
document: &Arc<LayoutDocument>,
|
||||
frame: ControlFrame,
|
||||
document_base_revision: u64,
|
||||
document_target_revision: u64,
|
||||
) -> Result<PreparedJob, String> {
|
||||
if frame.payload.is_some() {
|
||||
return Err(format!(
|
||||
@ -1143,6 +1265,8 @@ fn prepare_layout_job(
|
||||
context_hash: frame.context_hash,
|
||||
complete: frame.complete,
|
||||
root_metadata: frame.root_metadata,
|
||||
document_base_revision,
|
||||
document_target_revision,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -1176,6 +1300,8 @@ fn render_layout_payload(
|
||||
context_hash,
|
||||
complete,
|
||||
root_metadata,
|
||||
document_base_revision,
|
||||
document_target_revision,
|
||||
} => {
|
||||
let identity = TapeIdentity {
|
||||
session_id,
|
||||
@ -1208,17 +1334,21 @@ fn render_layout_payload(
|
||||
type LayoutRenderOutcome = Result<(Vec<u8>, LayoutTape, bool, u64, u64), String>;
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> LayoutRenderOutcome {
|
||||
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
|
||||
.as_ref()
|
||||
.filter(|baseline| {
|
||||
confirmed_baseline_matches(
|
||||
baseline,
|
||||
base_context,
|
||||
base_root_width,
|
||||
base_root_width_override,
|
||||
runtime_revision,
|
||||
context_hash,
|
||||
complete,
|
||||
document_base_revision,
|
||||
&base_identity,
|
||||
) && baseline.styles == document.styles
|
||||
})
|
||||
.cloned();
|
||||
@ -1275,6 +1405,8 @@ fn render_layout_payload(
|
||||
bytes,
|
||||
pending: Some(PendingBaseline {
|
||||
confirmed_identity: pending_identity,
|
||||
document: Arc::clone(&document),
|
||||
document_revision: document_target_revision,
|
||||
tape,
|
||||
styles: document.styles.clone(),
|
||||
}),
|
||||
@ -1329,7 +1461,7 @@ 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)?;
|
||||
let prepared = prepare_layout_job(&Arc::new(document), frame, 0, 1)?;
|
||||
let output = render_layout_payload(
|
||||
prepared.payload,
|
||||
SYNC_RENDER_SESSION_ID,
|
||||
@ -1847,14 +1979,41 @@ mod tests {
|
||||
|
||||
fn proof_layout_payload(frames: &str) -> Vec<u8> {
|
||||
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()
|
||||
}
|
||||
|
||||
fn column_proof_layout_payload(frames: &str) -> Vec<u8> {
|
||||
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()
|
||||
}
|
||||
@ -2083,10 +2242,11 @@ mod tests {
|
||||
assert_eq!(child_initial.ready_results, 0);
|
||||
assert_eq!(child_initial.pending_baselines, 0);
|
||||
assert!(child_initial.confirmed_baseline);
|
||||
assert!(!child_initial.layout_registered);
|
||||
assert!(child_initial.layout_registered);
|
||||
|
||||
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}]"#,
|
||||
);
|
||||
let second_tape = child.render_sync(1, &second).unwrap();
|
||||
@ -2110,6 +2270,152 @@ mod tests {
|
||||
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]
|
||||
fn confirmed_fork_falls_back_to_full_frame_on_identity_mismatch() {
|
||||
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||
@ -2120,7 +2426,9 @@ mod tests {
|
||||
assert!(parent.confirm(1, 1, 1).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}]"#,
|
||||
);
|
||||
let tape = child.render_sync(1, &mismatched).unwrap();
|
||||
|
||||
@ -2399,6 +2399,174 @@ remain retained identities."
|
||||
(equal '(:foreground "#222222")
|
||||
(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)
|
||||
|
||||
;;; ebox-commit-tests.el ends here
|
||||
|
||||
@ -1500,6 +1500,11 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(should (ebox-native-reflow-session-released-p session))
|
||||
(should-not (ebox-native-reflow-session-released-p
|
||||
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)
|
||||
:native-sync-confirmed-p))
|
||||
(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
|
||||
(plist-get (tp-surface-client-state surface)
|
||||
: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
|
||||
winning-session)))
|
||||
(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 (ebox-native-reflow-session-released-p winning-session))
|
||||
(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)
|
||||
(kill-buffer buffer))
|
||||
(when next-session
|
||||
|
||||
Loading…
Reference in New Issue
Block a user