add benchmark

This commit is contained in:
Evan Almloff 2022-04-20 21:44:37 -05:00
parent 366a0a8026
commit c8919ad77b
5 changed files with 650 additions and 60 deletions

View file

@ -88,3 +88,7 @@ harness = false
[[bench]] [[bench]]
name = "jsframework" name = "jsframework"
harness = false harness = false
[[bench]]
name = "tui_update"
harness = false

268
benches/tui_update.rs Normal file
View file

@ -0,0 +1,268 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
criterion_group!(mbenches, tui_update);
criterion_main!(mbenches);
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn tui_update(c: &mut Criterion) {
let mut group = c.benchmark_group("Update boxes");
// We can also use loops to define multiple benchmarks, even over multiple dimensions.
for size in 1..=8u32 {
let parameter_string = format!("{}", (3 * size).pow(2));
group.bench_with_input(
BenchmarkId::new("size", parameter_string),
&size,
|b, size| {
b.iter(|| match size {
1 => dioxus::tui::launch_cfg(
app3,
Config {
headless: true,
..Default::default()
},
),
2 => dioxus::tui::launch_cfg(
app6,
Config {
headless: true,
..Default::default()
},
),
3 => dioxus::tui::launch_cfg(
app9,
Config {
headless: true,
..Default::default()
},
),
4 => dioxus::tui::launch_cfg(
app12,
Config {
headless: true,
..Default::default()
},
),
5 => dioxus::tui::launch_cfg(
app15,
Config {
headless: true,
..Default::default()
},
),
6 => dioxus::tui::launch_cfg(
app18,
Config {
headless: true,
..Default::default()
},
),
7 => dioxus::tui::launch_cfg(
app21,
Config {
headless: true,
..Default::default()
},
),
8 => dioxus::tui::launch_cfg(
app24,
Config {
headless: true,
..Default::default()
},
),
_ => (),
})
},
);
}
}
#[derive(Props, PartialEq)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element {
let count = use_state(&cx, || 0);
let x = cx.props.x * 2;
let y = cx.props.y * 2;
let hue = cx.props.hue;
let display_hue = cx.props.hue as u32 / 10;
let count = count.get();
let alpha = cx.props.alpha + (count % 100) as f32;
cx.render(rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
})
}
#[derive(Props, PartialEq)]
struct GridProps {
size: usize,
}
#[allow(non_snake_case)]
fn Grid(cx: Scope<GridProps>) -> Element {
let size = cx.props.size;
let count = use_state(&cx, || 0);
let counts = use_ref(&cx, || vec![0; size * size]);
let ctx: TuiContext = cx.consume_context().unwrap();
if *count.get() + 1 >= (size * size) {
ctx.quit();
} else {
counts.with_mut(|c| {
let i = *count.current();
c[i] += 1;
c[i] = c[i] % 360;
});
count.with_mut(|i| {
*i += 1;
*i = *i % (size * size);
});
}
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..size).map(|x|
{
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..size).map(|y|
{
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
let key = format!("{}-{}", x, y);
cx.render(rsx! {
Box{
x: x,
y: y,
alpha: 100.0,
hue: alpha,
key: "{key}",
}
})
}
)
}
})
}
)
}
})
}
fn app3(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 3,
}
}
})
}
fn app6(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 6,
}
}
})
}
fn app9(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 9,
}
}
})
}
fn app12(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 12,
}
}
})
}
fn app15(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 15,
}
}
})
}
fn app18(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 18,
}
}
})
}
fn app21(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 21,
}
}
})
}
fn app24(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 24,
}
}
})
}

260
examples/tui_stress_test.rs Normal file
View file

