diff --git a/Cargo.lock b/Cargo.lock index 186a5d3660..f3d43b3270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2993,12 +2993,14 @@ dependencies = [ "kurbo", "moli-image", "moli-system-fonts", + "moli-trace", "parley", "read-fonts", "stylo", "stylo_taffy", "taffy", "thiserror", + "tracing", "wuff", ] diff --git a/moli-layout/Cargo.toml b/moli-layout/Cargo.toml index b968c42f48..5de7cb761e 100644 --- a/moli-layout/Cargo.toml +++ b/moli-layout/Cargo.toml @@ -10,12 +10,14 @@ euclid = "=0.22.14" kurbo = "=0.13.1" moli-image = { path = "../moli-image" } moli-system-fonts = { path = "../moli-system-fonts" } +moli-trace = { path = "../moli-trace" } parley = "=0.11.0" read-fonts = "0.41.0" stylo = "0.20" stylo_taffy = { git = "https://github.com/DioxusLabs/blitz", rev = "5081c65811a4396f5a99b2e0aca542a4a4a6606f", default-features = false, features = ["std", "block", "flexbox", "grid", "floats"] } taffy = { version = "=0.13.0", default-features = false, features = ["std", "block_layout", "flexbox", "grid", "float_layout", "content_size", "calc", "detailed_layout_info"] } thiserror = "2.0.17" +tracing = "0.1.41" wuff = { version = "0.2.9", default-features = false, features = ["z"] } [lints] diff --git a/moli-layout/examples/layout-cost.rs b/moli-layout/examples/layout-cost.rs new file mode 100644 index 0000000000..2042343efc --- /dev/null +++ b/moli-layout/examples/layout-cost.rs @@ -0,0 +1,346 @@ +//! Linux layout-backend cost probe. Compare release builds with identical inputs. +//! This measures layout and immutable geometry, not raster/encoding or protocol. + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + io::{self, Write}, + time::Instant, +}; + +use moli_layout::{ + DocumentLayoutServices, LayoutElementCategory, LayoutElementSemantics, LayoutError, + LayoutFlushReason, LayoutNamespace, LayoutPassRequest, LayoutPassResult, LayoutQuery, + LayoutQueryBatch, LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTableRole, + LayoutViewport, PaintColor, ResolvedLayoutElementStyles, ResolvedLayoutStyle, SystemFontPolicy, + WebFontFace, WebFontRegistration, build_layout_pass, +}; +use style::{ + Atom, + properties::ComputedValues, + values::{ + computed::{ + CSSPixelLength, LengthPercentage, NonNegativeLengthPercentage, + font::{ + FamilyName, FontFamily, FontFamilyList, FontFamilyNameSyntax, SingleFontFamily, + }, + length::Size as ComputedSize, + }, + generics::NonNegative, + specified::box_::Display, + }, +}; +use taffy::{Dimension, Size, Style, style_helpers::length}; + +struct Node { + parent: Option, + children: Vec, + text: Option, + tag: &'static str, + role: LayoutElementCategory, +} + +struct Source(Vec); + +impl LayoutSource for Source { + type NodeId = usize; + type ChildIter<'a> = std::iter::Copied>; + + fn root(&self) -> usize { + 0 + } + fn flat_parent(&self, node: usize) -> Option { + self.0[node].parent + } + fn flat_children(&self, node: usize) -> Self::ChildIter<'_> { + self.0[node].children.iter().copied() + } + fn node_kind(&self, node: usize) -> LayoutSourceKind { + if self.0[node].text.is_some() { + LayoutSourceKind::Text + } else { + LayoutSourceKind::Element + } + } + fn text(&self, node: usize) -> Option<&str> { + self.0[node].text.as_deref() + } + fn label(&self, node: usize) -> String { + format!("{}-{node}", self.0[node].tag) + } + fn element_semantics(&self, node: usize) -> Option { + self.0[node].text.is_none().then(|| { + LayoutElementSemantics::new( + LayoutNamespace::Html, + self.0[node].tag, + self.0[node].role, + None, + ) + }) + } +} + +struct Styles(Vec>); + +impl LayoutStyleResolver for Styles { + fn element_styles( + &mut self, + node: usize, + ) -> Result, LayoutError> { + Ok(self.0[node] + .clone() + .map(ResolvedLayoutElementStyles::from_primary)) + } +} + +fn css_length(value: f32) -> NonNegativeLengthPercentage { + NonNegative(LengthPercentage::new_length(CSSPixelLength::new(value))) +} + +fn text_style(base: &ComputedValues, display: Display, width: f32) -> ResolvedLayoutStyle { + let mut computed = base.clone(); + computed.mutate_box().set_display(display); + computed + .mutate_position() + .set_width(ComputedSize::LengthPercentage(css_length(width))); + computed + .mutate_position() + .set_height(ComputedSize::LengthPercentage(css_length(32.0))); + ResolvedLayoutStyle::from_stylo(style::servo_arc::Arc::new(computed)) +} + +fn append_node( + source: &mut Source, + styles: &mut Styles, + parent: Option, + tag: &'static str, + role: LayoutElementCategory, + style: Option, + text: Option, +) -> usize { + let id = source.0.len(); + source.0.push(Node { + parent, + children: Vec::new(), + text, + tag, + role, + }); + styles.0.push(style); + if let Some(parent) = parent { + source.0[parent].children.push(id); + } + id +} + +fn fixture( + container: &str, + unique: bool, + count: usize, + font: &ComputedValues, +) -> (Source, Styles, Vec) { + use moli_layout::LayoutDisplay; + let mut source = Source(Vec::new()); + let mut styles = Styles(Vec::new()); + let display = match container { + "blocks" => LayoutDisplay::Block, + "grid" => LayoutDisplay::Grid, + "table" => LayoutDisplay::Table, + _ => unreachable!(), + }; + let root_style = ResolvedLayoutStyle::synthetic( + display, + Style { + size: Size { + width: Dimension::length(640.0), + height: Dimension::auto(), + }, + grid_template_columns: vec![length(160.0); 4], + ..Style::default() + }, + PaintColor::TRANSPARENT, + ); + let role = if container == "table" { + LayoutElementCategory::Table(LayoutTableRole::Table) + } else { + LayoutElementCategory::Generic + }; + let root = append_node( + &mut source, + &mut styles, + None, + if container == "table" { "table" } else { "div" }, + role, + Some(root_style), + None, + ); + let mut row = root; + let mut cells = Vec::new(); + for index in 0..count { + if container == "table" && index % 4 == 0 { + row = append_node( + &mut source, + &mut styles, + Some(root), + "tr", + LayoutElementCategory::Table(LayoutTableRole::Row), + Some(ResolvedLayoutStyle::synthetic( + LayoutDisplay::TableRow, + Style::default(), + PaintColor::TRANSPARENT, + )), + None, + ); + } + let cell = append_node( + &mut source, + &mut styles, + Some(row), + if container == "table" { "td" } else { "div" }, + if container == "table" { + LayoutElementCategory::Table(LayoutTableRole::Cell) + } else { + LayoutElementCategory::Generic + }, + Some(text_style( + font, + if container == "table" { + Display::TableCell + } else { + Display::Block + }, + 100.0, + )), + None, + ); + append_node( + &mut source, + &mut styles, + Some(cell), + "#text", + LayoutElementCategory::Generic, + None, + Some(if unique { + format!("Item {index:04} text") + } else { + "Alpha Beta Gamma".to_owned() + }), + ); + cells.push(cell); + } + (source, styles, cells) +} + +fn cpu_ns() -> Result> { + Ok(std::fs::read_to_string("/proc/thread-self/schedstat")? + .split_whitespace() + .next() + .ok_or("missing thread CPU counter")? + .parse()?) +} + +fn fingerprint(pass: &LayoutPassResult, cells: &[usize]) -> u64 { + let mut hash = DefaultHasher::new(); + for &source in cells { + let batch = LayoutQueryBatch::new(vec![LayoutQuery::ElementMetrics { source }]); + format!("{:?}", pass.answer_queries(&batch).answers).hash(&mut hash); + let text_rects = pass.text_range_rects(source + 1, 0..usize::MAX); + assert!( + !text_rects.is_empty(), + "text must have actual font geometry" + ); + format!("{text_rects:?}").hash(&mut hash); + } + hash.finish() +} + +fn main() -> Result<(), Box> { + let args = std::env::args().skip(1).collect::>(); + if args.len() != 4 { + return Err( + "usage: layout-cost ".into(), + ); + } + let container = args[0].as_str(); + if !["blocks", "grid", "table"].contains(&container) + || !["repeated", "unique"].contains(&args[1].as_str()) + { + return Err("unknown container or text workload".into()); + } + let count: usize = args[2].parse()?; + let iterations: usize = args[3].parse()?; + if count == 0 || iterations == 0 { + return Err("nodes and iterations must be positive".into()); + } + let mut font = style::properties::style_structs::Font::initial_values(); + font.set_font_family(FontFamily { + families: FontFamilyList { + list: style::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName( + FamilyName { + name: Atom::from("Moli Ahem"), + syntax: FontFamilyNameSyntax::Quoted, + }, + ))), + }, + is_system_font: false, + is_initial: false, + }); + let font = ComputedValues::initial_values_with_font_override(font); + let mut services = DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled); + services.register_web_font(WebFontRegistration::new( + "fixed", + WebFontFace::new("Moli Ahem"), + include_bytes!("../tests/fixtures/moli-ahem.ttf").to_vec(), + ))?; + let (source, mut styles, cells) = fixture(container, args[1] == "unique", count, &font); + let request = LayoutPassRequest::new( + LayoutViewport::new(1280, 720, 1.0), + LayoutFlushReason::Explicit, + ); + let mut last = build_layout_pass(&source, &mut styles, &mut services, request)?; + println!( + "{{\"phase\":\"ready\",\"nodes\":{count},\"iterations\":{iterations},\"fingerprint\":{},\"box_slot_bytes\":{},\"taffy_cache_bytes\":{}}}", + fingerprint(&last, &cells), + std::mem::size_of::>(), + std::mem::size_of::() + ); + io::stdout().flush()?; + let mut control = String::new(); + io::stdin().read_line(&mut control)?; + if control.trim() != "go" { + return Err("expected go on stdin".into()); + } + for step in 0..iterations { + styles.0[cells[0]] = Some(text_style( + &font, + if container == "table" { + Display::TableCell + } else { + Display::Block + }, + 120.0 + (step % 7) as f32 * 5.0, + )); + let before_cpu = cpu_ns()?; + let started = Instant::now(); + let pass = build_layout_pass(&source, &mut styles, &mut services, request)?; + let wall_ns = started.elapsed().as_nanos(); + let after_cpu = cpu_ns()?; + last = pass; + println!( + "{{\"phase\":\"pass\",\"step\":{step},\"wall_ns\":{wall_ns},\"cpu_before_ns\":{before_cpu},\"cpu_after_ns\":{after_cpu},\"boxes\":{},\"fragments\":{}}}", + last.metrics.box_count, last.metrics.fragment_count + ); + } + println!( + "{{\"phase\":\"done\",\"fingerprint\":{},\"font_count\":{}}}", + fingerprint(&last, &cells), + services.web_font_count() + ); + io::stdout().flush()?; + control.clear(); + io::stdin().read_line(&mut control)?; + if control.trim() != "exit" { + return Err("expected exit on stdin".into()); + } + Ok(()) +} diff --git a/moli-layout/src/builder.rs b/moli-layout/src/builder.rs index 386760fca6..db4b60bde7 100644 --- a/moli-layout/src/builder.rs +++ b/moli-layout/src/builder.rs @@ -101,6 +101,7 @@ where } fn build(mut self) -> Result, LayoutError> { + let profile_started = moli_trace::cpu_profile_enabled().then(std::time::Instant::now); let source_root = self.source.root(); let source_label = self.source.label(source_root); if let Some(parent) = self.source.flat_parent(source_root) { @@ -185,9 +186,28 @@ where )?; } + let construction_finished = profile_started.map(|_| std::time::Instant::now()); + let allocated_box_count = world.boxes.len(); world.compact_reachable(); self.finalize_viewport_scroll_policy(&mut world); + let finalization_finished = profile_started.map(|_| std::time::Instant::now()); world.validate_invariants()?; + if let (Some(started), Some(constructed), Some(finalized)) = ( + profile_started, + construction_finished, + finalization_finished, + ) { + tracing::info!( + target: "moli_cpu_profile", + stage = "layout_box_construction", + root = ?source_root, + construction_us = constructed.duration_since(started).as_micros(), + finalization_us = finalized.duration_since(constructed).as_micros(), + validation_us = finalized.elapsed().as_micros(), + allocated_box_count, + compacted_box_count = world.boxes.len(), + ); + } world.css_image_references = self.css_image_references; Ok(world) } diff --git a/moli-layout/src/inline.rs b/moli-layout/src/inline.rs index c3c122891c..f28ac95c24 100644 --- a/moli-layout/src/inline.rs +++ b/moli-layout/src/inline.rs @@ -1942,6 +1942,11 @@ pub(crate) fn prepare_inline_contexts( ) where N: Copy + Debug + Eq + Hash, { + let profile_started = moli_trace::cpu_profile_enabled().then(std::time::Instant::now); + let mut collection_elapsed = std::time::Duration::ZERO; + let mut preparation_elapsed = std::time::Duration::ZERO; + let mut prepared_context_count = 0usize; + let mut text_unit_count = 0usize; for layout_box in &mut world.boxes { layout_box.inline_layout = None; layout_box.inline_context_owner = None; @@ -1960,21 +1965,43 @@ pub(crate) fn prepare_inline_contexts( if world.boxes[owner.index()].inline_flattened { continue; } + let collection_started = profile_started.map(|_| std::time::Instant::now()); let input = collect_inline_input(world, owner); + if let Some(started) = collection_started { + collection_elapsed += started.elapsed(); + text_unit_count += input.units.len(); + } if input.units.is_empty() && input.objects.is_empty() && world.boxes[owner.index()].text_selection.is_none() { continue; } + let preparation_started = profile_started.map(|_| std::time::Instant::now()); let parley = services.parley_mut(); let context = input.build(world, parley); + if let Some(started) = preparation_started { + preparation_elapsed += started.elapsed(); + prepared_context_count += 1; + } world.boxes[owner.index()].inline_layout = Some(context); initialized = true; } if initialized { services.text_layout_passes = services.text_layout_passes.saturating_add(1); } + if let Some(started) = profile_started { + tracing::info!( + target: "moli_cpu_profile", + stage = "layout_inline_preparation", + root = ?world.boxes[world.root().index()].source(), + total_us = started.elapsed().as_micros(), + collection_us = collection_elapsed.as_micros(), + preparation_us = preparation_elapsed.as_micros(), + prepared_context_count, + text_unit_count, + ); + } } struct InlineBuildInput { diff --git a/moli-renderer-v8/src/layout_renderer/mod.rs b/moli-renderer-v8/src/layout_renderer/mod.rs index 68f6b925a9..b6463a50a4 100644 --- a/moli-renderer-v8/src/layout_renderer/mod.rs +++ b/moli-renderer-v8/src/layout_renderer/mod.rs @@ -45,6 +45,7 @@ fn build_native_layout_pass_recursive( request: LayoutPassRequest, document_stack: &mut Vec, ) -> Result, moli_layout::LayoutError> { + let profile_started = moli_trace::cpu_profile_enabled().then(std::time::Instant::now); let document = runtime .dom_host() .owner_document_handle(root) @@ -58,8 +59,10 @@ fn build_native_layout_pass_recursive( root, request.requests_paint(), ); + let source_ready = profile_started.map(|_| std::time::Instant::now()); let mut styles = style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, request.viewport); + let styles_ready = profile_started.map(|_| std::time::Instant::now()); let result = { let mut frames = NativeEmbeddedFrameRenderer { runtime, @@ -71,6 +74,24 @@ fn build_native_layout_pass_recursive( }; build_layout_pass_with_embedded_frames(&source, &mut styles, services, request, &mut frames) }; + if let (Some(started), Some(source_ready), Some(styles_ready)) = + (profile_started, source_ready, styles_ready) + { + let finished = std::time::Instant::now(); + tracing::info!( + target: "moli_cpu_profile", + stage = "layout_native_adapter", + document = document.index_u32(), + root = ?root, + reason = ?request.reason, + total_us = finished.duration_since(started).as_micros(), + source_setup_us = source_ready.duration_since(started).as_micros(), + style_setup_us = styles_ready.duration_since(source_ready).as_micros(), + backend_us = finished.duration_since(styles_ready).as_micros(), + backend_reported_us = result.as_ref().map(|pass| pass.metrics.elapsed.as_micros()).ok(), + success = result.is_ok(), + ); + } styles.trace_profile(document); document_stack.pop(); result