Phase 10: Monitoring and automated benchmarks
10.1: Add Criterion dev-dependency and benchmarks for:
- matching_rules (100 rules)
- ComputedStyle::compute (empty, 5 props)
- resolve_string (no vars, 1 var, 3 vars)
10.3: Add --perf CLI flag and src/perf.rs module:
- PerfScope drop-guard timed scopes (vdom, style, render)
- Instrumented: VDOM eval (app.rs update), render (app.rs view),
style matching (style.rs compute_cached)
- Per-frame report printed to stderr at end of view()
10.2+10.4: Threshold check in perf report:
- Prints warning if total > 16ms (60 FPS frame budget)
Refactor: Move Message enum to lib.rs so benchmarks can import
the public API. Added lib.rs as library root.
This commit is contained in:
142
Cargo.lock
generated
142
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
99
benches/bench.rs
Normal file
99
benches/bench.rs
Normal file
@@ -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);
|
||||
37
src/app.rs
37
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,6 +821,7 @@ impl StyleSheet {
|
||||
props: &[(Cow<'_, str>, Cow<'_, str>)],
|
||||
matched_sheets: &[&'a HashMap<String, String>],
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
21
src/lib.rs
Normal file
21
src/lib.rs
Normal file
@@ -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>, String),
|
||||
ToggleChanged(Option<String>, bool),
|
||||
SliderChanged(Option<String>, f64),
|
||||
WindowScrolled(f32),
|
||||
ScrollableScrolled(u64, f32),
|
||||
}
|
||||
21
src/main.rs
21
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>, String),
|
||||
ToggleChanged(Option<String>, bool),
|
||||
SliderChanged(Option<String>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
87
src/perf.rs
Normal file
87
src/perf.rs
Normal file
@@ -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<bool> = Cell::new(false);
|
||||
static PERF_REPORT: RefCell<PerfReport> = 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<String> {
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user