perf: preserve native patch locality when styles append
This commit is contained in:
parent
9767c30208
commit
bc4516e171
@ -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),
|
||||
®istry,
|
||||
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),
|
||||
®istry,
|
||||
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);
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user