diff --git a/Cargo.lock b/Cargo.lock index 6cded64..fd2057d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "1.0.0" @@ -583,6 +589,12 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -629,6 +641,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.6.1" @@ -910,6 +949,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1479,6 +1554,7 @@ dependencies = [ "clap", "colored", "compact_str", + "criterion", "glt", "iced", "indicatif", @@ -1949,12 +2025,32 @@ dependencies = [ "syn", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2870,6 +2966,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "orbclient" version = "0.3.54" @@ -3016,6 +3118,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.17.16" @@ -3241,7 +3371,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -4051,6 +4181,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index 2d7ad1c..2728657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,10 @@ rayon = { version = "1.10", optional = true } [features] default = [] parallel = ["rayon"] + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "bench" +harness = false diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 0000000..afce1fe --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,99 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +fn bench_matching_rules(c: &mut Criterion) { + let mut group = c.benchmark_group("matching_rules"); + group.sample_size(10); + + group.bench_function("100_rules", |b| { + b.iter(|| { + let mut stylesheet = glint_runtime::interpreter::style::StyleSheet::new(); + for i in 0..100 { + stylesheet.add_rule( + format!(".class-{}", i), + std::collections::HashMap::from([("color".into(), "red".into())]), + ); + } + stylesheet.build_index(); + let _ = stylesheet.matching_rules( + "Button", + None, + &["class-1"], + &[], + &Default::default(), + &[], + &[], + &std::collections::HashMap::new(), + ); + }) + }); + + group.finish(); +} + +fn bench_style_compute(c: &mut Criterion) { + let mut group = c.benchmark_group("computed_style"); + group.sample_size(10); + + group.bench_function("compute_empty", |b| { + b.iter(|| { + let _ = glint_runtime::interpreter::style::ComputedStyle::compute( + &[], + &[], + ); + }) + }); + + group.bench_function("compute_5_props", |b| { + let props: Vec<(std::borrow::Cow<'_, str>, std::borrow::Cow<'_, str>)> = vec![ + ("color".into(), "red".into()), + ("padding".into(), "10px".into()), + ("font-size".into(), "16px".into()), + ("background".into(), "#fff".into()), + ("width".into(), "100px".into()), + ]; + b.iter(|| { + let _ = glint_runtime::interpreter::style::ComputedStyle::compute( + &props, + &[], + ); + }) + }); + + group.finish(); +} + +fn bench_resolve_string(c: &mut Criterion) { + let mut group = c.benchmark_group("resolve_string"); + group.sample_size(10); + + let vars = std::collections::HashMap::from([ + ("name".to_string(), glint_runtime::interpreter::Value::Str("world".into())), + ("count".to_string(), glint_runtime::interpreter::Value::Int(42)), + ]); + + group.bench_function("no_vars", |b| { + b.iter(|| { + use glint_runtime::interpreter::Interpreter; + let _ = Interpreter::resolve_string("hello world", &vars); + }) + }); + + group.bench_function("1_var", |b| { + b.iter(|| { + use glint_runtime::interpreter::Interpreter; + let _ = Interpreter::resolve_string("Hello $name!", &vars); + }) + }); + + group.bench_function("3_vars", |b| { + b.iter(|| { + use glint_runtime::interpreter::Interpreter; + let _ = Interpreter::resolve_string("$name has count $count and is $name", &vars); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_matching_rules, bench_style_compute, bench_resolve_string); +criterion_main!(benches); diff --git a/src/app.rs b/src/app.rs index 14b143b..21601a3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use iced::widget::{column, container}; use iced::{Length, Theme}; use crate::interpreter::{Document, Element, Interpreter, RheiContext, Value}; +use crate::perf; use crate::renderer::render_element; use crate::Message; @@ -63,6 +64,7 @@ impl GlintApp { let dirty_set = self.doc.tracker.take_dirty_set(); let root_refs: Vec<&Element<'static>> = self.doc.roots.iter().collect(); + let _vdom_scope = perf::PerfScope::new("vdom"); self.vdom_roots = Interpreter::evaluate_vdom_incr( &root_refs, &mut self.doc.variables, @@ -72,11 +74,13 @@ impl GlintApp { &[], &dirty_set, ); + drop(_vdom_scope); iced::Task::none() } pub fn view(&self) -> iced::Element<'_, Message, Theme, iced::Renderer> { + let _render_scope = perf::PerfScope::new("render"); let mut content = column![] .width(Length::Fill) .height(Length::Fill); @@ -115,20 +119,25 @@ impl GlintApp { let has_sticky = !global_sticky_layers.is_empty(); let has_fixed = !global_fixed_layers.is_empty(); - if !has_abs && !has_sticky && !has_fixed { - main_flow.into() - } else { - let mut stack_widget = iced::widget::stack![main_flow]; - for layer in global_abs_layers { - stack_widget = stack_widget.push(layer); + let render_result = { + if !has_abs && !has_sticky && !has_fixed { + main_flow.into() + } else { + let mut stack_widget = iced::widget::stack![main_flow]; + for layer in global_abs_layers { + stack_widget = stack_widget.push(layer); + } + for layer in global_sticky_layers { + stack_widget = stack_widget.push(layer); + } + for layer in global_fixed_layers { + stack_widget = stack_widget.push(layer); + } + stack_widget.into() } - for layer in global_sticky_layers { - stack_widget = stack_widget.push(layer); - } - for layer in global_fixed_layers { - stack_widget = stack_widget.push(layer); - } - stack_widget.into() - } + }; + drop(_render_scope); + perf::print_frame(); + render_result } } diff --git a/src/interpreter/style.rs b/src/interpreter/style.rs index 719010d..5c9de11 100644 --- a/src/interpreter/style.rs +++ b/src/interpreter/style.rs @@ -821,6 +821,7 @@ impl StyleSheet { props: &[(Cow<'_, str>, Cow<'_, str>)], matched_sheets: &[&'a HashMap], ) -> ComputedStyle { + let _scope = crate::perf::PerfScope::new("style"); let epoch = self.epoch; self.cache.lock().unwrap().get_or_compute(type_name, props, epoch, matched_sheets) } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..488901e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +pub mod app; +pub mod cli; +pub mod interpreter; +pub mod perf; +pub mod renderer; + +pub use app::GlintApp; +pub use interpreter::{Element, Interpreter, RheiContext, Value}; +pub use interpreter::style::{ComputedStyle, StyleSheet}; +pub use interpreter::types::{Document, Interner}; +pub use interpreter::reactive::{ElementId, ReactiveTracker}; + +#[derive(Debug, Clone)] +pub enum Message { + EventTriggered(String), + InputChanged(Option, String), + ToggleChanged(Option, bool), + SliderChanged(Option, f64), + WindowScrolled(f32), + ScrollableScrolled(u64, f32), +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 79ae87f..cca50db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,11 @@ mod app; mod cli; mod interpreter; +mod perf; mod renderer; use clap::{Parser, Subcommand}; - -#[derive(Debug, Clone)] -pub enum Message { - EventTriggered(String), - InputChanged(Option, String), - ToggleChanged(Option, bool), - SliderChanged(Option, f64), - WindowScrolled(f32), - ScrollableScrolled(u64, f32), -} +use glint_runtime::Message; #[derive(Parser)] #[command(name = "glint-runtime")] @@ -39,6 +31,10 @@ enum Commands { Run { /// Bytecode file to execute file: String, + + /// Print per-frame performance metrics (VDOM/Style/Render timing) + #[arg(long)] + perf: bool, }, } @@ -59,7 +55,10 @@ fn main() { }); cli::compile_files(&gltm_files, &glts_files, &output_path); } - Commands::Run { file } => { + Commands::Run { file, perf } => { + if perf { + crate::perf::set_enabled(true); + } cli::run_file(&file); } } diff --git a/src/perf.rs b/src/perf.rs new file mode 100644 index 0000000..0bef389 --- /dev/null +++ b/src/perf.rs @@ -0,0 +1,87 @@ +use std::cell::{Cell, RefCell}; +use std::time::Instant; + +#[derive(Debug, Clone, Default)] +pub struct PerfReport { + pub vdom_eval: f64, + pub style: f64, + pub render: f64, + pub total: f64, +} + +thread_local! { + static PERF_ENABLED: Cell = Cell::new(false); + static PERF_REPORT: RefCell = RefCell::new(PerfReport::default()); +} + +pub fn set_enabled(enabled: bool) { + PERF_ENABLED.with(|e| e.set(enabled)); +} + +pub fn is_enabled() -> bool { + PERF_ENABLED.with(|e| e.get()) +} + +pub struct PerfScope { + name: &'static str, + start: Instant, +} + +impl PerfScope { + pub fn new(name: &'static str) -> Self { + Self { + name, + start: Instant::now(), + } + } +} + +impl Drop for PerfScope { + fn drop(&mut self) { + if !is_enabled() { + return; + } + let ms = self.start.elapsed().as_secs_f64() * 1000.0; + PERF_REPORT.with(|r| { + let mut r = r.borrow_mut(); + match self.name { + "vdom" => r.vdom_eval += ms, + "style" => r.style += ms, + "render" => r.render += ms, + _ => {} + } + }); + } +} + +pub fn report_and_reset() -> Option { + if !is_enabled() { + return None; + } + PERF_REPORT.with(|r| { + let r = r.borrow(); + let total = r.vdom_eval + r.style + r.render; + let mut out = format!( + "VDOM: {:.2}ms | Style: {:.2}ms | Render: {:.2}ms | Total: {:.2}ms", + r.vdom_eval, r.style, r.render, total + ); + if total > 16.0 { + out.push_str(&format!( + " ⚠️ Frame budget exceeded! {:.2}ms > 16ms", + total + )); + } + Some(out) + }) +} + +pub fn reset() { + PERF_REPORT.with(|r| *r.borrow_mut() = PerfReport::default()); +} + +pub fn print_frame() { + if let Some(report) = report_and_reset() { + eprintln!("[perf] {}", report); + } + reset(); +}