Text, many performance improvements
This commit is contained in:
17
examples/reactive_layout_demo/Cargo.toml
Normal file
17
examples/reactive_layout_demo/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "ruin_ui_reactive_layout_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ruin_reactivity = { path = "../../lib/reactivity" }
|
||||
ruin_runtime = { package = "ruin-runtime", path = "../../lib/runtime" }
|
||||
ruin_ui = { path = "../../lib/ui" }
|
||||
ruin_ui_platform_wayland = { path = "../../lib/ui_platform_wayland" }
|
||||
ruin_ui_renderer_wgpu = { path = "../../lib/ui_renderer_wgpu" }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"std",
|
||||
] }
|
||||
593
examples/reactive_layout_demo/src/main.rs
Normal file
593
examples/reactive_layout_demo/src/main.rs
Normal file
@@ -0,0 +1,593 @@
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ruin_reactivity::effect;
|
||||
use ruin_runtime::channel::{mpsc, oneshot};
|
||||
use ruin_runtime::{clear_interval, queue_future, queue_task, set_interval, spawn_worker};
|
||||
use ruin_ui::{
|
||||
Color, Edges, Element, SceneSnapshot, TextStyle, TextSystem, TextWrap, UiSize, WindowSpec,
|
||||
layout_scene_with_text_system,
|
||||
};
|
||||
use ruin_ui_platform_wayland::WaylandWindow;
|
||||
use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer};
|
||||
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
struct MemoryTelemetry {
|
||||
rss_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
fn install_tracing() {
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::new(
|
||||
"info,ruin_runtime::runtime=trace,ruin_ui::platform=trace,ruin_reactivity::effect=trace",
|
||||
)
|
||||
});
|
||||
|
||||
let fmt_layer = fmt::layer()
|
||||
.with_target(true)
|
||||
.with_thread_ids(true)
|
||||
.with_thread_names(true)
|
||||
.compact();
|
||||
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt_layer)
|
||||
.try_init();
|
||||
}
|
||||
|
||||
enum WorkerCommand {
|
||||
ReplaceScene(SceneSnapshot),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
enum WorkerEvent {
|
||||
ViewportChanged(UiSize),
|
||||
FramePresented(Instant),
|
||||
Closed,
|
||||
}
|
||||
|
||||
struct WorkerState {
|
||||
window: WaylandWindow,
|
||||
renderer: WgpuSceneRenderer,
|
||||
latest_scene: Option<SceneSnapshot>,
|
||||
render_queued: bool,
|
||||
closed_emitted: bool,
|
||||
pending_viewport: Option<UiSize>,
|
||||
viewport_request_in_flight: Option<UiSize>,
|
||||
event_tx: mpsc::UnboundedSender<WorkerEvent>,
|
||||
}
|
||||
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() {
|
||||
install_tracing();
|
||||
|
||||
run_demo()
|
||||
.await
|
||||
.expect("reactive layout demo should run successfully");
|
||||
}
|
||||
|
||||
async fn run_demo() -> Result<(), Box<dyn Error>> {
|
||||
let initial_size = UiSize::new(960.0, 640.0);
|
||||
let viewport = ruin_reactivity::cell(initial_size);
|
||||
let animation_elapsed = ruin_reactivity::cell(Duration::ZERO);
|
||||
let memory_telemetry = ruin_reactivity::cell(sample_memory_telemetry());
|
||||
let version = Rc::new(std::cell::Cell::new(0u64));
|
||||
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
||||
let animation_epoch = Instant::now();
|
||||
let (scene_tx, mut scene_rx) = mpsc::unbounded_channel::<WorkerCommand>();
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<WorkerEvent>();
|
||||
let (closed_tx, mut closed_rx) = oneshot::channel::<()>();
|
||||
let closed_tx = Rc::new(RefCell::new(Some(closed_tx)));
|
||||
|
||||
let worker = spawn_worker(
|
||||
move || {
|
||||
let window = WaylandWindow::open(
|
||||
WindowSpec::new("Reactive layout demo")
|
||||
.app_id("dev.ruin.reactive-layout")
|
||||
.requested_inner_size(initial_size),
|
||||
)
|
||||
.expect("Wayland window should open");
|
||||
let renderer = WgpuSceneRenderer::new(
|
||||
window.surface_target(),
|
||||
initial_size.width as u32,
|
||||
initial_size.height as u32,
|
||||
)
|
||||
.expect("wgpu renderer should initialize");
|
||||
let state = Rc::new(RefCell::new(WorkerState {
|
||||
window,
|
||||
renderer,
|
||||
latest_scene: None,
|
||||
render_queued: false,
|
||||
closed_emitted: false,
|
||||
pending_viewport: None,
|
||||
viewport_request_in_flight: None,
|
||||
event_tx,
|
||||
}));
|
||||
|
||||
queue_future({
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
while let Some(command) = scene_rx.recv().await {
|
||||
match command {
|
||||
WorkerCommand::ReplaceScene(scene) => {
|
||||
{
|
||||
let mut state_ref = state.borrow_mut();
|
||||
state_ref.latest_scene = Some(scene);
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
schedule_worker_render(Rc::clone(&state));
|
||||
}
|
||||
WorkerCommand::Shutdown => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
schedule_worker_render(state);
|
||||
},
|
||||
|| {},
|
||||
);
|
||||
|
||||
let _scene_effect = effect({
|
||||
let viewport = viewport.clone();
|
||||
let animation_elapsed = animation_elapsed.clone();
|
||||
let memory_telemetry = memory_telemetry.clone();
|
||||
let version = Rc::clone(&version);
|
||||
let text_system = Rc::clone(&text_system);
|
||||
let scene_tx = scene_tx.clone();
|
||||
move || {
|
||||
let next_version = version.get().wrapping_add(1);
|
||||
version.set(next_version);
|
||||
let viewport_size = viewport.get();
|
||||
let elapsed = animation_elapsed.get();
|
||||
let telemetry = memory_telemetry.get();
|
||||
let layout_start = Instant::now();
|
||||
let scene = layout_scene_with_text_system(
|
||||
next_version,
|
||||
viewport_size,
|
||||
&build_dashboard_tree(viewport_size, elapsed, telemetry),
|
||||
&mut text_system.borrow_mut(),
|
||||
);
|
||||
tracing::trace!(
|
||||
target: "ruin_ui_reactive_layout_demo::perf",
|
||||
scene_version = next_version,
|
||||
layout_ms = layout_start.elapsed().as_secs_f64() * 1_000.0,
|
||||
"built scene"
|
||||
);
|
||||
let _ = scene_tx.send(WorkerCommand::ReplaceScene(scene));
|
||||
}
|
||||
});
|
||||
let memory_interval = set_interval(Duration::from_millis(500), {
|
||||
let memory_telemetry = memory_telemetry.clone();
|
||||
move || {
|
||||
memory_telemetry.set(sample_memory_telemetry());
|
||||
}
|
||||
});
|
||||
|
||||
queue_future({
|
||||
let viewport = viewport.clone();
|
||||
let animation_elapsed = animation_elapsed.clone();
|
||||
let closed_tx = Rc::clone(&closed_tx);
|
||||
async move {
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
let mut latest_viewport = None;
|
||||
let mut latest_frame = None;
|
||||
let mut closed = false;
|
||||
|
||||
match event {
|
||||
WorkerEvent::ViewportChanged(size) => latest_viewport = Some(size),
|
||||
WorkerEvent::FramePresented(presented_at) => latest_frame = Some(presented_at),
|
||||
WorkerEvent::Closed => closed = true,
|
||||
}
|
||||
|
||||
loop {
|
||||
match event_rx.try_recv() {
|
||||
Ok(WorkerEvent::ViewportChanged(size)) => latest_viewport = Some(size),
|
||||
Ok(WorkerEvent::FramePresented(presented_at)) => {
|
||||
latest_frame = Some(presented_at);
|
||||
}
|
||||
Ok(WorkerEvent::Closed) => {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
Err(mpsc::TryRecvError::Empty) => break,
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(size) = latest_viewport {
|
||||
viewport.set(size);
|
||||
}
|
||||
if let Some(presented_at) = latest_frame {
|
||||
animation_elapsed.set(presented_at.saturating_duration_since(animation_epoch));
|
||||
}
|
||||
if closed {
|
||||
if let Some(sender) = closed_tx.borrow_mut().take() {
|
||||
let _ = sender.send(());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
println!("Opening reactive layout demo window...");
|
||||
|
||||
let _ = closed_rx.recv().await;
|
||||
clear_interval(&memory_interval);
|
||||
let _ = scene_tx.send(WorkerCommand::Shutdown);
|
||||
let _ = worker;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schedule_worker_render(state: Rc<RefCell<WorkerState>>) {
|
||||
let should_queue = {
|
||||
let mut state_ref = state.borrow_mut();
|
||||
if state_ref.render_queued {
|
||||
false
|
||||
} else {
|
||||
state_ref.render_queued = true;
|
||||
true
|
||||
}
|
||||
};
|
||||
if !should_queue {
|
||||
return;
|
||||
}
|
||||
|
||||
queue_task({
|
||||
let state = Rc::clone(&state);
|
||||
move || {
|
||||
state.borrow_mut().render_queued = false;
|
||||
render_worker_now(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_worker_now(state: Rc<RefCell<WorkerState>>) {
|
||||
let mut state_ref = state.borrow_mut();
|
||||
state_ref
|
||||
.window
|
||||
.wait_for_events(Duration::from_millis(1))
|
||||
.expect("Wayland wait should succeed");
|
||||
if !state_ref.window.is_running() {
|
||||
drop(state_ref);
|
||||
notify_worker_closed(&state);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(frame) = state_ref.window.prepare_frame() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if frame.resized {
|
||||
state_ref.renderer.resize(frame.width, frame.height);
|
||||
state_ref.window.request_redraw();
|
||||
let resized_viewport = UiSize::new(frame.width as f32, frame.height as f32);
|
||||
state_ref.pending_viewport = Some(resized_viewport);
|
||||
let viewport_to_notify = if state_ref.viewport_request_in_flight.is_none() {
|
||||
state_ref.viewport_request_in_flight = Some(resized_viewport);
|
||||
Some(resized_viewport)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let event_tx = state_ref.event_tx.clone();
|
||||
drop(state_ref);
|
||||
if let Some(size) = viewport_to_notify {
|
||||
let _ = event_tx.send(WorkerEvent::ViewportChanged(size));
|
||||
}
|
||||
schedule_worker_render(state);
|
||||
return;
|
||||
}
|
||||
|
||||
let scene = state_ref.latest_scene.clone();
|
||||
let mut frame_presented_at = None;
|
||||
let mut viewport_to_notify = None;
|
||||
let retry = if let Some(scene) = scene.as_ref() {
|
||||
let current_viewport = UiSize::new(frame.width as f32, frame.height as f32);
|
||||
if scene.logical_size != current_viewport {
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
if state_ref.viewport_request_in_flight != Some(current_viewport) {
|
||||
state_ref.viewport_request_in_flight = Some(current_viewport);
|
||||
viewport_to_notify = Some(current_viewport);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
match state_ref.renderer.render(scene) {
|
||||
Ok(()) => {
|
||||
frame_presented_at = Some(Instant::now());
|
||||
if state_ref.pending_viewport == Some(scene.logical_size) {
|
||||
state_ref.pending_viewport = None;
|
||||
}
|
||||
if state_ref.viewport_request_in_flight == Some(scene.logical_size) {
|
||||
state_ref.viewport_request_in_flight = None;
|
||||
}
|
||||
if state_ref.viewport_request_in_flight.is_none()
|
||||
&& let Some(pending_viewport) = state_ref.pending_viewport
|
||||
&& pending_viewport != scene.logical_size
|
||||
{
|
||||
state_ref.viewport_request_in_flight = Some(pending_viewport);
|
||||
viewport_to_notify = Some(pending_viewport);
|
||||
}
|
||||
false
|
||||
}
|
||||
Err(RenderError::Lost | RenderError::Outdated) => {
|
||||
state_ref.renderer.resize(frame.width, frame.height);
|
||||
state_ref.window.request_redraw();
|
||||
true
|
||||
}
|
||||
Err(RenderError::Timeout | RenderError::Occluded | RenderError::Validation) => {
|
||||
state_ref.window.request_redraw();
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let event_tx = state_ref.event_tx.clone();
|
||||
drop(state_ref);
|
||||
|
||||
if let Some(presented_at) = frame_presented_at {
|
||||
let _ = event_tx.send(WorkerEvent::FramePresented(presented_at));
|
||||
}
|
||||
if let Some(size) = viewport_to_notify {
|
||||
let _ = event_tx.send(WorkerEvent::ViewportChanged(size));
|
||||
}
|
||||
|
||||
if retry {
|
||||
schedule_worker_render(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_worker_closed(state: &Rc<RefCell<WorkerState>>) {
|
||||
let event_tx = {
|
||||
let mut state_ref = state.borrow_mut();
|
||||
if state_ref.closed_emitted {
|
||||
return;
|
||||
}
|
||||
state_ref.closed_emitted = true;
|
||||
state_ref.event_tx.clone()
|
||||
};
|
||||
let _ = event_tx.send(WorkerEvent::Closed);
|
||||
}
|
||||
|
||||
fn build_dashboard_tree(
|
||||
viewport: UiSize,
|
||||
animation_elapsed: Duration,
|
||||
memory_telemetry: MemoryTelemetry,
|
||||
) -> Element {
|
||||
let gutter = (viewport.width * 0.02).clamp(12.0, 28.0);
|
||||
let header_height = (viewport.height * 0.12).clamp(72.0, 112.0);
|
||||
let sidebar_width = lerp(
|
||||
150.0,
|
||||
260.0,
|
||||
triangle_wave_seconds(animation_elapsed, 0.72, 0.0),
|
||||
);
|
||||
let inspector_width = lerp(
|
||||
180.0,
|
||||
280.0,
|
||||
triangle_wave_seconds(animation_elapsed, 0.84, 0.24),
|
||||
);
|
||||
let top_card_pulse = lerp(
|
||||
1.0,
|
||||
1.8,
|
||||
triangle_wave_seconds(animation_elapsed, 0.48, 0.0),
|
||||
);
|
||||
let bottom_left_pulse = lerp(
|
||||
1.0,
|
||||
2.1,
|
||||
triangle_wave_seconds(animation_elapsed, 0.64, 0.12),
|
||||
);
|
||||
let bottom_right_pulse = lerp(
|
||||
1.0,
|
||||
1.7,
|
||||
triangle_wave_seconds(animation_elapsed, 0.80, 0.30),
|
||||
);
|
||||
let memory_label = format_memory_telemetry(memory_telemetry);
|
||||
|
||||
Element::column()
|
||||
.background(Color::rgb(0x10, 0x14, 0x24))
|
||||
.padding(Edges::all(gutter))
|
||||
.gap(gutter)
|
||||
.children([
|
||||
Element::text(
|
||||
format!(
|
||||
"RUIN reactive layout | viewport {:.0} × {:.0} | {memory_label}. Resize the window to watch this block reflow inside the layout tree as the container width changes.",
|
||||
viewport.width, viewport.height,
|
||||
),
|
||||
TextStyle::new(18.0, Color::rgb(0xF4, 0xF7, 0xFF))
|
||||
.with_line_height(24.0)
|
||||
.with_wrap(TextWrap::Word),
|
||||
)
|
||||
.padding(Edges::all(gutter * 0.8))
|
||||
.background(Color::rgb(0x17, 0x22, 0x36)),
|
||||
Element::row()
|
||||
.height(header_height)
|
||||
.padding(Edges::all(gutter * 0.6))
|
||||
.gap(gutter * 0.6)
|
||||
.background(Color::rgb(0x1D, 0x27, 0x42))
|
||||
.children([
|
||||
Element::new()
|
||||
.width((viewport.width * 0.16).clamp(120.0, 220.0))
|
||||
.background(Color::rgb(0x6D, 0x79, 0xFF)),
|
||||
Element::new()
|
||||
.flex(1.0)
|
||||
.background(Color::rgb(0x2D, 0x3E, 0x68)),
|
||||
Element::new()
|
||||
.width((viewport.width * 0.12).clamp(96.0, 180.0))
|
||||
.background(Color::rgb(0x39, 0x68, 0xA3)),
|
||||
]),
|
||||
Element::row().flex(1.0).gap(gutter).children([
|
||||
Element::column()
|
||||
.width(sidebar_width)
|
||||
.gap(gutter * 0.6)
|
||||
.padding(Edges::all(gutter * 0.6))
|
||||
.background(Color::rgb(0x1B, 0x22, 0x36))
|
||||
.children([
|
||||
Element::new()
|
||||
.height(54.0)
|
||||
.background(Color::rgb(0x4A, 0x5A, 0x86)),
|
||||
Element::new()
|
||||
.height(54.0)
|
||||
.background(Color::rgb(0x58, 0x6C, 0xA0)),
|
||||
Element::new()
|
||||
.height(54.0)
|
||||
.background(Color::rgb(0x66, 0x7E, 0xB8)),
|
||||
Element::new()
|
||||
.flex(1.0)
|
||||
.background(Color::rgb(0x2A, 0x33, 0x50)),
|
||||
]),
|
||||
Element::column().flex(1.0).gap(gutter).children([
|
||||
Element::row().height(140.0).gap(gutter).children([
|
||||
Element::new()
|
||||
.flex(top_card_pulse)
|
||||
.background(Color::rgb(0x6A, 0x3D, 0x3D)),
|
||||
Element::new()
|
||||
.flex(1.0)
|
||||
.background(Color::rgb(0x5E, 0x52, 0x2C)),
|
||||
Element::new()
|
||||
.flex(1.0 + (top_card_pulse - 1.0) * 0.6)
|
||||
.background(Color::rgb(0x3A, 0x5E, 0x49)),
|
||||
]),
|
||||
Element::row().flex(1.0).gap(gutter).children([
|
||||
Element::column()
|
||||
.flex(bottom_left_pulse)
|
||||
.gap(gutter * 0.6)
|
||||
.padding(Edges::all(gutter * 0.6))
|
||||
.background(Color::rgb(0x1B, 0x2E, 0x3B))
|
||||
.children([
|
||||
Element::new()
|
||||
.height(64.0)
|
||||
.background(Color::rgb(0x2F, 0x72, 0x91)),
|
||||
Element::new()
|
||||
.flex(1.0)
|
||||
.background(Color::rgb(0x1F, 0x4B, 0x62)),
|
||||
]),
|
||||
Element::column()
|
||||
.flex(bottom_right_pulse)
|
||||
.gap(gutter * 0.6)
|
||||
.padding(Edges::all(gutter * 0.6))
|
||||
.background(Color::rgb(0x2C, 0x1F, 0x38))
|
||||
.children([
|
||||
Element::new()
|
||||
.height(64.0)
|
||||
.background(Color::rgb(0x7B, 0x4D, 0xA6)),
|
||||
Element::new()
|
||||
.flex(1.0)
|
||||
.background(Color::rgb(0x4D, 0x2E, 0x6B)),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Element::column()
|
||||
.width(inspector_width)
|
||||
.gap(gutter * 0.6)
|
||||
.padding(Edges::all(gutter * 0.6))
|
||||
.background(Color::rgb(0x1F, 0x23, 0x34))
|
||||
.children([
|
||||
Element::new()
|
||||
.height(120.0)
|
||||
.background(Color::rgb(0x67, 0x4B, 0x2D)),
|
||||
Element::new()
|
||||
.height(88.0)
|
||||
.background(Color::rgb(0x6C, 0x60, 0x38)),
|
||||
Element::text(
|
||||
LOREM_IPSUM,
|
||||
TextStyle::new(15.0, Color::rgb(0xE8, 0xEB, 0xF7))
|
||||
.with_line_height(22.0)
|
||||
.with_wrap(TextWrap::Word),
|
||||
)
|
||||
.flex(1.0)
|
||||
.padding(Edges::all(gutter * 0.65))
|
||||
.background(Color::rgb(0x30, 0x35, 0x48)),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
fn triangle_wave_seconds(elapsed: Duration, period_seconds: f32, phase_offset_seconds: f32) -> f32 {
|
||||
let period = period_seconds.max(f32::EPSILON);
|
||||
let phase = (elapsed.as_secs_f32() + phase_offset_seconds).rem_euclid(period);
|
||||
let half = period * 0.5;
|
||||
if phase <= half {
|
||||
phase / half
|
||||
} else {
|
||||
(period - phase) / half
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp(start: f32, end: f32, t: f32) -> f32 {
|
||||
start + (end - start) * t.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn sample_memory_telemetry() -> MemoryTelemetry {
|
||||
MemoryTelemetry {
|
||||
rss_bytes: sample_process_rss_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_process_rss_bytes() -> Option<usize> {
|
||||
let statm = fs::read_to_string("/proc/self/statm").ok()?;
|
||||
parse_rss_bytes(&statm, ruin_runtime::page_size())
|
||||
}
|
||||
|
||||
fn parse_rss_bytes(statm: &str, page_size: usize) -> Option<usize> {
|
||||
let resident_pages = statm.split_whitespace().nth(1)?.parse::<usize>().ok()?;
|
||||
resident_pages.checked_mul(page_size)
|
||||
}
|
||||
|
||||
fn format_memory_telemetry(telemetry: MemoryTelemetry) -> String {
|
||||
match telemetry.rss_bytes {
|
||||
Some(bytes) => format!("RSS {}", format_mib(bytes)),
|
||||
None => String::from("RSS unavailable"),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_mib(bytes: usize) -> String {
|
||||
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MemoryTelemetry, format_memory_telemetry, parse_rss_bytes, triangle_wave_seconds};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn parses_resident_pages_from_statm() {
|
||||
let rss = parse_rss_bytes("4096 512 0 0 0 0 0\n", 4096);
|
||||
assert_eq!(rss, Some(2 * 1024 * 1024));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_memory_telemetry() {
|
||||
let telemetry = MemoryTelemetry {
|
||||
rss_bytes: Some(3 * 1024 * 1024),
|
||||
};
|
||||
assert_eq!(format_memory_telemetry(telemetry), "RSS 3.0 MiB");
|
||||
assert_eq!(
|
||||
format_memory_telemetry(MemoryTelemetry::default()),
|
||||
"RSS unavailable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangle_wave_seconds_tracks_elapsed_time() {
|
||||
assert_eq!(
|
||||
triangle_wave_seconds(Duration::from_millis(0), 1.0, 0.0),
|
||||
0.0
|
||||
);
|
||||
assert!((triangle_wave_seconds(Duration::from_millis(500), 1.0, 0.0) - 1.0).abs() < 1e-6);
|
||||
assert!((triangle_wave_seconds(Duration::from_millis(750), 1.0, 0.0) - 0.5).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user