Perf profiling and some low hanging perf improvements in text rendering
This commit is contained in:
@@ -36,6 +36,35 @@ pub struct LayoutSnapshot {
|
||||
pub root_min_size: UiSize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct LayoutFrameStats {
|
||||
pub total_ms: f64,
|
||||
pub nodes: usize,
|
||||
pub text_nodes: usize,
|
||||
pub container_nodes: usize,
|
||||
pub background_quads: usize,
|
||||
pub intrinsic_calls: usize,
|
||||
pub intrinsic_size_calls: usize,
|
||||
pub intrinsic_text_calls: usize,
|
||||
pub intrinsic_container_calls: usize,
|
||||
pub intrinsic_ms: f64,
|
||||
pub text_prepare_calls: usize,
|
||||
pub text_prepare_ms: f64,
|
||||
pub viewport_culled: usize,
|
||||
pub layout_cache_hits: usize,
|
||||
pub layout_cache_misses: usize,
|
||||
pub intrinsic_cache_hits: usize,
|
||||
pub text_requests: u32,
|
||||
pub text_cache_hits: u32,
|
||||
pub text_cache_misses: u32,
|
||||
pub text_output_glyphs: u32,
|
||||
pub text_family_resolve_ms: f64,
|
||||
pub text_buffer_build_ms: f64,
|
||||
pub text_glyph_collect_ms: f64,
|
||||
pub text_miss_ms: f64,
|
||||
pub scene_items: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct LayoutPath(Vec<u32>);
|
||||
|
||||
@@ -194,6 +223,16 @@ pub fn layout_snapshot_with_cache(
|
||||
text_system: &mut TextSystem,
|
||||
layout_cache: &mut LayoutCache,
|
||||
) -> LayoutSnapshot {
|
||||
layout_snapshot_with_cache_and_stats(version, logical_size, root, text_system, layout_cache).0
|
||||
}
|
||||
|
||||
pub fn layout_snapshot_with_cache_and_stats(
|
||||
version: u64,
|
||||
logical_size: UiSize,
|
||||
root: &Element,
|
||||
text_system: &mut TextSystem,
|
||||
layout_cache: &mut LayoutCache,
|
||||
) -> (LayoutSnapshot, LayoutFrameStats) {
|
||||
let layout_started = Instant::now();
|
||||
let perf_enabled = tracing::enabled!(target: "ruin_ui::layout_perf", tracing::Level::DEBUG);
|
||||
let mut perf_stats = LayoutPerfStats::new(perf_enabled);
|
||||
@@ -215,48 +254,78 @@ pub fn layout_snapshot_with_cache(
|
||||
None,
|
||||
);
|
||||
let text_stats = text_system.take_frame_stats();
|
||||
let stats = LayoutFrameStats {
|
||||
total_ms: layout_started.elapsed().as_secs_f64() * 1_000.0,
|
||||
nodes: perf_stats.nodes,
|
||||
text_nodes: perf_stats.text_nodes,
|
||||
container_nodes: perf_stats.container_nodes,
|
||||
background_quads: perf_stats.background_quads,
|
||||
intrinsic_calls: perf_stats.intrinsic_calls,
|
||||
intrinsic_size_calls: perf_stats.intrinsic_size_calls,
|
||||
intrinsic_text_calls: perf_stats.intrinsic_text_calls,
|
||||
intrinsic_container_calls: perf_stats.intrinsic_container_calls,
|
||||
intrinsic_ms: perf_stats.intrinsic_ms,
|
||||
text_prepare_calls: perf_stats.text_prepare_calls,
|
||||
text_prepare_ms: perf_stats.text_prepare_ms,
|
||||
viewport_culled: perf_stats.viewport_culled,
|
||||
layout_cache_hits: perf_stats.layout_cache_hits,
|
||||
layout_cache_misses: perf_stats.layout_cache_misses,
|
||||
intrinsic_cache_hits: perf_stats.intrinsic_cache_hits,
|
||||
text_requests: text_stats.requests,
|
||||
text_cache_hits: text_stats.cache_hits,
|
||||
text_cache_misses: text_stats.cache_misses,
|
||||
text_output_glyphs: text_stats.output_glyphs,
|
||||
text_family_resolve_ms: text_stats.family_resolve_ms,
|
||||
text_buffer_build_ms: text_stats.buffer_build_ms,
|
||||
text_glyph_collect_ms: text_stats.glyph_collect_ms,
|
||||
text_miss_ms: text_stats.miss_ms,
|
||||
scene_items: scene.items.len(),
|
||||
};
|
||||
if perf_stats.enabled {
|
||||
tracing::debug!(
|
||||
target: "ruin_ui::layout_perf",
|
||||
scene_version = version,
|
||||
width = logical_size.width,
|
||||
height = logical_size.height,
|
||||
total_ms = layout_started.elapsed().as_secs_f64() * 1_000.0,
|
||||
nodes = perf_stats.nodes,
|
||||
text_nodes = perf_stats.text_nodes,
|
||||
container_nodes = perf_stats.container_nodes,
|
||||
background_quads = perf_stats.background_quads,
|
||||
intrinsic_calls = perf_stats.intrinsic_calls,
|
||||
intrinsic_size_calls = perf_stats.intrinsic_size_calls,
|
||||
intrinsic_text_calls = perf_stats.intrinsic_text_calls,
|
||||
intrinsic_container_calls = perf_stats.intrinsic_container_calls,
|
||||
intrinsic_ms = perf_stats.intrinsic_ms,
|
||||
text_prepare_calls = perf_stats.text_prepare_calls,
|
||||
text_prepare_ms = perf_stats.text_prepare_ms,
|
||||
viewport_culled = perf_stats.viewport_culled,
|
||||
layout_cache_hits = perf_stats.layout_cache_hits,
|
||||
layout_cache_misses = perf_stats.layout_cache_misses,
|
||||
intrinsic_cache_hits = perf_stats.intrinsic_cache_hits,
|
||||
text_requests = text_stats.requests,
|
||||
text_cache_hits = text_stats.cache_hits,
|
||||
text_cache_misses = text_stats.cache_misses,
|
||||
text_output_glyphs = text_stats.output_glyphs,
|
||||
text_family_resolve_ms = text_stats.family_resolve_ms,
|
||||
text_buffer_build_ms = text_stats.buffer_build_ms,
|
||||
text_glyph_collect_ms = text_stats.glyph_collect_ms,
|
||||
text_miss_ms = text_stats.miss_ms,
|
||||
scene_items = scene.items.len(),
|
||||
total_ms = stats.total_ms,
|
||||
nodes = stats.nodes,
|
||||
text_nodes = stats.text_nodes,
|
||||
container_nodes = stats.container_nodes,
|
||||
background_quads = stats.background_quads,
|
||||
intrinsic_calls = stats.intrinsic_calls,
|
||||
intrinsic_size_calls = stats.intrinsic_size_calls,
|
||||
intrinsic_text_calls = stats.intrinsic_text_calls,
|
||||
intrinsic_container_calls = stats.intrinsic_container_calls,
|
||||
intrinsic_ms = stats.intrinsic_ms,
|
||||
text_prepare_calls = stats.text_prepare_calls,
|
||||
text_prepare_ms = stats.text_prepare_ms,
|
||||
viewport_culled = stats.viewport_culled,
|
||||
layout_cache_hits = stats.layout_cache_hits,
|
||||
layout_cache_misses = stats.layout_cache_misses,
|
||||
intrinsic_cache_hits = stats.intrinsic_cache_hits,
|
||||
text_requests = stats.text_requests,
|
||||
text_cache_hits = stats.text_cache_hits,
|
||||
text_cache_misses = stats.text_cache_misses,
|
||||
text_output_glyphs = stats.text_output_glyphs,
|
||||
text_family_resolve_ms = stats.text_family_resolve_ms,
|
||||
text_buffer_build_ms = stats.text_buffer_build_ms,
|
||||
text_glyph_collect_ms = stats.text_glyph_collect_ms,
|
||||
text_miss_ms = stats.text_miss_ms,
|
||||
scene_items = stats.scene_items,
|
||||
"layout snapshot perf"
|
||||
);
|
||||
}
|
||||
let root_min_size = element_min_size(root);
|
||||
LayoutSnapshot {
|
||||
scene,
|
||||
interaction_tree: InteractionTree {
|
||||
root: interaction_root,
|
||||
(
|
||||
LayoutSnapshot {
|
||||
scene,
|
||||
interaction_tree: InteractionTree {
|
||||
root: interaction_root,
|
||||
},
|
||||
root_min_size,
|
||||
},
|
||||
root_min_size,
|
||||
}
|
||||
stats,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -2903,7 +2972,7 @@ mod tests {
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Text(text) => Some(text.text.as_str()),
|
||||
DisplayItem::Text(text) => Some(text.text.as_ref()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -31,8 +31,9 @@ pub use interaction::{
|
||||
};
|
||||
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
||||
pub use layout::{
|
||||
HitTarget, InteractionTree, LayoutCache, LayoutNode, LayoutPath, LayoutSnapshot, ScrollMetrics,
|
||||
TextHitTarget, element_min_size, layout_snapshot, layout_snapshot_with_cache,
|
||||
HitTarget, InteractionTree, LayoutCache, LayoutFrameStats, LayoutNode, LayoutPath,
|
||||
LayoutSnapshot, ScrollMetrics, TextHitTarget, element_min_size, layout_snapshot,
|
||||
layout_snapshot_with_cache, layout_snapshot_with_cache_and_stats,
|
||||
layout_snapshot_with_text_system,
|
||||
};
|
||||
pub use layout::{layout_scene, layout_scene_with_text_system};
|
||||
|
||||
@@ -189,7 +189,7 @@ pub struct PreparedTextLine {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PreparedText {
|
||||
pub element_id: Option<ElementId>,
|
||||
pub text: String,
|
||||
pub text: Arc<str>,
|
||||
pub origin: Point,
|
||||
pub bounds: Option<UiSize>,
|
||||
pub font_size: f32,
|
||||
@@ -206,6 +206,8 @@ pub struct PreparedText {
|
||||
/// Add `origin` to convert to absolute window coords.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TextLayoutData {
|
||||
pub text: Arc<str>,
|
||||
pub cache_key: u64,
|
||||
pub lines: Vec<PreparedTextLine>,
|
||||
pub glyphs: Vec<GlyphInstance>,
|
||||
/// Measured (unclamped) size of the laid-out text.
|
||||
@@ -225,7 +227,6 @@ impl PreparedText {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_layout(
|
||||
element_id: Option<ElementId>,
|
||||
text: String,
|
||||
origin: Point,
|
||||
bounds: Option<UiSize>,
|
||||
font_size: f32,
|
||||
@@ -237,7 +238,7 @@ impl PreparedText {
|
||||
) -> Self {
|
||||
Self {
|
||||
element_id,
|
||||
text,
|
||||
text: layout.text.clone(),
|
||||
origin,
|
||||
bounds,
|
||||
font_size,
|
||||
@@ -256,6 +257,10 @@ impl PreparedText {
|
||||
Arc::as_ptr(&self.layout)
|
||||
}
|
||||
|
||||
pub fn layout_cache_key(&self) -> u64 {
|
||||
self.layout.cache_key
|
||||
}
|
||||
|
||||
/// Create a monospace `PreparedText` without going through the text system.
|
||||
/// Used for testing and low-level terminal-style rendering.
|
||||
pub fn monospace(
|
||||
@@ -265,7 +270,7 @@ impl PreparedText {
|
||||
advance: f32,
|
||||
color: Color,
|
||||
) -> Self {
|
||||
let text = text.into();
|
||||
let text: Arc<str> = Arc::from(text.into());
|
||||
// Glyphs are stored in LOCAL (origin-relative) coordinates.
|
||||
let mut local_x = 0.0f32;
|
||||
let mut glyphs = Vec::with_capacity(text.chars().count());
|
||||
@@ -293,7 +298,7 @@ impl PreparedText {
|
||||
let size = UiSize::new(local_x, font_size);
|
||||
Self {
|
||||
element_id: None,
|
||||
text,
|
||||
text: text.clone(),
|
||||
origin,
|
||||
bounds: None,
|
||||
font_size,
|
||||
@@ -302,6 +307,8 @@ impl PreparedText {
|
||||
selectable: true,
|
||||
selection_style: TextSelectionStyle::DEFAULT,
|
||||
layout: Arc::new(TextLayoutData {
|
||||
text: text.clone(),
|
||||
cache_key: monospace_text_cache_key(&text, font_size, advance, color),
|
||||
lines: vec![line],
|
||||
glyphs,
|
||||
size,
|
||||
@@ -466,7 +473,10 @@ impl PreparedText {
|
||||
if range.is_empty() {
|
||||
return;
|
||||
}
|
||||
for glyph in Arc::make_mut(&mut self.layout).glyphs.iter_mut() {
|
||||
let layout = Arc::make_mut(&mut self.layout);
|
||||
layout.cache_key =
|
||||
selected_text_cache_key(layout.cache_key, range.start, range.end, selected_color);
|
||||
for glyph in layout.glyphs.iter_mut() {
|
||||
if glyph.text_end > range.start && glyph.text_start < range.end {
|
||||
glyph.color = selected_color;
|
||||
}
|
||||
@@ -583,6 +593,28 @@ impl PreparedText {
|
||||
}
|
||||
}
|
||||
|
||||
fn monospace_text_cache_key(text: &str, font_size: f32, advance: f32, color: Color) -> u64 {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
text.hash(&mut hasher);
|
||||
font_size.to_bits().hash(&mut hasher);
|
||||
advance.to_bits().hash(&mut hasher);
|
||||
color.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
fn selected_text_cache_key(base: u64, start: usize, end: usize, color: Color) -> u64 {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
base.hash(&mut hasher);
|
||||
start.hash(&mut hasher);
|
||||
end.hash(&mut hasher);
|
||||
color.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum WordClass {
|
||||
Word,
|
||||
@@ -886,6 +918,8 @@ mod tests {
|
||||
}
|
||||
let orig_size = text.layout.size;
|
||||
text.layout = Arc::new(TextLayoutData {
|
||||
text: text.text.clone(),
|
||||
cache_key: text.layout.cache_key,
|
||||
lines,
|
||||
glyphs,
|
||||
size: orig_size,
|
||||
|
||||
@@ -269,7 +269,7 @@ pub struct TextSystem {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct TextFrameStats {
|
||||
pub struct TextFrameStats {
|
||||
pub requests: u32,
|
||||
pub cache_hits: u32,
|
||||
pub cache_misses: u32,
|
||||
@@ -341,7 +341,6 @@ impl TextSystem {
|
||||
bounds: Option<UiSize>,
|
||||
) -> PreparedText {
|
||||
let bounds = bounds.or(style.bounds);
|
||||
let text = combined_text(spans);
|
||||
// Use the cached Arc directly — glyphs are in local (origin-0) coords.
|
||||
let layout_data = self.layout(
|
||||
spans,
|
||||
@@ -352,7 +351,6 @@ impl TextSystem {
|
||||
|
||||
PreparedText::from_layout(
|
||||
None,
|
||||
text,
|
||||
origin,
|
||||
bounds,
|
||||
style.font_size,
|
||||
@@ -476,7 +474,8 @@ impl TextSystem {
|
||||
}
|
||||
self.frame_stats.buffer_build_ms += buffer_build_started.elapsed().as_secs_f64() * 1_000.0;
|
||||
|
||||
let line_starts = line_start_offsets(&combined_text(spans));
|
||||
let text = combined_text(spans);
|
||||
let line_starts = line_start_offsets(&text);
|
||||
let mut measured_width: f32 = 0.0;
|
||||
let mut measured_height: f32 = 0.0;
|
||||
let mut lines = Vec::new();
|
||||
@@ -527,6 +526,8 @@ impl TextSystem {
|
||||
glyph_collect_started.elapsed().as_secs_f64() * 1_000.0;
|
||||
|
||||
let layout = Arc::new(TextLayoutData {
|
||||
text: Arc::from(text),
|
||||
cache_key,
|
||||
lines,
|
||||
glyphs,
|
||||
size: UiSize::new(measured_width.max(0.0), measured_height.max(0.0)),
|
||||
|
||||
@@ -244,6 +244,7 @@ enum ElementContent {
|
||||
pub(crate) struct TextNode {
|
||||
pub spans: Vec<TextSpan>,
|
||||
pub style: TextStyle,
|
||||
content_hash: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -281,12 +282,14 @@ impl Element {
|
||||
}
|
||||
|
||||
pub fn spans(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
||||
let spans = spans.into_iter().collect::<Vec<_>>();
|
||||
Self {
|
||||
id: None,
|
||||
style: Style::default(),
|
||||
children: Vec::new(),
|
||||
content: ElementContent::Text(TextNode {
|
||||
spans: spans.into_iter().collect(),
|
||||
content_hash: text_node_content_hash(&spans, &style),
|
||||
spans,
|
||||
style,
|
||||
}),
|
||||
}
|
||||
@@ -599,8 +602,7 @@ impl Hash for Style {
|
||||
|
||||
impl Hash for TextNode {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.spans.hash(state);
|
||||
self.style.hash(state);
|
||||
self.content_hash.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +613,15 @@ impl Hash for ImageNode {
|
||||
}
|
||||
}
|
||||
|
||||
fn text_node_content_hash(spans: &[TextSpan], style: &TextStyle) -> u64 {
|
||||
use std::hash::DefaultHasher;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
spans.hash(&mut hasher);
|
||||
style.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
impl Hash for ScrollBoxNode {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.offset_y.to_bits().hash(state);
|
||||
|
||||
Reference in New Issue
Block a user