perf: preserve native patch locality when styles append

This commit is contained in:
Kinneyzhang 2026-09-05 07:43:02 +08:00
parent 76bce0bb8d
commit 13e360a1a5
3 changed files with 231 additions and 2 deletions

View File

@ -1248,6 +1248,13 @@ impl StyleTemplate {
}
}
pub(crate) fn style_registry_extends_exact_prefix(
old: &[StyleTemplate],
target: &[StyleTemplate],
) -> bool {
target.starts_with(old)
}
fn composed_face(
properties: &AtomProperties,
styles: &[CompiledStyle],
@ -2269,6 +2276,17 @@ fn root_metadata_payload(
}))
}
fn validate_tape_style_ids(properties: &AtomProperties, style_count: u32) -> Result<(), String> {
if properties
.style_ids
.iter()
.any(|style_id| *style_id >= style_count)
{
return Err("Native layout tape has invalid style id".to_owned());
}
Ok(())
}
fn flatten_layout_tape(tape: LayoutTape, complete: bool) -> Result<FlatLayoutTape, String> {
if tape.lines.is_empty() {
return Err("Native layout tape has no lines".to_owned());
@ -2289,6 +2307,7 @@ fn flatten_layout_tape(tape: LayoutTape, complete: bool) -> Result<FlatLayoutTap
width,
properties,
} => {
validate_tape_style_ids(&properties, tape.style_count)?;
let width = tape_width(width, "text width")?;
if text.is_empty() || text.contains('\n') {
return Err("Native layout tape has invalid text atom".to_owned());
@ -2304,6 +2323,7 @@ fn flatten_layout_tape(tape: LayoutTape, complete: bool) -> Result<FlatLayoutTap
.ok_or_else(|| "Native layout tape width overflow".to_owned())?;
}
TapeAtom::Space { width, properties } => {
validate_tape_style_ids(&properties, tape.style_count)?;
let width = tape_width(width, "space width")?;
if width == 0 {
return Err("Native layout tape has empty pixel space".to_owned());
@ -2323,6 +2343,7 @@ fn flatten_layout_tape(tape: LayoutTape, complete: bool) -> Result<FlatLayoutTap
return Err("Native layout tape line width invariant failed".to_owned());
}
if let Some(properties) = line.break_after {
validate_tape_style_ids(&properties, tape.style_count)?;
characters.push(TapeCharacter {
value: '\n',
pixel_width: None,
@ -2616,7 +2637,7 @@ pub(crate) fn encode_layout_patch_tape(
) -> Result<Vec<u8>, String> {
let old = flatten_layout_tape(old_tape, identity.complete)?;
let target = flatten_layout_tape(target_tape, identity.complete)?;
if old.style_count != target.style_count || target.style_count as usize != styles.len() {
if old.style_count > target.style_count || target.style_count as usize != styles.len() {
return Err("Native layout patch style table mismatch".to_owned());
}
let compiled_styles = styles
@ -5922,6 +5943,10 @@ mod tests {
serde_json::from_str(json).unwrap()
}
fn styles(json: &str) -> Vec<StyleTemplate> {
serde_json::from_str(json).unwrap()
}
fn cluster(text: &str, width: i64, source_template_id: Option<u32>) -> MeasuredCluster {
MeasuredCluster {
text: text.to_owned(),
@ -6115,6 +6140,77 @@ mod tests {
}
}
#[test]
fn style_registry_reuse_requires_an_exact_stable_prefix() {
let old = styles(
r##"[{"mode":"set","face":{"foreground":"#111111"}},{"mode":"add","face":{"background":"#222222"}}]"##,
);
let appended = styles(
r##"[{"mode":"set","face":{"foreground":"#111111"}},{"mode":"add","face":{"background":"#222222"}},{"mode":"set","face":{"foreground":"#333333"}}]"##,
);
let changed = styles(
r##"[{"mode":"set","face":{"foreground":"#999999"}},{"mode":"add","face":{"background":"#222222"}},{"mode":"set","face":{"foreground":"#333333"}}]"##,
);
let removed = styles(r##"[{"mode":"set","face":{"foreground":"#111111"}}]"##);
let renumbered = styles(
r##"[{"mode":"add","face":{"background":"#222222"}},{"mode":"set","face":{"foreground":"#111111"}},{"mode":"set","face":{"foreground":"#333333"}}]"##,
);
assert!(style_registry_extends_exact_prefix(&old, &appended));
assert!(!style_registry_extends_exact_prefix(&old, &changed));
assert!(!style_registry_extends_exact_prefix(&old, &removed));
assert!(!style_registry_extends_exact_prefix(&old, &renumbered));
}
#[test]
fn layout_patch_accepts_appended_styles_but_validates_each_tape_count() {
fn tape(style_count: u32, style_id: u32) -> LayoutTape {
LayoutTape {
style_count,
lines: vec![TapeLine {
width: 1,
atoms: vec![TapeAtom::Text {
text: "x".to_owned(),
width: 1,
properties: AtomProperties {
style_ids: vec![style_id],
..AtomProperties::default()
},
}],
break_after: None,
}],
}
}
let registry = styles(
r##"[{"mode":"set","face":{"foreground":"#111111"}},{"mode":"set","face":{"background":"#222222"}}]"##,
);
let encoded = encode_layout_patch_tape(
tape(1, 0),
tape(2, 1),
&registry,
complete_identity(),
false,
4096,
)
.unwrap();
assert_ne!(
u16::from_le_bytes(encoded[6..8].try_into().unwrap()) & (1 << 2),
0
);
let error = encode_layout_patch_tape(
tape(1, 1),
tape(2, 1),
&registry,
complete_identity(),
false,
4096,
)
.unwrap_err();
assert_eq!(error, "Native layout tape has invalid style id");
}
#[test]
fn typed_column_lowering_stretches_centered_child() {
let mut text = auto_text_box(3, measured_text(vec![vec![cluster("x", 10, None)]]), None);

View File

@ -1349,7 +1349,10 @@ fn render_layout_payload(
baseline,
document_base_revision,
&base_identity,
) && baseline.styles == document.styles
) && layout::style_registry_extends_exact_prefix(
&baseline.styles,
&document.styles,
)
})
.cloned();
if require_confirmed_patch_base && base_hit.is_none() {
@ -2011,6 +2014,35 @@ mod tests {
.into_bytes()
}
fn styled_layout_payload(
document_base_revision: u64,
document_target_revision: u64,
style_count: u32,
styles: &str,
foreground_style: u32,
frames: &str,
) -> Vec<u8> {
let payload = String::from_utf8(replacement_layout_payload(
document_base_revision,
document_target_revision,
frames,
))
.unwrap();
payload
.replacen(
r#""style-count":0"#,
&format!(r#""style-count":{style_count}"#),
1,
)
.replacen(r#""styles":[]"#, &format!(r#""styles":{styles}"#), 1)
.replacen(
r#""foreground-style":null"#,
&format!(r#""foreground-style":{foreground_style}"#),
1,
)
.into_bytes()
}
fn column_proof_layout_payload(frames: &str) -> Vec<u8> {
format!(
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}}}"#
@ -2440,6 +2472,55 @@ mod tests {
child.stop(true);
}
#[test]
fn confirmed_patch_base_accepts_only_append_stable_style_registries() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = styled_layout_payload(
0,
1,
1,
r##"[{"mode":"set","face":{"foreground":"#111111"}}]"##,
0,
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"root-width":120,"runtime-revision":0,"context-hash":77}]"#,
);
assert!(!tape_patch_p(&parent.render_sync(1, &first).unwrap()));
assert!(parent.confirm(1, 1, 1).unwrap());
let appended_child = parent.fork_confirmed().unwrap();
let appended = styled_layout_payload(
1,
2,
2,
r##"[{"mode":"set","face":{"foreground":"#111111"}},{"mode":"set","face":{"background":"#222222"}}]"##,
1,
r#"[{"key":2,"viewport-width":120,"viewport-height":10,"root-width":120,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":1,"context-hash":77}]"#,
);
assert!(tape_patch_p(
&appended_child.render_sync(1, &appended).unwrap()
));
assert_eq!(appended_child.stats().baseline_hits, 1);
assert_eq!(appended_child.stats().base_renders, 0);
let changed_child = parent.fork_confirmed().unwrap();
let changed = styled_layout_payload(
1,
2,
2,
r##"[{"mode":"set","face":{"foreground":"#999999"}},{"mode":"set","face":{"background":"#222222"}}]"##,
1,
r#"[{"key":3,"viewport-width":120,"viewport-height":10,"root-width":120,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":1,"context-hash":77}]"#,
);
assert!(!tape_patch_p(
&changed_child.render_sync(1, &changed).unwrap()
));
assert_eq!(changed_child.stats().baseline_hits, 0);
assert_eq!(changed_child.stats().base_renders, 0);
parent.stop(true);
appended_child.stop(true);
changed_child.stop(true);
}
#[test]
fn synchronous_proof_render_returns_full_tape_without_session() {
let payload =

View File

@ -1673,6 +1673,58 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(when next-session
(should (ebox-native-reflow-session-released-p next-session))))))
(ert-deftest ebox-native-appended-style-keeps-a-local-property-patch ()
"An appended style preserves native and TP locality with exact output."
(skip-unless (ebox-native-reflow-layout-ready-p))
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-native-appended-style*")))
(unwind-protect
(cl-labels
((source
(background)
(apply
#'ebox-test-column
(cl-loop
for index below 32
collect
(apply
#'ebox-test-box
(append
(list :key index :source-identity index
(ebox-test-text (char-to-string (+ ?A index))))
(and (= index 16) background
(list :bgcolor background)))))))
(node-ids
(state)
(let (ids)
(ebox-surface-test--walk-runtime
(plist-get state :root-node)
(lambda (node) (push (plist-get node :node-id) ids)))
(nreverse ids))))
(let ((ebox-viewport-width 240)
(ebox-viewport-height 40)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm nil))
(ebox-render-to-buffer buffer (source nil))
(let* ((surface
(with-current-buffer buffer ebox-surface--buffer-surface))
(old-state (tp-surface-client-state surface))
(old-node-ids (node-ids old-state))
(report (ebox-commit buffer (source "#224466")))
(state (tp-surface-client-state surface))
(tp-report (tp-surface-report surface))
(expected (ebox-surface-test--render-runtime state))
(actual
(with-current-buffer buffer
(buffer-substring (point-min) (point-max)))))
(should (eq (plist-get report :projection-kind) 'native-frame))
(should (eq (plist-get report :native-frame-kind) 'patch))
(should (= (plist-get tp-report :touched-characters) 2))
(should (equal (node-ids state) old-node-ids))
(should (equal-including-properties actual expected)))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-viewport-reflow-retains-final-sized-flex-child-fragments ()
"Viewport reflow should reuse final-sized Flex child fragments exactly."
(ebox-surface-test--reset-render-state)