@ -0,0 +1,260 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
criterion_group!(mbenches, tui_update);
criterion_main!(mbenches);
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn tui_update(c: &mut Criterion) {
let mut group = c.benchmark_group("Update boxes");
// We can also use loops to define multiple benchmarks, even over multiple dimensions.
for size in 1..=8u32 {
let parameter_string = format!("{}", (3 * size).pow(2));
group.bench_with_input(
BenchmarkId::new("size", parameter_string),
&size,
|b, size| {
b.iter(|| match size {
1 => dioxus::tui::launch_cfg(
app3,
Config {
..Default::default()
},
),
2 => dioxus::tui::launch_cfg(
app6,
Config {
..Default::default()
},
),
3 => dioxus::tui::launch_cfg(
app9,
Config {
..Default::default()
},
),
4 => dioxus::tui::launch_cfg(
app12,
Config {
..Default::default()
},
),
5 => dioxus::tui::launch_cfg(
app15,
Config {
..Default::default()
},
),
6 => dioxus::tui::launch_cfg(
app18,
Config {
..Default::default()
},
),
7 => dioxus::tui::launch_cfg(
app21,
Config {
..Default::default()
},
),
8 => dioxus::tui::launch_cfg(
app24,
Config {
..Default::default()
},
),
_ => (),
})
},
);
}
}
#[derive(Props, PartialEq)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element {
let count = use_state(&cx, || 0);
let x = cx.props.x * 2;
let y = cx.props.y * 2;
let hue = cx.props.hue;
let display_hue = cx.props.hue as u32 / 10;
let count = count.get();
let alpha = cx.props.alpha + (count % 100) as f32;
cx.render(rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
})
}
#[derive(Props, PartialEq)]
struct GridProps {
size: usize,
}
#[allow(non_snake_case)]
fn Grid(cx: Scope<GridProps>) -> Element {
let size = cx.props.size;
let count = use_state(&cx, || 0);
let counts = use_ref(&cx, || vec![0; size * size]);
let ctx: TuiContext = cx.consume_context().unwrap();
if *count.get() + 1 >= (size * size) {
ctx.quit();
} else {
counts.with_mut(|c| {
let i = *count.current();
c[i] += 1;
c[i] = c[i] % 360;
});
count.with_mut(|i| {
*i += 1;
*i = *i % (size * size);
});
}
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..size).map(|x|
{
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..size).map(|y|
{
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
let key = format!("{}-{}", x, y);
cx.render(rsx! {
Box{
x: x,
y: y,
alpha: 100.0,
hue: alpha,
key: "{key}",
}
})
}
)
}
})
}
)
}
})
}
fn app3(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 3,
}
}
})
}
fn app6(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 6,
}
}
})
}
fn app9(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 9,
}
}
})
}
fn app12(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 12,
}
}
})
}
fn app15(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 15,
}
}
})
}
fn app18(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 18,
}
}
})
}
fn app21(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 21,
}
}
})
}
fn app24(cx: Scope) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: 24,
}
}
})
}

View file

@ -1,6 +1,21 @@
#[derive(Default, Clone, Copy)] #[derive(Clone, Copy)]
pub struct Config { pub struct Config {
pub rendering_mode: RenderingMode, pub rendering_mode: RenderingMode,
/// Controls if the terminal quit when the user presses `ctrl+c`?
/// To handle quiting on your own, use the [crate::TuiContext] root context.
pub ctrl_c_quit: bool,
/// Controls if the terminal should dislay anything, usefull for testing.
pub headless: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
rendering_mode: Default::default(),
ctrl_c_quit: true,
headless: false,
}
}
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]

View file

