Restaged repo, allocator and runtime implemented, ioring-backed async fs/net/channel/timer primitives
This commit is contained in:
81
lib/runtime/examples/async_fs_showcase.rs
Normal file
81
lib/runtime/examples/async_fs_showcase.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use ruin_runtime::fs::{self, File};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn preview(bytes: &[u8]) -> String {
|
||||
String::from_utf8_lossy(bytes).replace('\n', "\\n")
|
||||
}
|
||||
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let cargo_toml = manifest_dir.join("Cargo.toml");
|
||||
let src_dir = manifest_dir.join("src");
|
||||
|
||||
println!("manifest dir: {}", manifest_dir.display());
|
||||
|
||||
let cargo_meta = fs::metadata(&cargo_toml)
|
||||
.await
|
||||
.expect("Cargo.toml metadata should load");
|
||||
println!(
|
||||
"Cargo.toml: {} bytes, file={}, empty={}",
|
||||
cargo_meta.len(),
|
||||
cargo_meta.is_file(),
|
||||
cargo_meta.is_empty()
|
||||
);
|
||||
|
||||
let mut file = File::open(&cargo_toml)
|
||||
.await
|
||||
.expect("Cargo.toml should open for reading");
|
||||
let file_meta = file
|
||||
.metadata()
|
||||
.await
|
||||
.expect("opened file metadata should load");
|
||||
println!("opened file metadata size: {}", file_meta.len());
|
||||
|
||||
let mut sequential = vec![0; 96];
|
||||
let sequential_read = file
|
||||
.read(&mut sequential)
|
||||
.await
|
||||
.expect("sequential read should succeed");
|
||||
sequential.truncate(sequential_read);
|
||||
println!(
|
||||
"sequential read ({sequential_read} bytes): {}",
|
||||
preview(&sequential)
|
||||
);
|
||||
|
||||
let cloned = file.try_clone().await.expect("file clone should succeed");
|
||||
let mut positioned = [0u8; 48];
|
||||
let positioned_read = cloned
|
||||
.read_at(0, &mut positioned)
|
||||
.await
|
||||
.expect("positioned read should succeed");
|
||||
println!(
|
||||
"positioned read ({positioned_read} bytes): {}",
|
||||
preview(&positioned[..positioned_read])
|
||||
);
|
||||
|
||||
let cargo_text = fs::read_to_string(&cargo_toml)
|
||||
.await
|
||||
.expect("read_to_string should succeed");
|
||||
println!("Cargo.toml line count: {}", cargo_text.lines().count());
|
||||
|
||||
let mut dir = fs::read_dir(&src_dir)
|
||||
.await
|
||||
.expect("src directory should be readable");
|
||||
let mut entries = Vec::new();
|
||||
while let Some(entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.expect("read_dir stream should succeed")
|
||||
{
|
||||
let metadata = entry.metadata().await.expect("entry metadata should load");
|
||||
let kind = if metadata.is_dir() { "dir" } else { "file" };
|
||||
entries.push((entry.file_name().to_string_lossy().into_owned(), kind));
|
||||
}
|
||||
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
|
||||
println!("src entries:");
|
||||
for (name, kind) in entries.iter().take(8) {
|
||||
println!(" - {name} ({kind})");
|
||||
}
|
||||
}
|
||||
160
lib/runtime/examples/channel_showcase.rs
Normal file
160
lib/runtime/examples/channel_showcase.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use ruin_runtime::channel::{mpsc, oneshot};
|
||||
use ruin_runtime::{queue_future, spawn_worker, time::sleep};
|
||||
use std::fmt;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
static START: OnceLock<Instant> = OnceLock::new();
|
||||
static ACTUAL_ORDER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
macro_rules! log_event {
|
||||
($expected:literal, $($arg:tt)*) => {{
|
||||
log_event_impl($expected, format_args!($($arg)*));
|
||||
}};
|
||||
}
|
||||
|
||||
fn log_event_impl(expected: usize, message: fmt::Arguments<'_>) {
|
||||
let actual = ACTUAL_ORDER.fetch_add(1, Ordering::SeqCst);
|
||||
let elapsed = START
|
||||
.get()
|
||||
.expect("showcase start time should be initialized")
|
||||
.elapsed()
|
||||
.as_millis();
|
||||
println!(
|
||||
"[actual {actual:02} | expected {expected:02} | +{elapsed:04}ms | ts {}] {message}",
|
||||
unix_timestamp_millis(),
|
||||
);
|
||||
}
|
||||
|
||||
fn unix_timestamp_millis() -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock should be after the Unix epoch");
|
||||
format!("{}.{:03}", now.as_secs(), now.subsec_millis())
|
||||
}
|
||||
|
||||
enum WorkerEvent {
|
||||
Log(String),
|
||||
PresentRequest {
|
||||
frame: &'static str,
|
||||
ack: oneshot::Sender<&'static str>,
|
||||
},
|
||||
}
|
||||
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
START.get_or_init(Instant::now);
|
||||
|
||||
let (job_tx, mut job_rx) = mpsc::channel::<&'static str>(1);
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<WorkerEvent>();
|
||||
|
||||
let worker = spawn_worker(
|
||||
move || {
|
||||
queue_future(async move {
|
||||
while let Some(job) = job_rx.recv().await {
|
||||
event_tx
|
||||
.send(WorkerEvent::Log(format!(
|
||||
"[worker] accepted job `{job}` from main thread"
|
||||
)))
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("worker should be able to report accepted jobs")
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
if job == "upload-frame" {
|
||||
let (ack_tx, mut ack_rx) = oneshot::channel();
|
||||
event_tx
|
||||
.send(WorkerEvent::PresentRequest {
|
||||
frame: job,
|
||||
ack: ack_tx,
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("worker should be able to request presentation")
|
||||
});
|
||||
let ack = ack_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("main thread should acknowledge frame");
|
||||
event_tx
|
||||
.send(WorkerEvent::Log(format!(
|
||||
"[worker] got oneshot ack `{ack}` for `{job}`"
|
||||
)))
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("worker should be able to report ack reception")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
event_tx
|
||||
.send(WorkerEvent::Log(
|
||||
"[worker] bounded command channel closed; worker is done".into(),
|
||||
))
|
||||
.unwrap_or_else(|_| panic!("worker should be able to report shutdown"));
|
||||
});
|
||||
},
|
||||
|| log_event!(12, "[main] worker exited"),
|
||||
);
|
||||
|
||||
queue_future(async move {
|
||||
log_event!(1, "[main] bounded mpsc send: enqueue `prepare-scene`");
|
||||
job_tx
|
||||
.send("prepare-scene")
|
||||
.await
|
||||
.expect("prepare-scene should be sent");
|
||||
|
||||
log_event!(
|
||||
2,
|
||||
"[main] bounded mpsc send: enqueue `upload-frame` (fits once worker drains capacity)"
|
||||
);
|
||||
job_tx
|
||||
.send("upload-frame")
|
||||
.await
|
||||
.expect("upload-frame should be sent");
|
||||
|
||||
log_event!(
|
||||
3,
|
||||
"[main] bounded mpsc send: enqueue `flush-stats` (waits for capacity/backpressure)"
|
||||
);
|
||||
job_tx
|
||||
.send("flush-stats")
|
||||
.await
|
||||
.expect("flush-stats should be sent");
|
||||
|
||||
log_event!(
|
||||
5,
|
||||
"[main] drop bounded sender to close worker command stream"
|
||||
);
|
||||
drop(job_tx);
|
||||
});
|
||||
|
||||
let mut event_count = 0usize;
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
event_count += 1;
|
||||
match event {
|
||||
WorkerEvent::Log(message) => {
|
||||
let expected = match event_count {
|
||||
1 => 4,
|
||||
2 => 6,
|
||||
4 => 9,
|
||||
5 => 10,
|
||||
6 => 11,
|
||||
_ => 10 + event_count,
|
||||
};
|
||||
log_event_impl(expected, format_args!("{message}"));
|
||||
}
|
||||
WorkerEvent::PresentRequest { frame, ack } => {
|
||||
log_event!(
|
||||
7,
|
||||
"[main] unbounded mpsc recv: worker requests presentation for `{frame}`"
|
||||
);
|
||||
ack.send("presented")
|
||||
.expect("main thread should be able to answer oneshot");
|
||||
log_event!(8, "[main] oneshot send: acknowledged frame presentation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = worker;
|
||||
Ok(())
|
||||
}
|
||||
75
lib/runtime/examples/hyper_http_client.rs
Normal file
75
lib/runtime/examples/hyper_http_client.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::io::{Read as _, Write as _};
|
||||
use std::net::TcpListener as StdTcpListener;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Empty};
|
||||
use hyper::Request;
|
||||
use ruin_runtime::time::sleep;
|
||||
use ruin_runtime::{clear_interval, queue_future, set_interval};
|
||||
|
||||
fn spawn_demo_server() -> std::io::Result<(std::net::SocketAddr, thread::JoinHandle<()>)> {
|
||||
let listener = StdTcpListener::bind(("127.0.0.1", 0))?;
|
||||
let address = listener.local_addr()?;
|
||||
let handle = thread::Builder::new()
|
||||
.name("hyper-demo-server".into())
|
||||
.spawn(move || {
|
||||
let (mut stream, peer) = listener.accept().expect("demo server should accept");
|
||||
let mut request = [0; 1024];
|
||||
let read = stream.read(&mut request).expect("demo server should read");
|
||||
println!("[server] accepted {peer}, saw {} request bytes", read);
|
||||
|
||||
let response = concat!(
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"content-type: text/plain; charset=utf-8\r\n",
|
||||
"content-length: 24\r\n",
|
||||
"connection: close\r\n",
|
||||
"\r\n",
|
||||
"hello from ruin runtime!"
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("demo server should reply");
|
||||
})
|
||||
.map_err(std::io::Error::other)?;
|
||||
Ok((address, handle))
|
||||
}
|
||||
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (address, server) = spawn_demo_server()?;
|
||||
|
||||
let stream = ruin_runtime::net::TcpStream::connect(address).await?;
|
||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(stream).await?;
|
||||
queue_future(async move {
|
||||
if let Err(error) = connection.await {
|
||||
eprintln!("[runtime] hyper connection ended with error: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
println!("Sleeping a moment to let the server start...");
|
||||
let interval = set_interval(Duration::from_millis(400), || println!("..."));
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
clear_interval(&interval);
|
||||
println!("Let's go!");
|
||||
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("http://{address}/demo"))
|
||||
.header("host", address.to_string())
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
let response = sender.send_request(request).await?;
|
||||
let status = response.status();
|
||||
let body = response.into_body().collect().await?.to_bytes();
|
||||
|
||||
println!(
|
||||
"[client] status={status}, body={}",
|
||||
String::from_utf8_lossy(&body)
|
||||
);
|
||||
|
||||
server
|
||||
.join()
|
||||
.expect("demo server thread should exit cleanly");
|
||||
Ok(())
|
||||
}
|
||||
228
lib/runtime/examples/runtime_loop_showcase.rs
Normal file
228
lib/runtime/examples/runtime_loop_showcase.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use ruin_runtime::{
|
||||
IntervalHandle, ThreadHandle, clear_interval, current_thread_handle, queue_future,
|
||||
queue_microtask, queue_task, set_interval, set_timeout, spawn_worker, yield_now,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
static START: OnceLock<Instant> = OnceLock::new();
|
||||
static ACTUAL_ORDER: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
macro_rules! log_event {
|
||||
($expected:literal, $($arg:tt)*) => {{
|
||||
log_event_impl($expected, format_args!($($arg)*));
|
||||
}};
|
||||
}
|
||||
|
||||
fn log_event_impl(expected: usize, message: fmt::Arguments<'_>) {
|
||||
let actual = ACTUAL_ORDER.fetch_add(1, Ordering::SeqCst);
|
||||
let elapsed = START
|
||||
.get()
|
||||
.expect("showcase start time should be initialized")
|
||||
.elapsed()
|
||||
.as_millis();
|
||||
println!(
|
||||
"[actual {actual:02} | expected {expected:02} | +{elapsed:04}ms | ts {}] {message}",
|
||||
unix_timestamp_millis(),
|
||||
);
|
||||
}
|
||||
|
||||
fn unix_timestamp_millis() -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock should be after the Unix epoch");
|
||||
format!("{}.{:03}", now.as_secs(), now.subsec_millis())
|
||||
}
|
||||
|
||||
fn queue_log(handle: &ThreadHandle, expected: usize, message: impl Into<String>) {
|
||||
let message = message.into();
|
||||
let queued = handle.queue_task(move || {
|
||||
log_event_impl(expected, format_args!("{message}"));
|
||||
});
|
||||
assert!(queued, "main thread should accept log task {expected}");
|
||||
}
|
||||
|
||||
fn queue_log_microtask(handle: &ThreadHandle, expected: usize, message: impl Into<String>) {
|
||||
let message = message.into();
|
||||
let queued = handle.queue_microtask(move || {
|
||||
log_event_impl(expected, format_args!("{message}"));
|
||||
});
|
||||
assert!(queued, "main thread should accept log microtask {expected}");
|
||||
}
|
||||
|
||||
#[ruin_runtime::main]
|
||||
fn main() {
|
||||
START.get_or_init(Instant::now);
|
||||
|
||||
queue_microtask(|| log_event!(1, "[main] boot microtask: prime UI state"));
|
||||
|
||||
queue_future(async {
|
||||
log_event!(2, "[main] future: fetch scene metadata");
|
||||
yield_now().await;
|
||||
log_event!(4, "[main] future: scene metadata cached");
|
||||
});
|
||||
|
||||
queue_microtask(|| {
|
||||
log_event!(3, "[main] microtask queued immediately");
|
||||
});
|
||||
|
||||
let main_handle = current_thread_handle();
|
||||
queue_task(move || {
|
||||
log_event!(
|
||||
5,
|
||||
"[main] boot task: paint first frame and start background worker"
|
||||
);
|
||||
|
||||
let dashboard_interval = Rc::new(RefCell::new(None::<IntervalHandle>));
|
||||
let dashboard_ticks = Rc::new(Cell::new(0usize));
|
||||
{
|
||||
let slot = Rc::clone(&dashboard_interval);
|
||||
let ticks = Rc::clone(&dashboard_ticks);
|
||||
set_dashboard_interval(slot, ticks);
|
||||
}
|
||||
|
||||
set_timeout(Duration::from_millis(30), || {
|
||||
log_event!(11, "[main] timeout: network snapshot ready");
|
||||
});
|
||||
|
||||
let main_for_worker = main_handle.clone();
|
||||
let worker = spawn_worker(
|
||||
move || {
|
||||
queue_log(
|
||||
&main_for_worker,
|
||||
6,
|
||||
"[worker->main] startup task: prepare upload queue",
|
||||
);
|
||||
|
||||
{
|
||||
let main_for_microtask = main_for_worker.clone();
|
||||
queue_microtask(move || {
|
||||
queue_log(
|
||||
&main_for_microtask,
|
||||
7,
|
||||
"[worker->main] microtask: inspect staging buffers",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let main_for_future = main_for_worker.clone();
|
||||
queue_future(async move {
|
||||
queue_log(
|
||||
&main_for_future,
|
||||
8,
|
||||
"[worker->main] future: compile shader variants",
|
||||
);
|
||||
yield_now().await;
|
||||
queue_log(
|
||||
&main_for_future,
|
||||
9,
|
||||
"[worker->main] future: shader cache is warm",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let main_for_task = main_for_worker.clone();
|
||||
queue_task(move || {
|
||||
queue_log(
|
||||
&main_for_task,
|
||||
10,
|
||||
"[worker->main] task: upload static geometry",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let sample_interval = Rc::new(RefCell::new(None::<IntervalHandle>));
|
||||
let sample_count = Rc::new(Cell::new(0usize));
|
||||
{
|
||||
let slot = Rc::clone(&sample_interval);
|
||||
let count = Rc::clone(&sample_count);
|
||||
let main_for_samples = main_for_worker.clone();
|
||||
let handle = set_interval(Duration::from_millis(40), move || {
|
||||
let next = count.get() + 1;
|
||||
count.set(next);
|
||||
queue_log(
|
||||
&main_for_samples,
|
||||
if next == 1 { 12 } else { 17 },
|
||||
format!("[worker->main] interval: sample batch {next} ready"),
|
||||
);
|
||||
if next == 2 {
|
||||
let interval = slot.borrow_mut().take().expect("interval should exist");
|
||||
clear_interval(&interval);
|
||||
queue_log(&main_for_samples, 18, "[worker->main] interval stopped");
|
||||
}
|
||||
});
|
||||
*sample_interval.borrow_mut() = Some(handle);
|
||||
}
|
||||
|
||||
{
|
||||
let main_for_flush = main_for_worker.clone();
|
||||
set_timeout(Duration::from_millis(110), move || {
|
||||
queue_log_microtask(
|
||||
&main_for_flush,
|
||||
20,
|
||||
"[worker->main] timeout: flushed final upload batch",
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|| log_event!(21, "[main] worker exited"),
|
||||
);
|
||||
|
||||
set_timeout(Duration::from_millis(70), move || {
|
||||
let queued = worker.queue_task({
|
||||
let main_from_remote_task = main_handle.clone();
|
||||
move || {
|
||||
queue_log(
|
||||
&main_from_remote_task,
|
||||
15,
|
||||
"[worker->main] remote task: upload late texture atlas",
|
||||
);
|
||||
|
||||
let main_from_remote_microtask = main_from_remote_task.clone();
|
||||
queue_microtask(move || {
|
||||
queue_log(
|
||||
&main_from_remote_microtask,
|
||||
16,
|
||||
"[worker->main] remote microtask: retire staging pages",
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
log_event!(
|
||||
14,
|
||||
"[main] timeout: queue late texture upload on worker (queued={queued})"
|
||||
);
|
||||
});
|
||||
|
||||
set_timeout(Duration::from_millis(140), || {
|
||||
log_event!(22, "[main] final timeout: commit frame statistics");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn set_dashboard_interval(slot: Rc<RefCell<Option<IntervalHandle>>>, ticks: Rc<Cell<usize>>) {
|
||||
let slot_for_callback = Rc::clone(&slot);
|
||||
let handle = set_interval(Duration::from_millis(50), move || {
|
||||
let next = ticks.get() + 1;
|
||||
ticks.set(next);
|
||||
if next == 1 {
|
||||
log_event!(13, "[main] interval: dashboard tick 1");
|
||||
return;
|
||||
}
|
||||
|
||||
let interval = slot_for_callback
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.expect("interval should exist");
|
||||
clear_interval(&interval);
|
||||
log_event!(19, "[main] interval: dashboard tick 2 and stop");
|
||||
});
|
||||
*slot.borrow_mut() = Some(handle);
|
||||
}
|
||||
Reference in New Issue
Block a user