Improved docs

This commit is contained in:
2026-01-31 21:01:30 +01:00
parent 6fe25f0320
commit 37d2de41ce
54 changed files with 2 additions and 54 deletions
@@ -0,0 +1,180 @@
---
sidebar_position: 0
title: Performance Benchmarking
---
# Performance Benchmarking
Optimizing rendering performance is crucial for smooth and responsive TUI applications, especially those with complex layouts or frequent updates. OSUI provides a built-in `Benchmark` engine wrapper that allows you to easily measure the rendering speed of your components.
## The `Benchmark` Engine
The `osui::engine::benchmark` module offers the `Benchmark<T: Engine>` struct, which wraps an existing engine (like `Console`) and records detailed timing information for its rendering cycles.
### How it works:
1. You instantiate a `Benchmark` by passing it another `Engine` (e.g., `Console::new()`).
2. When you call `benchmark_engine.run(YourApp {})`, the `Benchmark` engine takes over.
3. Instead of running the application indefinitely, it performs a fixed number of render cycles (defaulting to 40 in `Benchmark::run`).
4. For each cycle, it precisely measures the time taken to `render` your root component.
5. After all cycles, it clears the screen and returns a `BenchmarkResult` containing statistics.
## `BenchmarkResult`
The `BenchmarkResult` struct holds the collected performance metrics:
```rust
pub struct BenchmarkResult {
pub average: u128, // Average render time in microseconds
pub min: u128, // Minimum render time in microseconds
pub max: u128, // Maximum render time in microseconds
pub total_render: u128, // Sum of all render times in microseconds
pub total: u128, // Total time spent during the benchmark (including setup)
}
```
These values are typically in microseconds (`µs`).
## Basic Usage Example
Let's use the `simple_benchmark.rs` example to see the `Benchmark` engine in action:
```rust title="examples/simple_benchmark.rs"
use osui::prelude::*;
use std::sync::Arc; // Needed for Arc<Context>
pub fn main() {
// 1. Create a Console engine instance.
let console_engine = Console::new();
// 2. Wrap it with the Benchmark engine.
let benchmark_engine = Benchmark::new(console_engine);
// 3. Run your application (or component) through the Benchmark engine.
let benchmark_result = benchmark_engine.run(App {}).expect("Failed to run benchmark");
// 4. Print the results.
println!("Avg: {} μs", benchmark_result.average);
println!("Min: {} μs", benchmark_result.min);
println!("Max: {} μs", benchmark_result.max);
println!("Tot: {} μs", benchmark_result.total);
println!("Tot Render: {} μs", benchmark_result.total_render);
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
"Hello, world!"
}
.view(&cx)
}
```
To run this example:
```bash
cargo run --example simple_benchmark
```
You will see output similar to:
```
Avg: 1078 μs
Min: 1078 μs
Max: 1078 μs
Tot: 43120 μs
Tot Render: 43120 μs
```
*(Note: Actual values will vary based on your system and terminal emulator.)*
## Advanced Usage: Benchmarking Complex Scenarios
The `benchmark.rs` example demonstrates how to benchmark nested components and iterate through different complexity levels. This is useful for identifying performance bottlenecks in specific UI patterns.
```rust title="examples/benchmark.rs"
use osui::prelude::*;
use std::collections::HashMap; // Needed for HashMap
pub fn main() {
let engine = Arc::new(Benchmark::new(Console::new())); // Wrap Console in Benchmark, then Arc it.
let mut benchmark_results: HashMap<(usize, usize), BenchmarkResult> = HashMap::new();
// Iterate through different levels of nesting (n) and iterations (i)
for i in 0..15 {
for n in 0..15 {
let res = {
let mut results = Vec::with_capacity(6);
// Run each specific benchmark configuration multiple times (e.g., 6)
// to get more consistent results, then pick the median (index 3 after sort).
for _ in 0..6 {
results.push(
engine
.run(App {
n: n * 72, // Scale nesting depth
i: i * 72, // Scale iteration count (for loops)
})
.expect("Failed to run engine"),
);
}
results.sort_by_key(|r| r.total_render); // Sort by total render time
results[3].clone() // Take the median result
};
benchmark_results.insert((i, n as usize), res);
}
}
// Output results in CSV format
println!("Iterx72,Nestingx72,Time µs");
for ((i, n), bench) in benchmark_results.iter() {
println!("{i},{n},{}", bench.total_render);
}
}
// A recursive component that creates nested children and loops
#[component]
fn App(cx: &Arc<Context>, n: usize, i: usize) -> View {
let n = n.clone(); // Clone props for closure (necessary for #[component] macro)
let i = i.clone();
if n == 0 {
// Base case: deepest level, render a simple string
rsx! {
"Hello, world!"
}
.view(&cx)
} else {
// Recursive case: create `i` number of children, each with reduced nesting `n-1`
rsx! {
@for _ in (0..i) { // Loop `i` times
App { n: n - 1, i: 0 } // Create a nested App component
}
}
.view(&cx)
}
}
```
This example:
* Uses an `Arc<Benchmark>` to allow `run` to be called multiple times.
* Iterates through different `n` (nesting depth) and `i` (number of children in a loop) values.
* Runs each configuration multiple times and takes the median `total_render` to reduce noise.
* Prints the results in CSV format, which can be easily imported into spreadsheet software for analysis (like the `benchmark.csv` in the repository).
## Interpreting Results
* **`average`, `min`, `max`**: Provide insight into the consistency of your rendering performance. A large difference between `min` and `max` might indicate inconsistencies or external factors affecting performance.
* **`total_render`**: The sum of all individual render cycle times. This is often the most important metric for overall performance.
* **`total`**: The total time for the benchmark process, including setup. This is less about rendering speed and more about the overhead of the benchmark itself.
When analyzing benchmarks, look for:
* **Linear vs. Non-linear Scaling**: How does `total_render` increase as you increase nesting depth or the number of components? Ideally, it should scale linearly.
* **Bottlenecks**: Can you isolate which components or `rsx!` patterns (e.g., complex loops, many dynamic scopes) contribute most to render time?
* **Regression**: Use benchmarks in your CI/CD pipeline to detect performance regressions introduced by new code.
By leveraging OSUI's `Benchmark` engine, you can gain valuable insights into your TUI application's performance characteristics and make data-driven decisions for optimization.
**Next:** Learn how to customize OSUI by [Implementing a Custom Engine](./01-customizing-the-engine.md).
@@ -0,0 +1,260 @@
---
sidebar_position: 1
title: Customizing the Engine
---
# Customizing the Engine
OSUI's `Engine` and `CommandExecutor` traits are designed to be highly extensible. While the `Console` engine provides `crossterm`-based terminal rendering, you might want to create a custom engine for various reasons:
* **Different Rendering Backend**: Render to a graphical window (e.g., using `minifb` or `pixels`), a web canvas, or a specific hardware display.
* **Headless Testing**: Create a dummy engine that doesn't render anything but processes all commands and component logic, useful for fast unit or integration tests.
* **Logging/Debugging**: An engine that logs all `DrawInstruction`s to a file for analysis.
* **Specialized Behavior**: Implement custom render loops, input handling, or command processing.
This guide will walk you through the process of implementing your own `Engine` and `CommandExecutor`.
## Implementing `CommandExecutor`
First, let's define a custom `CommandExecutor`. This trait is responsible for processing commands issued by components (e.g., `cx.stop()`).
```rust
use osui::prelude::*;
use std::{
any::Any,
sync::{Arc, Mutex},
};
// Define a custom command
#[derive(Debug, Clone)]
pub struct CustomCommand(pub String);
impl Command for CustomCommand {
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct MyCustomExecutor {
running: Mutex<bool>,
received_commands: Mutex<Vec<CustomCommand>>,
}
impl MyCustomExecutor {
pub fn new() -> Arc<Self> {
Arc::new(Self {
running: Mutex::new(true),
received_commands: Mutex::new(Vec::new()),
})
}
pub fn stop_engine(&self) -> crate::Result<()> {
*self.running.lock()? = false;
Ok(())
}
pub fn get_status(&self) -> bool {
*self.running.lock().unwrap()
}
pub fn get_received_commands(&self) -> Vec<CustomCommand> {
self.received_commands.lock().unwrap().clone()
}
}
impl CommandExecutor for MyCustomExecutor {
fn execute_command(&self, command: &Arc<dyn Command>) -> crate::Result<()> {
let command_any = command.as_any();
// Handle built-in Stop command
if let Some(commands::Stop) = command_any.downcast_ref::<commands::Stop>() {
println!("MyCustomExecutor: Received Stop command.");
return self.stop_engine();
}
// Handle our custom command
if let Some(custom_cmd) = command_any.downcast_ref::<CustomCommand>() {
println!("MyCustomExecutor: Received CustomCommand: {:?}", custom_cmd);
self.received_commands.lock().unwrap().push(custom_cmd.clone());
return Ok(());
}
println!("MyCustomExecutor: Unhandled command.");
Ok(())
}
}
```
## Implementing `Engine`
Now, let's create a simple "headless" engine that doesn't draw to the terminal but just logs rendering events.
```rust
use osui::prelude::*;
use std::sync::Arc;
pub struct MyHeadlessEngine {
executor: Arc<MyCustomExecutor>,
log_output: Mutex<Vec<String>>,
}
impl MyHeadlessEngine {
pub fn new() -> Self {
Self {
executor: MyCustomExecutor::new(),
log_output: Mutex::new(Vec::new()),
}
}
// Helper to log messages
fn log(&self, msg: &str) {
self.log_output.lock().unwrap().push(msg.to_string());
}
pub fn get_log(&self) -> Vec<String> {
self.log_output.lock().unwrap().clone()
}
}
impl Engine for MyHeadlessEngine {
fn run<C: ComponentImpl + 'static>(&self, component: C) -> crate::Result<()> {
self.log("Engine: Initializing component...");
let cx = self.init(component);
while self.executor.get_status() {
self.log("Engine: Starting render cycle...");
self.render(&cx);
self.log("Engine: Render cycle complete. Delaying...");
self.render_delay(); // Use default delay or implement custom
}
self.log("Engine: Application stopped.");
Ok(())
}
fn init<C: ComponentImpl + 'static>(&self, component: C) -> Arc<Context> {
// Perform any setup needed for your custom engine
self.log("Engine: Component initialized.");
let cx = Context::new(component, self.executor.clone());
cx.refresh(); // Initial render of the component
cx
}
fn render(&self, cx: &Arc<Context>) {
let area = Area { x: 0, y: 0, width: 80, height: 24 }; // Define a virtual screen size
let draw_ctx = self.render_view(&area, &cx.get_view());
self.draw_context(&draw_ctx);
}
// No actual delay for headless, or keep default for testing loop speed
fn render_delay(&self) {
// crate::sleep(16); // Uncomment for actual delay
}
fn render_view(&self, area: &Area, view: &View) -> DrawContext {
self.log(&format!("Engine: Rendering view in area: {:?}", area));
let mut context = DrawContext::new(area.clone());
view(&mut context); // Execute the View closure to populate DrawContext
context
}
fn draw_context(&self, ctx: &DrawContext) {
self.log(&format!("Engine: Drawing context with {} instructions.", ctx.drawing.len()));
for inst in &ctx.drawing {
match inst {
DrawInstruction::Text(point, text) => self.log(&format!(" Draw Text at {:?}: '{}'", point, text)),
DrawInstruction::View(area, view) => {
self.log(&format!(" Draw Child View in area: {:?}", area));
self.draw_context(&self.render_view(area, view)); // Recursively render child views
},
DrawInstruction::Child(point, child_ctx) => {
self.log(&format!(" Draw Child DrawContext at {:?}.", point));
self.draw_context(child_ctx); // Recursively draw child contexts
},
}
}
}
fn executor(&self) -> Arc<dyn CommandExecutor> {
self.executor.clone()
}
}
```
## Using Your Custom Engine
```rust
use osui::prelude::*;
use std::sync::Arc;
// (Include MyHeadlessEngine, MyCustomExecutor, CustomCommand definitions here)
#[component]
fn MyApp(cx: &Arc<Context>) -> View {
let counter = use_state(0);
use_effect(
{
let cx = cx.clone();
let counter = counter.clone();
move || {
// Periodically increment counter and emit custom command
loop {
sleep(200);
let new_val = *counter.get() + 1;
counter.set(new_val);
if new_val >= 3 {
cx.execute(CustomCommand(format!("Counter reached {}", new_val))).expect("Cmd failed");
cx.stop().expect("Stop failed");
break;
}
}
}
},
&[], // Run once on mount
);
rsx! {
format!("Counter value: {}", counter.get_dl())
}.view(&cx)
}
fn main() {
let my_engine = MyHeadlessEngine::new();
let executor = my_engine.executor.clone(); // Get a reference to the executor
my_engine.run(MyApp {}).expect("Failed to run custom engine");
println!("\n--- Engine Log ---");
for line in my_engine.get_log() {
println!("{}", line);
}
println!("\n--- Received Commands ---");
for cmd in executor.get_received_commands() {
println!("{:?}", cmd);
}
}
```
### Explanation:
1. **`MyCustomExecutor`**: Implements `CommandExecutor`. It handles the built-in `commands::Stop` and our new `CustomCommand`. It also keeps a log of received custom commands for verification.
2. **`MyHeadlessEngine`**: Implements `Engine`.
* It takes `MyCustomExecutor` as its command executor.
* `run` method establishes a basic loop that continues as long as `executor.get_status()` is `true`.
* `render` orchestrates the `render_view` and `draw_context` calls.
* `render_view` executes the component `View` and collects `DrawInstruction`s.
* `draw_context` iterates through `DrawInstruction`s, logging them instead of actually drawing to a terminal. It recursively handles `DrawInstruction::View` and `DrawInstruction::Child`.
3. **`MyApp` Component**:
* Uses `use_state` for a counter.
* Uses `use_effect` to periodically increment the counter.
* When the counter reaches 3, it `execute`s our `CustomCommand` and then `stop()`s the engine.
4. **`main` Function**:
* Instantiates `MyHeadlessEngine`.
* Calls `my_engine.run(MyApp {})`.
* After the engine stops, it prints the internal log and received commands from the executor, allowing you to verify that component logic and commands were processed correctly.
By following this pattern, you can integrate OSUI's powerful component and state management system with virtually any rendering or execution environment you desire.
**Next:** Dive into the internals of OSUI's macro system in [Internals: Macros](./02-internals-macros.md).
@@ -0,0 +1,131 @@
---
sidebar_position: 2
title: Internals: Macros
---
# Internals: Macros
OSUI heavily relies on procedural macros to provide its ergonomic, declarative syntax. The `osui-macros` crate contains the logic for the `#[component]` attribute macro and the `rsx!` function macro. Understanding how these macros work under the hood gives you a deeper insight into OSUI's architecture and capabilities.
## Introduction to Procedural Macros
Procedural macros are functions that operate on the Rust syntax tree (Abstract Syntax Tree or AST) during compilation. They receive `TokenStream`s as input and produce `TokenStream`s as output, effectively transforming your code. OSUI's macros leverage the following crates:
* **`syn`**: A parser for Rust's syntax tree. It allows macros to parse input `TokenStream`s into structured Rust AST types (like `ItemFn`, `Expr`, `Path`, etc.).
* **`quote`**: A quasiquoting library that makes it easy to generate Rust code (as `TokenStream`s) from AST fragments.
* **`proc-macro2`**: Provides types like `TokenStream` and `Ident` that are compatible with `syn` and `quote`, enabling ergonomic manipulation of tokens.
## `#[component]` Attribute Macro (`macros/src/lib.rs`)
The `#[component]` macro transforms a Rust function into an OSUI component struct.
### Input
It takes an `ItemFn` (the parsed function definition) as input.
```rust
// Original function in user's code
#[component]
pub fn MyComponent(cx: &Arc<Context>, prop1: &String, prop2: &i32) -> View {
// ... function body ...
}
```
### Core Logic
1. **Parse Function Signature**:
* It extracts the function's name (`MyComponent`).
* It validates the first argument `cx: &Arc<Context>`.
* It iterates through the remaining arguments (`prop1: &String`, `prop2: &i32`), which become the component's props.
2. **Generate Component Struct**: For each prop parameter, it determines the *owned* type (e.g., `&String` becomes `String`, `&i32` becomes `i32`). It then uses `quote!` to generate a new struct:
```rust
pub struct MyComponent {
pub prop1: String, // Owned types
pub prop2: i32,
}
```
3. **Implement `ComponentImpl`**: It then generates an `impl ComponentImpl for MyComponent` block. The `call` method of this trait:
* Takes `&self` and `cx: &Arc<Context>`.
* Internally calls a generated `Self::component` method (which is your original function's body).
* Passes `cx` and references (`&self.prop1`, `&self.prop2`) to the stored props from the generated struct.
```rust
impl MyComponent {
// This is your original function, renamed and wrapped
pub fn component(cx: &Arc<Context>, prop1: &str, prop2: &i32) -> View { /* ... body ... */ }
}
impl ComponentImpl for MyComponent {
fn call(&self, cx: &Arc<Context>) -> View {
Self::component(cx, &self.prop1, &self.prop2) // Pass references to owned props
}
}
```
### Output
The macro replaces the original function with the generated struct, its `component` method, and the `ComponentImpl` implementation. This transformation makes `MyComponent` a valid OSUI component that can be instantiated with props in `rsx!`.
## `rsx!` Function Macro (`macros/src/parse.rs` & `macros/src/emit.rs`)
The `rsx!` macro is more complex, involving a two-step process: parsing the custom syntax and then emitting standard Rust code.
### 1. Parsing (`macros/src/parse.rs`)
The `parse` module defines an AST (Abstract Syntax Tree) for the `rsx!` syntax.
* **`RsxRoot`**: The top-level container, holding a vector of `RsxNode`s.
* **`RsxNode`**: An enum representing different types of nodes in the `rsx!` tree:
* `Text(LitStr)`: For `"Hello"` literals.
* `Expr(Expr)`: For `@{some_expression}` blocks.
* `Component { path: Path, props: Vec<RsxProp>, children: Vec<RsxNode> }`: For `MyComponent { prop: val, ... }`.
* `Mount(Ident)`: For `!my_mount_hook`.
* `If { deps: Vec<Dep>, cond: Expr, children: Vec<RsxNode> }`: For `@if condition { ... }`.
* `For { deps: Vec<Dep>, pat: Pat, expr: Expr, children: Vec<RsxNode> }`: For `@for item in items { ... }`.
* **`RsxProp`**: Represents a `name: value` pair for component props.
* **`Dep`**: Represents a dependency for dynamic blocks (`%my_state as my_alias`).
The `parse::RsxRoot::parse` method uses `syn`'s `ParseStream` to tokenize the `rsx!` input and build this AST. It intelligently differentiates between text, expressions, component names, and control flow keywords (`@if`, `@for`, `!`).
### 2. Emitting (`macros/src/emit.rs`)
The `emit` module takes the parsed `RsxRoot` AST and converts it into a `TokenStream` of standard Rust code that constructs `osui::frontend::Rsx` objects.
* **`emit_rsx(root: RsxRoot)`**: The entry point, which initializes an `osui::frontend::Rsx` object and then iterates through the `root.nodes`.
* **`emit_node_scope(node: &RsxNode)`**: For each `RsxNode`, it generates code that calls the appropriate `Rsx` builder method:
* **`RsxNode::Text`**: Emits `r.static_scope(move |scope| { scope.view(...) });` which draws text.
* **`RsxNode::Expr`**: Emits `r.child(expression);`.
* **`RsxNode::Component`**: Emits `r.static_scope(move |scope| { scope.child(ComponentName { props: ... }, None); });`. If children are present, they are recursively emitted into a nested `Rsx` object and passed as the `children` prop.
* **`RsxNode::Mount`**: Emits `mount_hook_instance.mount();`.
* **`RsxNode::If`**: Emits `r.dynamic_scope(move |scope| { if condition { ... } else { ... } }, dependencies);`. The `dependencies` are converted to `Vec<Arc<dyn HookDependency>>`.
* **`RsxNode::For`**: Similar to `If`, emits `r.dynamic_scope(move |scope| { for pattern in expr { ... } }, dependencies);`.
### Output
The `rsx!` macro produces a `TokenStream` that looks something like this (simplified):
```rust
// For: rsx! { "Hello" MyComponent { prop: value } }
{
let mut r = osui::frontend::Rsx::new();
r.static_scope(move |scope| {
scope.view(std::sync::Arc::new(move |ctx| {
ctx.draw_text(osui::render::Point { x: 0, y: 0 }, &format!("Hello"))
}));
});
r.static_scope(move |scope| {
scope.child(
MyComponent { prop: value.to_string() }, // Note: Prop value converted to owned type
None,
);
});
r
}
```
This generated code, when compiled, constructs the `osui::frontend::Rsx` object that OSUI's runtime can then interpret to build the component tree and render the UI.
## Summary
The `osui-macros` crate plays a pivotal role in shaping OSUI's developer experience. `#[component]` streamlines component definition, while `rsx!` provides a powerful, declarative way to compose UIs by transforming custom syntax into efficient runtime calls. These macros are complex but essential for creating a modern, React-like development flow in a TUI environment.
**Next:** Learn how you can contribute to the OSUI project in [Contributing](./03-contributing.md).
@@ -0,0 +1,115 @@
---
sidebar_position: 3
title: Contributing
---
# Contributing to OSUI
We welcome and appreciate contributions from the community! Whether it's reporting bugs, suggesting features, improving documentation, or submitting code, your help makes OSUI better for everyone.
## How to Contribute
### 1. Report Bugs
If you find a bug, please open an issue on our [GitHub repository](https://github.com/osui-rs/osui/issues). When reporting a bug, please include:
* A clear and concise description of the bug.
* Steps to reproduce the behavior.
* Expected behavior.
* Actual behavior.
* Your OSUI version, Rust version, and operating system.
* Any relevant code snippets or error messages.
### 2. Suggest Features
Have an idea for a new feature or improvement? Feel free to open an issue on GitHub to discuss it. Please provide:
* A clear and concise description of the proposed feature.
* Why you think it would be valuable to OSUI.
* Any potential use cases or examples.
### 3. Improve Documentation
Good documentation is vital for any library. If you find errors, omissions, or areas that could be explained more clearly in our docs, please consider:
* Opening an issue to point out the specific area.
* Submitting a pull request with your suggested improvements.
### 4. Contribute Code
If you'd like to contribute code, here's the general workflow:
#### Fork the Repository
First, fork the [osui-rs/osui](https://github.com/osui-rs/osui) repository to your own GitHub account.
#### Clone Your Fork
```bash
git clone https://github.com/YOUR_USERNAME/osui.git
cd osui
```
#### Create a New Branch
Create a new branch for your feature or bug fix. Use a descriptive name:
```bash
git checkout -b feature/my-awesome-feature
# or
git checkout -b bugfix/fix-rendering-issue
```
#### Make Your Changes
* Write clean, idiomatic Rust code.
* Follow existing code style and conventions.
* Add comments where necessary to explain complex logic.
* **Write Tests**: If you're adding new features or fixing bugs, please include appropriate unit and/or integration tests to cover your changes.
* **Update Documentation**: If your changes affect the public API or add new functionality, please update the relevant documentation files.
#### Run Tests
Before submitting a pull request, ensure all existing tests pass and your new tests pass:
```bash
cargo test --workspace
```
#### Format and Lint
Make sure your code is formatted correctly and passes lint checks:
```bash
cargo fmt --all
cargo clippy --all-targets --all-features
```
#### Commit Your Changes
Commit your changes with clear and concise commit messages. A good commit message explains *what* changed and *why*.
```bash
git commit -m "feat: Add new awesome feature"
# or
git commit -m "fix: Resolve rendering issue on Windows"
```
#### Push to Your Fork
```bash
git push origin feature/my-awesome-feature
```
#### Create a Pull Request
Go to the [osui-rs/osui](https://github.com/osui-rs/osui) repository on GitHub and open a new pull request.
* Provide a clear title and description for your pull request.
* Reference any related issues (e.g., "Fixes #123" or "Closes #456").
* The project maintainers will review your PR, provide feedback, and work with you to get it merged.
## Code of Conduct
Please note that this project is released with a [Contributor Code of Conduct](https://github.com/osui-rs/osui/blob/main/CODE_OF_CONDUCT.md). By participating in this project, you agree to abide by its terms.
Thank you for considering contributing to OSUI! Your efforts help foster a vibrant and robust TUI ecosystem in Rust.