@ -6,13 +6,19 @@ use crossterm::{
}; };
use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::exports::futures_channel::mpsc::unbounded;
use dioxus_core::*; use dioxus_core::*;
use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt}; use futures::{
channel::mpsc::{UnboundedReceiver, UnboundedSender},
pin_mut, StreamExt,
};
use std::{ use std::{
collections::HashMap, collections::HashMap,
io, io,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use stretch2::{prelude::Size, Stretch}; use stretch2::{
prelude::{Node, Size},
Stretch,
};
use style::RinkStyle; use style::RinkStyle;
use tui::{backend::CrosstermBackend, Terminal}; use tui::{backend::CrosstermBackend, Terminal};
@ -30,6 +36,16 @@ pub use hooks::*;
pub use layout::*; pub use layout::*;
pub use render::*; pub use render::*;
#[derive(Clone)]
pub struct TuiContext {
tx: UnboundedSender<InputEvent>,
}
impl TuiContext {
pub fn quit(&self) {
self.tx.unbounded_send(InputEvent::Close).unwrap();
}
}
pub fn launch(app: Component<()>) { pub fn launch(app: Component<()>) {
launch_cfg(app, Config::default()) launch_cfg(app, Config::default())
} }
@ -37,8 +53,34 @@ pub fn launch(app: Component<()>) {
pub fn launch_cfg(app: Component<()>, cfg: Config) { pub fn launch_cfg(app: Component<()>, cfg: Config) {
let mut dom = VirtualDom::new(app); let mut dom = VirtualDom::new(app);
let (tx, rx) = unbounded(); let (tx, rx) = unbounded();
// Setup input handling
let (event_tx, event_rx) = unbounded();
let event_tx_clone = event_tx.clone();
if !cfg.headless {
std::thread::spawn(move || {
let tick_rate = Duration::from_millis(100);
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout).unwrap() {
let evt = crossterm::event::read().unwrap();
event_tx.unbounded_send(InputEvent::UserInput(evt)).unwrap();
}
if last_tick.elapsed() >= tick_rate {
event_tx.unbounded_send(InputEvent::Tick).unwrap();
last_tick = Instant::now();
}
}
});
}
let cx = dom.base_scope(); let cx = dom.base_scope();
cx.provide_root_context(TuiContext { tx: event_tx_clone });
let (handler, state) = RinkInputHandler::new(rx, cx); let (handler, state) = RinkInputHandler::new(rx, cx);
@ -46,7 +88,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
dom.rebuild(); dom.rebuild();
render_vdom(&mut dom, tx, handler, cfg).unwrap(); render_vdom(&mut dom, event_rx, tx, handler, cfg).unwrap();
} }
pub struct TuiNode<'a> { pub struct TuiNode<'a> {
@ -56,35 +98,13 @@ pub struct TuiNode<'a> {
pub node: &'a VNode<'a>, pub node: &'a VNode<'a>,
} }
pub fn render_vdom( fn render_vdom(
vdom: &mut VirtualDom, vdom: &mut VirtualDom,
mut event_reciever: UnboundedReceiver<InputEvent>,
ctx: UnboundedSender<TermEvent>, ctx: UnboundedSender<TermEvent>,
handler: RinkInputHandler, handler: RinkInputHandler,
cfg: Config, cfg: Config,
) -> Result<()> { ) -> Result<()> {
// Setup input handling
let (tx, mut rx) = unbounded();
std::thread::spawn(move || {
let tick_rate = Duration::from_millis(100);
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout).unwrap() {
let evt = crossterm::event::read().unwrap();
tx.unbounded_send(InputEvent::UserInput(evt)).unwrap();
}
if last_tick.elapsed() >= tick_rate {
tx.unbounded_send(InputEvent::Tick).unwrap();
last_tick = Instant::now();
}
}
});
tokio::runtime::Builder::new_current_thread() tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build()? .build()?
@ -92,13 +112,17 @@ pub fn render_vdom(
/* /*
Get the terminal to calcualte the layout from Get the terminal to calcualte the layout from
*/ */
enable_raw_mode().unwrap(); let mut terminal = (!cfg.headless).then(|| {
let mut stdout = std::io::stdout(); enable_raw_mode().unwrap();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); let mut stdout = std::io::stdout();
let backend = CrosstermBackend::new(io::stdout()); execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
let mut terminal = Terminal::new(backend).unwrap(); let backend = CrosstermBackend::new(io::stdout());
Terminal::new(backend).unwrap()
});
terminal.clear().unwrap(); if let Some(terminal) = &mut terminal {
terminal.clear().unwrap();
}
loop { loop {
/* /*
@ -126,34 +150,51 @@ pub fn render_vdom(
let root_layout = nodes[&node_id].layout; let root_layout = nodes[&node_id].layout;
let mut events = Vec::new(); let mut events = Vec::new();
terminal.draw(|frame| { fn resize(dims: tui::layout::Rect, stretch: &mut Stretch, root_layout: Node) {
// size is guaranteed to not change when rendering
let dims = frame.size();
let width = dims.width; let width = dims.width;
let height = dims.height; let height = dims.height;
layout
stretch
.compute_layout( .compute_layout(
root_layout, root_layout,
Size { Size {
width: stretch2::prelude::Number::Defined(width as f32), width: stretch2::prelude::Number::Defined((width - 1) as f32),
height: stretch2::prelude::Number::Defined(height as f32), height: stretch2::prelude::Number::Defined((height - 1) as f32),
}, },
) )
.unwrap(); .unwrap();
}
// resolve events before rendering if let Some(terminal) = &mut terminal {
events = handler.get_events(vdom, &layout, &mut nodes, root_node); terminal.draw(|frame| {
render::render_vnode( // size is guaranteed to not change when rendering
frame, resize(frame.size(), &mut layout, root_layout);
&layout,
&mut nodes, // resolve events before rendering
vdom, events = handler.get_events(vdom, &layout, &mut nodes, root_node);
root_node, render::render_vnode(
&RinkStyle::default(), frame,
cfg, &layout,
&mut nodes,
vdom,
root_node,
&RinkStyle::default(),
cfg,
);
assert!(nodes.is_empty());
})?;
} else {
resize(
tui::layout::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
&mut layout,
root_layout,
); );
assert!(nodes.is_empty()); }
})?;
for e in events { for e in events {
vdom.handle_message(SchedulerMsg::Event(e)); vdom.handle_message(SchedulerMsg::Event(e));
@ -164,7 +205,7 @@ pub fn render_vdom(
let wait = vdom.wait_for_work(); let wait = vdom.wait_for_work();
pin_mut!(wait); pin_mut!(wait);
match select(wait, rx.next()).await { match select(wait, event_reciever.next()).await {
Either::Left((_a, _b)) => { Either::Left((_a, _b)) => {
// //
} }
@ -194,13 +235,15 @@ pub fn render_vdom(
vdom.work_with_deadline(|| false); vdom.work_with_deadline(|| false);
} }
disable_raw_mode()?; if let Some(terminal) = &mut terminal {
execute!( disable_raw_mode()?;
terminal.backend_mut(), execute!(
LeaveAlternateScreen, terminal.backend_mut(),
DisableMouseCapture LeaveAlternateScreen,
)?; DisableMouseCapture
terminal.show_cursor()?; )?;
terminal.show_cursor()?;
}
Ok(()) Ok(())
}) })