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 76bce0bb8d
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`.
- 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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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));
}

View File

@ -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();

View File

@ -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

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-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