Perf profiling and some low hanging perf improvements in text rendering
This commit is contained in:
366
examples/perf_scenarios/src/main.rs
Normal file
366
examples/perf_scenarios/src/main.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use std::env;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use ruin_perf::{PerfReportBuilder, RunMetadata};
|
||||
use ruin_ui::{
|
||||
AlignItems, Color, Edges, Element, FlexDirection, LayoutCache, LayoutFrameStats,
|
||||
ScrollbarStyle, TextStyle, TextSystem, UiSize, layout_snapshot_with_cache_and_stats,
|
||||
};
|
||||
|
||||
const DEFAULT_FRAMES: u64 = 300;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Config {
|
||||
scenario: Scenario,
|
||||
frames: u64,
|
||||
output: Option<PathBuf>,
|
||||
text_kb: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum Scenario {
|
||||
Counter,
|
||||
LargeScrollList,
|
||||
Calculator,
|
||||
ResizeConfigure,
|
||||
LargeTextBody,
|
||||
}
|
||||
|
||||
impl Scenario {
|
||||
const ALL: &'static [Self] = &[
|
||||
Self::Counter,
|
||||
Self::LargeScrollList,
|
||||
Self::Calculator,
|
||||
Self::ResizeConfigure,
|
||||
Self::LargeTextBody,
|
||||
];
|
||||
|
||||
const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Counter => "counter",
|
||||
Self::LargeScrollList => "large-scroll-list",
|
||||
Self::Calculator => "calculator",
|
||||
Self::ResizeConfigure => "resize-configure",
|
||||
Self::LargeTextBody => "large-text-body",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
Self::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|scenario| scenario.name() == value)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let config = Config::parse(env::args().skip(1))?;
|
||||
let report = run_scenario(&config);
|
||||
let output = config
|
||||
.output
|
||||
.clone()
|
||||
.unwrap_or_else(|| report.default_output_path());
|
||||
report.write_json(&output)?;
|
||||
report.print_summary();
|
||||
println!();
|
||||
println!("wrote {}", output.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn parse(mut args: impl Iterator<Item = String>) -> io::Result<Self> {
|
||||
let mut scenario = Scenario::LargeScrollList;
|
||||
let mut frames = DEFAULT_FRAMES;
|
||||
let mut output = None;
|
||||
let mut text_kb = 256usize;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--scenario" => {
|
||||
let value = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--scenario needs a value")
|
||||
})?;
|
||||
scenario = Scenario::parse(&value).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unknown scenario '{value}'"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
"--frames" => {
|
||||
let value = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--frames needs a value")
|
||||
})?;
|
||||
frames = value.parse::<u64>().map_err(|error| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid frame count '{value}': {error}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
"--output" => {
|
||||
let value = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--output needs a value")
|
||||
})?;
|
||||
output = Some(PathBuf::from(value));
|
||||
}
|
||||
"--text-kb" => {
|
||||
let value = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--text-kb needs a value")
|
||||
})?;
|
||||
text_kb = value.parse::<usize>().map_err(|error| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid text size '{value}': {error}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
"--list" => {
|
||||
for scenario in Scenario::ALL {
|
||||
println!("{}", scenario.name());
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_help();
|
||||
std::process::exit(0);
|
||||
}
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unknown argument '{other}'"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
scenario,
|
||||
frames,
|
||||
output,
|
||||
text_kb,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"ruin_perf_scenarios --scenario <name> [--frames <n>] [--output <path>] [--text-kb <n>]"
|
||||
);
|
||||
println!();
|
||||
println!("scenarios:");
|
||||
for scenario in Scenario::ALL {
|
||||
println!(" {}", scenario.name());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_scenario(config: &Config) -> ruin_perf::PerfReport {
|
||||
let mut report =
|
||||
PerfReportBuilder::new(RunMetadata::new(config.scenario.name(), config.frames));
|
||||
let mut text_system = TextSystem::new();
|
||||
let mut layout_cache = LayoutCache::new();
|
||||
|
||||
report.note("headless layout/app-build scenario; renderer/platform timings are not included");
|
||||
if config.scenario == Scenario::LargeTextBody {
|
||||
report.set_counter("text_body_kb", config.text_kb as f64);
|
||||
}
|
||||
|
||||
for frame in 0..config.frames {
|
||||
let build_started = Instant::now();
|
||||
let (tree, viewport) = match config.scenario {
|
||||
Scenario::Counter => counter_tree(frame),
|
||||
Scenario::LargeScrollList => large_scroll_list_tree(frame),
|
||||
Scenario::Calculator => calculator_tree(frame),
|
||||
Scenario::ResizeConfigure => resize_configure_tree(frame),
|
||||
Scenario::LargeTextBody => large_text_body_tree(frame, config.text_kb),
|
||||
};
|
||||
let build_ms = build_started.elapsed().as_secs_f64() * 1_000.0;
|
||||
let (_snapshot, stats) = layout_snapshot_with_cache_and_stats(
|
||||
frame + 1,
|
||||
viewport,
|
||||
&tree,
|
||||
&mut text_system,
|
||||
&mut layout_cache,
|
||||
);
|
||||
|
||||
report.record_ms("app_build", build_ms);
|
||||
record_layout_stats(&mut report, stats);
|
||||
}
|
||||
|
||||
report.finish()
|
||||
}
|
||||
|
||||
fn record_layout_stats(report: &mut PerfReportBuilder, stats: LayoutFrameStats) {
|
||||
report.record_ms("layout_total", stats.total_ms);
|
||||
report.record_ms("layout_intrinsic", stats.intrinsic_ms);
|
||||
report.record_ms("text_prepare", stats.text_prepare_ms);
|
||||
report.record_ms("text_cache_miss", stats.text_miss_ms);
|
||||
report.record("layout_nodes", "count", stats.nodes as f64);
|
||||
report.record("scene_items", "count", stats.scene_items as f64);
|
||||
report.record("text_glyphs", "count", stats.text_output_glyphs as f64);
|
||||
report.add_counter("layout_cache_hits", stats.layout_cache_hits as f64);
|
||||
report.add_counter("layout_cache_misses", stats.layout_cache_misses as f64);
|
||||
report.add_counter("intrinsic_cache_hits", stats.intrinsic_cache_hits as f64);
|
||||
report.add_counter("viewport_culled", stats.viewport_culled as f64);
|
||||
report.add_counter("text_cache_hits", stats.text_cache_hits as f64);
|
||||
report.add_counter("text_cache_misses", stats.text_cache_misses as f64);
|
||||
report.add_counter("text_requests", stats.text_requests as f64);
|
||||
}
|
||||
|
||||
fn text_style(size: f32) -> TextStyle {
|
||||
TextStyle::new(size, Color::rgb(0x20, 0x20, 0x20))
|
||||
}
|
||||
|
||||
fn counter_tree(frame: u64) -> (Element, UiSize) {
|
||||
let count = frame as i64;
|
||||
let root = Element::column()
|
||||
.gap(12.0)
|
||||
.padding(Edges::all(24.0))
|
||||
.align_items(AlignItems::Center)
|
||||
.child(Element::text("Counter", text_style(28.0)))
|
||||
.child(Element::text(format!("count = {count}"), text_style(18.0)))
|
||||
.child(Element::text(
|
||||
format!("double = {}", count * 2),
|
||||
text_style(18.0),
|
||||
))
|
||||
.child(button_row(["-1", "Reset", "+1"]));
|
||||
(root, UiSize::new(480.0, 320.0))
|
||||
}
|
||||
|
||||
fn large_scroll_list_tree(frame: u64) -> (Element, UiSize) {
|
||||
let item_count = 1_000usize;
|
||||
let item_height = 28.0;
|
||||
let viewport = UiSize::new(480.0, 640.0);
|
||||
let max_offset = (item_count as f32 * item_height - viewport.height).max(0.0);
|
||||
let offset = ((frame as f32 * 19.0) % max_offset.max(1.0)).round();
|
||||
let mut list = Element::column().gap(0.0);
|
||||
for index in 0..item_count {
|
||||
list = list.child(Element::new().height(item_height).child(Element::text(
|
||||
format!("list item {index:04}"),
|
||||
text_style(14.0),
|
||||
)));
|
||||
}
|
||||
let root = Element::new().child(
|
||||
Element::scroll_box(offset)
|
||||
.width(viewport.width)
|
||||
.height(viewport.height)
|
||||
.scrollbar_style(ScrollbarStyle::default())
|
||||
.child(list),
|
||||
);
|
||||
(root, viewport)
|
||||
}
|
||||
|
||||
fn calculator_tree(frame: u64) -> (Element, UiSize) {
|
||||
let expression = match frame % 6 {
|
||||
0 => "2",
|
||||
1 => "2+",
|
||||
2 => "2+2",
|
||||
3 => "4",
|
||||
4 => "4*8",
|
||||
_ => "32",
|
||||
};
|
||||
let history_len = (frame as usize / 12).min(40);
|
||||
let mut history = Element::column().gap(4.0);
|
||||
for index in 0..history_len {
|
||||
history = history.child(Element::text(
|
||||
format!("{} + {} = {}", index, index + 1, index * 2 + 1),
|
||||
text_style(12.0),
|
||||
));
|
||||
}
|
||||
|
||||
let root = Element::column()
|
||||
.gap(10.0)
|
||||
.padding(Edges::all(20.0))
|
||||
.child(Element::text("Calculator", text_style(28.0)))
|
||||
.child(display_panel(expression))
|
||||
.child(button_grid())
|
||||
.child(Element::scroll_box(10_000.0).height(180.0).child(history));
|
||||
(root, UiSize::new(420.0, 720.0))
|
||||
}
|
||||
|
||||
fn resize_configure_tree(frame: u64) -> (Element, UiSize) {
|
||||
let width = 360.0 + (frame % 90) as f32 * 4.0;
|
||||
let height = 280.0 + (frame % 60) as f32 * 3.0;
|
||||
let mut content = Element::column().gap(8.0).padding(Edges::all(16.0));
|
||||
for index in 0..40 {
|
||||
content = content.child(Element::text(
|
||||
format!("resize row {index:02}: {width:.0} x {height:.0}"),
|
||||
text_style(14.0),
|
||||
));
|
||||
}
|
||||
(content, UiSize::new(width, height))
|
||||
}
|
||||
|
||||
fn large_text_body_tree(frame: u64, text_kb: usize) -> (Element, UiSize) {
|
||||
let viewport = UiSize::new(720.0, 720.0);
|
||||
let text_width = 680.0;
|
||||
let line_height = 18.0;
|
||||
let text = large_text_body(text_kb);
|
||||
let estimated_line_count = (text.len() as f32 / 72.0).ceil();
|
||||
let estimated_height = (estimated_line_count * line_height).max(viewport.height);
|
||||
let max_offset = (estimated_height - viewport.height).max(0.0);
|
||||
let offset = ((frame as f32 * 127.0) % max_offset.max(1.0)).round();
|
||||
let style = text_style(14.0)
|
||||
.with_line_height(line_height)
|
||||
.with_bounds(UiSize::new(text_width, estimated_height));
|
||||
|
||||
let root = Element::new().child(
|
||||
Element::scroll_box(offset)
|
||||
.width(viewport.width)
|
||||
.height(viewport.height)
|
||||
.child(
|
||||
Element::new()
|
||||
.width(text_width)
|
||||
.child(Element::paragraph(text, style)),
|
||||
),
|
||||
);
|
||||
(root, viewport)
|
||||
}
|
||||
|
||||
fn large_text_body(text_kb: usize) -> String {
|
||||
let target_bytes = text_kb.saturating_mul(1024).max(1);
|
||||
let paragraph = "RUIN large text profiling paragraph: the quick brown fox jumps over the lazy dog. This line is intentionally ordinary prose with enough words to exercise wrapping, glyph collection, hit-test geometry, and cache behavior.\n";
|
||||
let mut text = String::with_capacity(target_bytes + paragraph.len());
|
||||
while text.len() < target_bytes {
|
||||
text.push_str(paragraph);
|
||||
}
|
||||
text.truncate(target_bytes);
|
||||
text
|
||||
}
|
||||
|
||||
fn display_panel(expression: &str) -> Element {
|
||||
Element::new()
|
||||
.padding(Edges::all(12.0))
|
||||
.border(1.0, Color::rgb(0x80, 0x80, 0x80))
|
||||
.child(Element::text(expression, text_style(24.0)))
|
||||
}
|
||||
|
||||
fn button_grid() -> Element {
|
||||
let rows = [
|
||||
["7", "8", "9", "/"],
|
||||
["4", "5", "6", "*"],
|
||||
["1", "2", "3", "-"],
|
||||
["0", ".", "=", "+"],
|
||||
];
|
||||
let mut grid = Element::column().gap(6.0);
|
||||
for row in rows {
|
||||
grid = grid.child(button_row(row));
|
||||
}
|
||||
grid
|
||||
}
|
||||
|
||||
fn button_row<const N: usize>(labels: [&str; N]) -> Element {
|
||||
let mut row = Element::new().direction(FlexDirection::Row).gap(6.0);
|
||||
for label in labels {
|
||||
row = row.child(
|
||||
Element::new()
|
||||
.width(76.0)
|
||||
.height(36.0)
|
||||
.border(1.0, Color::rgb(0x90, 0x90, 0x90))
|
||||
.child(Element::text(label, text_style(16.0))),
|
||||
);
|
||||
}
|
||||
row
|
||||
}
|
||||
Reference in New Issue
Block a user