From 259cf6192b920dff4cce0486488d929bf4e05b96 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 24 Nov 2023 19:05:31 +0100 Subject: [PATCH 1/4] executor: Remove non-functional rtos-trace-interrupt. --- embassy-executor/Cargo.toml | 3 --- embassy-executor/src/lib.rs | 23 +---------------------- embassy-macros/Cargo.toml | 4 +--- examples/nrf-rtos-trace/Cargo.toml | 2 +- 4 files changed, 3 insertions(+), 29 deletions(-) diff --git a/embassy-executor/Cargo.toml b/embassy-executor/Cargo.toml index 3a623e99..54ea1a54 100644 --- a/embassy-executor/Cargo.toml +++ b/embassy-executor/Cargo.toml @@ -49,9 +49,6 @@ turbowakers = [] integrated-timers = ["dep:embassy-time"] -# Trace interrupt invocations with rtos-trace. -rtos-trace-interrupt = ["rtos-trace", "embassy-macros/rtos-trace-interrupt"] - [dependencies] defmt = { version = "0.3", optional = true } log = { version = "0.4.14", optional = true } diff --git a/embassy-executor/src/lib.rs b/embassy-executor/src/lib.rs index f2c86d8e..3b61b4ba 100644 --- a/embassy-executor/src/lib.rs +++ b/embassy-executor/src/lib.rs @@ -44,25 +44,4 @@ pub use spawner::*; /// Implementation details for embassy macros. /// Do not use. Used for macros and HALs only. Not covered by semver guarantees. #[doc(hidden)] -pub mod _export { - #[cfg(feature = "rtos-trace")] - pub use rtos_trace::trace; - - /// Expands the given block of code when `embassy-executor` is compiled with - /// the `rtos-trace-interrupt` feature. - #[doc(hidden)] - #[macro_export] - #[cfg(feature = "rtos-trace-interrupt")] - macro_rules! rtos_trace_interrupt { - ($($tt:tt)*) => { $($tt)* }; - } - - /// Does not expand the given block of code when `embassy-executor` is - /// compiled without the `rtos-trace-interrupt` feature. - #[doc(hidden)] - #[macro_export] - #[cfg(not(feature = "rtos-trace-interrupt"))] - macro_rules! rtos_trace_interrupt { - ($($tt:tt)*) => {}; - } -} +pub mod _export {} diff --git a/embassy-macros/Cargo.toml b/embassy-macros/Cargo.toml index a893cd30..4e94bf83 100644 --- a/embassy-macros/Cargo.toml +++ b/embassy-macros/Cargo.toml @@ -20,6 +20,4 @@ proc-macro2 = "1.0.29" [lib] proc-macro = true -[features] -# Enabling this cause interrupt::take! to require embassy-executor -rtos-trace-interrupt = [] +[features] \ No newline at end of file diff --git a/examples/nrf-rtos-trace/Cargo.toml b/examples/nrf-rtos-trace/Cargo.toml index e30b13f2..e3524deb 100644 --- a/examples/nrf-rtos-trace/Cargo.toml +++ b/examples/nrf-rtos-trace/Cargo.toml @@ -17,7 +17,7 @@ log = [ [dependencies] embassy-sync = { version = "0.4.0", path = "../../embassy-sync" } -embassy-executor = { version = "0.3.3", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "rtos-trace", "rtos-trace-interrupt", "integrated-timers"] } +embassy-executor = { version = "0.3.3", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "rtos-trace", "integrated-timers"] } embassy-time = { version = "0.1.5", path = "../../embassy-time" } embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["nrf52840", "time-driver-rtc1", "gpiote", "unstable-pac"] } From 1fbc150fd6392d8268aa35d15380c02e363c4eb8 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 24 Nov 2023 21:33:30 +0100 Subject: [PATCH 2/4] executor: add some tests. --- .github/ci/test.sh | 2 + embassy-executor/Cargo.toml | 3 + embassy-executor/tests/test.rs | 137 +++++++++++++++++++++++++++++++++ rust-toolchain.toml | 2 +- 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 embassy-executor/tests/test.rs diff --git a/.github/ci/test.sh b/.github/ci/test.sh index af0f21c2..e48d6e65 100755 --- a/.github/ci/test.sh +++ b/.github/ci/test.sh @@ -15,6 +15,8 @@ export CARGO_NET_GIT_FETCH_WITH_CLI=true hashtime restore /ci/cache/filetime.json || true hashtime save /ci/cache/filetime.json +MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --manifest-path ./embassy-executor/Cargo.toml --features nightly + cargo test --manifest-path ./embassy-sync/Cargo.toml cargo test --manifest-path ./embassy-embedded-hal/Cargo.toml cargo test --manifest-path ./embassy-hal-internal/Cargo.toml diff --git a/embassy-executor/Cargo.toml b/embassy-executor/Cargo.toml index 54ea1a54..f0e85a2c 100644 --- a/embassy-executor/Cargo.toml +++ b/embassy-executor/Cargo.toml @@ -68,3 +68,6 @@ cortex-m = { version = "0.7.6", optional = true } # arch-wasm dependencies wasm-bindgen = { version = "0.2.82", optional = true } js-sys = { version = "0.3", optional = true } + +[dev-dependencies] +critical-section = { version = "1.1", features = ["std"] } diff --git a/embassy-executor/tests/test.rs b/embassy-executor/tests/test.rs new file mode 100644 index 00000000..0dbd391e --- /dev/null +++ b/embassy-executor/tests/test.rs @@ -0,0 +1,137 @@ +#![cfg_attr(feature = "nightly", feature(type_alias_impl_trait))] + +use std::boxed::Box; +use std::future::poll_fn; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use embassy_executor::raw::Executor; +use embassy_executor::task; + +#[export_name = "__pender"] +fn __pender(context: *mut ()) { + unsafe { + let trace = &*(context as *const Trace); + trace.push("pend"); + } +} + +#[derive(Clone)] +struct Trace { + trace: Arc>>, +} + +impl Trace { + fn new() -> Self { + Self { + trace: Arc::new(Mutex::new(Vec::new())), + } + } + fn push(&self, value: &'static str) { + self.trace.lock().unwrap().push(value) + } + + fn get(&self) -> Vec<&'static str> { + self.trace.lock().unwrap().clone() + } +} + +fn setup() -> (&'static Executor, Trace) { + let trace = Trace::new(); + let context = Box::leak(Box::new(trace.clone())) as *mut _ as *mut (); + let executor = &*Box::leak(Box::new(Executor::new(context))); + (executor, trace) +} + +#[test] +fn executor_noop() { + let (executor, trace) = setup(); + unsafe { executor.poll() }; + assert!(trace.get().is_empty()) +} + +#[test] +fn executor_task() { + #[task] + async fn task1(trace: Trace) { + trace.push("poll task1") + } + + let (executor, trace) = setup(); + executor.spawner().spawn(task1(trace.clone())).unwrap(); + + unsafe { executor.poll() }; + unsafe { executor.poll() }; + + assert_eq!( + trace.get(), + &[ + "pend", // spawning a task pends the executor + "poll task1", // poll only once. + ] + ) +} + +#[test] +fn executor_task_self_wake() { + #[task] + async fn task1(trace: Trace) { + poll_fn(|cx| { + trace.push("poll task1"); + cx.waker().wake_by_ref(); + Poll::Pending + }) + .await + } + + let (executor, trace) = setup(); + executor.spawner().spawn(task1(trace.clone())).unwrap(); + + unsafe { executor.poll() }; + unsafe { executor.poll() }; + + assert_eq!( + trace.get(), + &[ + "pend", // spawning a task pends the executor + "poll task1", // + "pend", // task self-wakes + "poll task1", // + "pend", // task self-wakes + ] + ) +} + +#[test] +fn executor_task_self_wake_twice() { + #[task] + async fn task1(trace: Trace) { + poll_fn(|cx| { + trace.push("poll task1"); + cx.waker().wake_by_ref(); + trace.push("poll task1 wake 2"); + cx.waker().wake_by_ref(); + Poll::Pending + }) + .await + } + + let (executor, trace) = setup(); + executor.spawner().spawn(task1(trace.clone())).unwrap(); + + unsafe { executor.poll() }; + unsafe { executor.poll() }; + + assert_eq!( + trace.get(), + &[ + "pend", // spawning a task pends the executor + "poll task1", // + "pend", // task self-wakes + "poll task1 wake 2", // task self-wakes again, shouldn't pend + "poll task1", // + "pend", // task self-wakes + "poll task1 wake 2", // task self-wakes again, shouldn't pend + ] + ) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 419c3108..11f53ee4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,7 +2,7 @@ # https://rust-lang.github.io/rustup-components-history [toolchain] channel = "nightly-2023-11-01" -components = [ "rust-src", "rustfmt", "llvm-tools" ] +components = [ "rust-src", "rustfmt", "llvm-tools", "miri" ] targets = [ "thumbv7em-none-eabi", "thumbv7m-none-eabi", From 171cdb94c7906670723b0965ca66d72a2352ac73 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 24 Nov 2023 21:37:27 +0100 Subject: [PATCH 3/4] executor: add support for main/task macros in stable (allocates tasks in an arena) --- .github/ci/test.sh | 1 + embassy-executor/Cargo.toml | 2 +- embassy-executor/src/arch/cortex_m.rs | 1 - embassy-executor/src/arch/riscv32.rs | 1 - embassy-executor/src/arch/std.rs | 1 - embassy-executor/src/arch/wasm.rs | 1 - embassy-executor/src/arch/xtensa.rs | 1 - embassy-executor/src/lib.rs | 95 ++++++++++++++++++++++++++- embassy-macros/Cargo.toml | 3 +- embassy-macros/src/macros/task.rs | 9 +++ 10 files changed, 105 insertions(+), 10 deletions(-) diff --git a/.github/ci/test.sh b/.github/ci/test.sh index e48d6e65..dfc2b08c 100755 --- a/.github/ci/test.sh +++ b/.github/ci/test.sh @@ -15,6 +15,7 @@ export CARGO_NET_GIT_FETCH_WITH_CLI=true hashtime restore /ci/cache/filetime.json || true hashtime save /ci/cache/filetime.json +MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --manifest-path ./embassy-executor/Cargo.toml MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --manifest-path ./embassy-executor/Cargo.toml --features nightly cargo test --manifest-path ./embassy-sync/Cargo.toml diff --git a/embassy-executor/Cargo.toml b/embassy-executor/Cargo.toml index f0e85a2c..3ff8440c 100644 --- a/embassy-executor/Cargo.toml +++ b/embassy-executor/Cargo.toml @@ -43,7 +43,7 @@ executor-thread = [] executor-interrupt = [] # Enable nightly-only features -nightly = [] +nightly = ["embassy-macros/nightly"] turbowakers = [] diff --git a/embassy-executor/src/arch/cortex_m.rs b/embassy-executor/src/arch/cortex_m.rs index 55299c94..4a6d5857 100644 --- a/embassy-executor/src/arch/cortex_m.rs +++ b/embassy-executor/src/arch/cortex_m.rs @@ -51,7 +51,6 @@ mod thread { use core::arch::asm; use core::marker::PhantomData; - #[cfg(feature = "nightly")] pub use embassy_macros::main_cortex_m as main; use crate::{raw, Spawner}; diff --git a/embassy-executor/src/arch/riscv32.rs b/embassy-executor/src/arch/riscv32.rs index 6814e784..ca12c340 100644 --- a/embassy-executor/src/arch/riscv32.rs +++ b/embassy-executor/src/arch/riscv32.rs @@ -7,7 +7,6 @@ pub use thread::*; mod thread { use core::marker::PhantomData; - #[cfg(feature = "nightly")] pub use embassy_macros::main_riscv as main; use portable_atomic::{AtomicBool, Ordering}; diff --git a/embassy-executor/src/arch/std.rs b/embassy-executor/src/arch/std.rs index 5b2f7e2e..598bb050 100644 --- a/embassy-executor/src/arch/std.rs +++ b/embassy-executor/src/arch/std.rs @@ -8,7 +8,6 @@ mod thread { use std::marker::PhantomData; use std::sync::{Condvar, Mutex}; - #[cfg(feature = "nightly")] pub use embassy_macros::main_std as main; use crate::{raw, Spawner}; diff --git a/embassy-executor/src/arch/wasm.rs b/embassy-executor/src/arch/wasm.rs index 15aed867..3faa9257 100644 --- a/embassy-executor/src/arch/wasm.rs +++ b/embassy-executor/src/arch/wasm.rs @@ -8,7 +8,6 @@ mod thread { use core::marker::PhantomData; - #[cfg(feature = "nightly")] pub use embassy_macros::main_wasm as main; use js_sys::Promise; use wasm_bindgen::prelude::*; diff --git a/embassy-executor/src/arch/xtensa.rs b/embassy-executor/src/arch/xtensa.rs index d335594e..6ed9f9e7 100644 --- a/embassy-executor/src/arch/xtensa.rs +++ b/embassy-executor/src/arch/xtensa.rs @@ -8,7 +8,6 @@ mod thread { use core::marker::PhantomData; use core::sync::atomic::{AtomicBool, Ordering}; - #[cfg(feature = "nightly")] pub use embassy_macros::main_riscv as main; use crate::{raw, Spawner}; diff --git a/embassy-executor/src/lib.rs b/embassy-executor/src/lib.rs index 3b61b4ba..ac7dbb03 100644 --- a/embassy-executor/src/lib.rs +++ b/embassy-executor/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(any(feature = "arch-std", feature = "arch-wasm")), no_std)] -#![cfg_attr(all(feature = "nightly", feature = "arch-xtensa"), feature(asm_experimental_arch))] +#![cfg_attr(feature = "arch-xtensa", feature(asm_experimental_arch))] #![allow(clippy::new_without_default)] #![doc = include_str!("../README.md")] #![warn(missing_docs)] @@ -7,7 +7,6 @@ // This mod MUST go first, so that the others see its macros. pub(crate) mod fmt; -#[cfg(feature = "nightly")] pub use embassy_macros::task; macro_rules! check_at_most_one { @@ -44,4 +43,94 @@ pub use spawner::*; /// Implementation details for embassy macros. /// Do not use. Used for macros and HALs only. Not covered by semver guarantees. #[doc(hidden)] -pub mod _export {} +#[cfg(not(feature = "nightly"))] +pub mod _export { + use core::alloc::Layout; + use core::cell::{Cell, UnsafeCell}; + use core::future::Future; + use core::mem::MaybeUninit; + use core::ptr::null_mut; + + use critical_section::{CriticalSection, Mutex}; + + use crate::raw::TaskPool; + + struct Arena { + buf: UnsafeCell>, + ptr: Mutex>, + } + + unsafe impl Sync for Arena {} + unsafe impl Send for Arena {} + + impl Arena { + const fn new() -> Self { + Self { + buf: UnsafeCell::new(MaybeUninit::uninit()), + ptr: Mutex::new(Cell::new(null_mut())), + } + } + + fn alloc(&'static self, cs: CriticalSection) -> &'static mut MaybeUninit { + let layout = Layout::new::(); + + let start = self.buf.get().cast::(); + let end = unsafe { start.add(N) }; + + let mut ptr = self.ptr.borrow(cs).get(); + if ptr.is_null() { + ptr = self.buf.get().cast::(); + } + + let bytes_left = (end as usize) - (ptr as usize); + let align_offset = (ptr as usize).next_multiple_of(layout.align()) - (ptr as usize); + + if align_offset + layout.size() > bytes_left { + panic!("arena full"); + } + + let res = unsafe { ptr.add(align_offset) }; + let ptr = unsafe { ptr.add(align_offset + layout.size()) }; + + self.ptr.borrow(cs).set(ptr); + + unsafe { &mut *(res as *mut MaybeUninit) } + } + } + + const ARENA_SIZE: usize = 16 * 1024; + static ARENA: Arena = Arena::new(); + + pub struct TaskPoolRef { + // type-erased `&'static mut TaskPool` + // Needed because statics can't have generics. + ptr: Mutex>, + } + unsafe impl Sync for TaskPoolRef {} + unsafe impl Send for TaskPoolRef {} + + impl TaskPoolRef { + pub const fn new() -> Self { + Self { + ptr: Mutex::new(Cell::new(null_mut())), + } + } + + /// Get the pool for this ref, allocating it from the arena the first time. + /// + /// safety: for a given TaskPoolRef instance, must always call with the exact + /// same generic params. + pub unsafe fn get(&'static self) -> &'static TaskPool { + critical_section::with(|cs| { + let ptr = self.ptr.borrow(cs); + if ptr.get().is_null() { + let pool = ARENA.alloc::>(cs); + pool.write(TaskPool::new()); + ptr.set(pool as *mut _ as _); + } + + unsafe { &*(ptr.get() as *const _) } + }) + } + } +} diff --git a/embassy-macros/Cargo.toml b/embassy-macros/Cargo.toml index 4e94bf83..e13104cf 100644 --- a/embassy-macros/Cargo.toml +++ b/embassy-macros/Cargo.toml @@ -20,4 +20,5 @@ proc-macro2 = "1.0.29" [lib] proc-macro = true -[features] \ No newline at end of file +[features] +nightly = [] \ No newline at end of file diff --git a/embassy-macros/src/macros/task.rs b/embassy-macros/src/macros/task.rs index 1d30434e..5161e102 100644 --- a/embassy-macros/src/macros/task.rs +++ b/embassy-macros/src/macros/task.rs @@ -79,6 +79,7 @@ pub fn run(args: &[NestedMeta], f: syn::ItemFn) -> Result ::embassy_executor::SpawnToken { type Fut = impl ::core::future::Future + 'static; @@ -87,6 +88,14 @@ pub fn run(args: &[NestedMeta], f: syn::ItemFn) -> Result ::embassy_executor::SpawnToken { + const POOL_SIZE: usize = #pool_size; + static POOL: ::embassy_executor::_export::TaskPoolRef = ::embassy_executor::_export::TaskPoolRef::new(); + unsafe { POOL.get::<_, POOL_SIZE>()._spawn_async_fn(move || #task_inner_ident(#(#arg_names,)*)) } + } + }; task_outer.attrs.append(&mut task_inner.attrs.clone()); From 996c3c1f7e389b1e7d26ca6f02524fff3d63212e Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 24 Nov 2023 22:39:08 +0100 Subject: [PATCH 4/4] executor: make task arena size configurable. --- embassy-executor/Cargo.toml | 90 ++++++++++++++++++++++++-------- embassy-executor/README.md | 28 +++++++++- embassy-executor/build.rs | 93 +++++++++++++++++++++++++++++++++- embassy-executor/gen_config.py | 82 ++++++++++++++++++++++++++++++ embassy-executor/src/lib.rs | 10 ++-- 5 files changed, 276 insertions(+), 27 deletions(-) create mode 100644 embassy-executor/gen_config.py diff --git a/embassy-executor/Cargo.toml b/embassy-executor/Cargo.toml index 3ff8440c..ae46b17c 100644 --- a/embassy-executor/Cargo.toml +++ b/embassy-executor/Cargo.toml @@ -27,28 +27,6 @@ default-target = "thumbv7em-none-eabi" targets = ["thumbv7em-none-eabi"] features = ["nightly", "defmt", "arch-cortex-m", "executor-thread", "executor-interrupt"] -[features] - -# Architecture -_arch = [] # some arch was picked -arch-std = ["_arch", "critical-section/std"] -arch-cortex-m = ["_arch", "dep:cortex-m"] -arch-xtensa = ["_arch"] -arch-riscv32 = ["_arch", "dep:portable-atomic"] -arch-wasm = ["_arch", "dep:wasm-bindgen", "dep:js-sys"] - -# Enable the thread-mode executor (using WFE/SEV in Cortex-M, WFI in other embedded archs) -executor-thread = [] -# Enable the interrupt-mode executor (available in Cortex-M only) -executor-interrupt = [] - -# Enable nightly-only features -nightly = ["embassy-macros/nightly"] - -turbowakers = [] - -integrated-timers = ["dep:embassy-time"] - [dependencies] defmt = { version = "0.3", optional = true } log = { version = "0.4.14", optional = true } @@ -71,3 +49,71 @@ js-sys = { version = "0.3", optional = true } [dev-dependencies] critical-section = { version = "1.1", features = ["std"] } + + +[features] + +# Architecture +_arch = [] # some arch was picked +arch-std = ["_arch", "critical-section/std"] +arch-cortex-m = ["_arch", "dep:cortex-m"] +arch-xtensa = ["_arch"] +arch-riscv32 = ["_arch", "dep:portable-atomic"] +arch-wasm = ["_arch", "dep:wasm-bindgen", "dep:js-sys"] + +# Enable the thread-mode executor (using WFE/SEV in Cortex-M, WFI in other embedded archs) +executor-thread = [] +# Enable the interrupt-mode executor (available in Cortex-M only) +executor-interrupt = [] + +# Enable nightly-only features +nightly = ["embassy-macros/nightly"] + +turbowakers = [] + +integrated-timers = ["dep:embassy-time"] + +# BEGIN AUTOGENERATED CONFIG FEATURES +# Generated by gen_config.py. DO NOT EDIT. +task-arena-size-64 = [] +task-arena-size-128 = [] +task-arena-size-192 = [] +task-arena-size-256 = [] +task-arena-size-320 = [] +task-arena-size-384 = [] +task-arena-size-512 = [] +task-arena-size-640 = [] +task-arena-size-768 = [] +task-arena-size-1024 = [] +task-arena-size-1280 = [] +task-arena-size-1536 = [] +task-arena-size-2048 = [] +task-arena-size-2560 = [] +task-arena-size-3072 = [] +task-arena-size-4096 = [] # Default +task-arena-size-5120 = [] +task-arena-size-6144 = [] +task-arena-size-8192 = [] +task-arena-size-10240 = [] +task-arena-size-12288 = [] +task-arena-size-16384 = [] +task-arena-size-20480 = [] +task-arena-size-24576 = [] +task-arena-size-32768 = [] +task-arena-size-40960 = [] +task-arena-size-49152 = [] +task-arena-size-65536 = [] +task-arena-size-81920 = [] +task-arena-size-98304 = [] +task-arena-size-131072 = [] +task-arena-size-163840 = [] +task-arena-size-196608 = [] +task-arena-size-262144 = [] +task-arena-size-327680 = [] +task-arena-size-393216 = [] +task-arena-size-524288 = [] +task-arena-size-655360 = [] +task-arena-size-786432 = [] +task-arena-size-1048576 = [] + +# END AUTOGENERATED CONFIG FEATURES diff --git a/embassy-executor/README.md b/embassy-executor/README.md index 3c1448a1..80ecfc71 100644 --- a/embassy-executor/README.md +++ b/embassy-executor/README.md @@ -2,10 +2,36 @@ An async/await executor designed for embedded usage. -- No `alloc`, no heap needed. Task futures are statically allocated. +- No `alloc`, no heap needed. +- With nightly Rust, task futures can be fully statically allocated. - No "fixed capacity" data structures, executor works with 1 or 1000 tasks without needing config/tuning. - Integrated timer queue: sleeping is easy, just do `Timer::after_secs(1).await;`. - No busy-loop polling: CPU sleeps when there's no work to do, using interrupts or `WFE/SEV`. - Efficient polling: a wake will only poll the woken task, not all of them. - Fair: a task can't monopolize CPU time even if it's constantly being woken. All other tasks get a chance to run before a given task gets polled for the second time. - Creating multiple executor instances is supported, to run tasks with multiple priority levels. This allows higher-priority tasks to preempt lower-priority tasks. + +## Task arena + +When the `nightly` Cargo feature is not enabled, `embassy-executor` allocates tasks out of an arena (a very simple bump allocator). + +If the task arena gets full, the program will panic at runtime. To guarantee this doesn't happen, you must set the size to the sum of sizes of all tasks. + +Tasks are allocated from the arena when spawned for the first time. If the task exists, the allocation is not released to the arena, but can be reused to spawn the task again. For multiple-instance tasks (like `#[embassy_executor::task(pool_size = 4)]`), the first spawn will allocate memory for all instances. This is done for performance and to increase predictability (for example, spawning at least 1 instance of every task at boot guarantees an immediate panic if the arena is too small, while allocating instances on-demand could delay the panic to only when the program is under load). + +The arena size can be configured in two ways: + +- Via Cargo features: enable a Cargo feature like `task-arena-size-8192`. Only a selection of values + is available, check `Cargo.toml` for the list. +- Via environment variables at build time: set the variable named `EMBASSY_EXECUTOR_TASK_ARENA_SIZE`. For example + `EMBASSY_EXECUTOR_TASK_ARENA_SIZE=4321 cargo build`. You can also set them in the `[env]` section of `.cargo/config.toml`. + Any value can be set, unlike with Cargo features. + +Environment variables take precedence over Cargo features. If two Cargo features are enabled for the same setting +with different values, compilation fails. + +## Statically allocating tasks + +When using nightly Rust, enable the `nightly` Cargo feature. This will make `embassy-executor` use the `type_alias_impl_trait` feature to allocate all tasks in `static`s. Each task gets its own `static`, with the exact size to hold the task (or multiple instances of it, if using `pool_size`) calculated automatically at compile time. If tasks don't fit in RAM, this is detected at compile time by the linker. Runtime panics due to running out of memory are not possible. + +The configured arena size is ignored, no arena is used at all. diff --git a/embassy-executor/build.rs b/embassy-executor/build.rs index 6fe82b44..07f31e3f 100644 --- a/embassy-executor/build.rs +++ b/embassy-executor/build.rs @@ -1,6 +1,97 @@ -use std::env; +use std::collections::HashMap; +use std::fmt::Write; +use std::path::PathBuf; +use std::{env, fs}; + +static CONFIGS: &[(&str, usize)] = &[ + // BEGIN AUTOGENERATED CONFIG FEATURES + // Generated by gen_config.py. DO NOT EDIT. + ("TASK_ARENA_SIZE", 4096), + // END AUTOGENERATED CONFIG FEATURES +]; + +struct ConfigState { + value: usize, + seen_feature: bool, + seen_env: bool, +} fn main() { + let crate_name = env::var("CARGO_PKG_NAME") + .unwrap() + .to_ascii_uppercase() + .replace('-', "_"); + + // only rebuild if build.rs changed. Otherwise Cargo will rebuild if any + // other file changed. + println!("cargo:rerun-if-changed=build.rs"); + + // Rebuild if config envvar changed. + for (name, _) in CONFIGS { + println!("cargo:rerun-if-env-changed={crate_name}_{name}"); + } + + let mut configs = HashMap::new(); + for (name, default) in CONFIGS { + configs.insert( + *name, + ConfigState { + value: *default, + seen_env: false, + seen_feature: false, + }, + ); + } + + let prefix = format!("{crate_name}_"); + for (var, value) in env::vars() { + if let Some(name) = var.strip_prefix(&prefix) { + let Some(cfg) = configs.get_mut(name) else { + panic!("Unknown env var {name}") + }; + + let Ok(value) = value.parse::() else { + panic!("Invalid value for env var {name}: {value}") + }; + + cfg.value = value; + cfg.seen_env = true; + } + + if let Some(feature) = var.strip_prefix("CARGO_FEATURE_") { + if let Some(i) = feature.rfind('_') { + let name = &feature[..i]; + let value = &feature[i + 1..]; + if let Some(cfg) = configs.get_mut(name) { + let Ok(value) = value.parse::() else { + panic!("Invalid value for feature {name}: {value}") + }; + + // envvars take priority. + if !cfg.seen_env { + if cfg.seen_feature { + panic!("multiple values set for feature {}: {} and {}", name, cfg.value, value); + } + + cfg.value = value; + cfg.seen_feature = true; + } + } + } + } + } + + let mut data = String::new(); + + for (name, cfg) in &configs { + writeln!(&mut data, "pub const {}: usize = {};", name, cfg.value).unwrap(); + } + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let out_file = out_dir.join("config.rs").to_string_lossy().to_string(); + fs::write(out_file, data).unwrap(); + + // cortex-m targets let target = env::var("TARGET").unwrap(); if target.starts_with("thumbv6m-") { diff --git a/embassy-executor/gen_config.py b/embassy-executor/gen_config.py new file mode 100644 index 00000000..e427d29f --- /dev/null +++ b/embassy-executor/gen_config.py @@ -0,0 +1,82 @@ +import os + +abspath = os.path.abspath(__file__) +dname = os.path.dirname(abspath) +os.chdir(dname) + +features = [] + + +def feature(name, default, min=None, max=None, pow2=None, vals=None, factors=[]): + if vals is None: + assert min is not None + assert max is not None + + vals = set() + val = min + while val <= max: + vals.add(val) + for f in factors: + if val * f <= max: + vals.add(val * f) + if (pow2 == True or (isinstance(pow2, int) and val >= pow2)) and val > 0: + val *= 2 + else: + val += 1 + vals.add(default) + vals = sorted(list(vals)) + + features.append( + { + "name": name, + "default": default, + "vals": vals, + } + ) + + +feature( + "task_arena_size", default=4096, min=64, max=1024 * 1024, pow2=True, factors=[3, 5] +) + +# ========= Update Cargo.toml + +things = "" +for f in features: + name = f["name"].replace("_", "-") + for val in f["vals"]: + things += f"{name}-{val} = []" + if val == f["default"]: + things += " # Default" + things += "\n" + things += "\n" + +SEPARATOR_START = "# BEGIN AUTOGENERATED CONFIG FEATURES\n" +SEPARATOR_END = "# END AUTOGENERATED CONFIG FEATURES\n" +HELP = "# Generated by gen_config.py. DO NOT EDIT.\n" +with open("Cargo.toml", "r") as f: + data = f.read() +before, data = data.split(SEPARATOR_START, maxsplit=1) +_, after = data.split(SEPARATOR_END, maxsplit=1) +data = before + SEPARATOR_START + HELP + things + SEPARATOR_END + after +with open("Cargo.toml", "w") as f: + f.write(data) + + +# ========= Update build.rs + +things = "" +for f in features: + name = f["name"].upper() + things += f' ("{name}", {f["default"]}),\n' + +SEPARATOR_START = "// BEGIN AUTOGENERATED CONFIG FEATURES\n" +SEPARATOR_END = "// END AUTOGENERATED CONFIG FEATURES\n" +HELP = " // Generated by gen_config.py. DO NOT EDIT.\n" +with open("build.rs", "r") as f: + data = f.read() +before, data = data.split(SEPARATOR_START, maxsplit=1) +_, after = data.split(SEPARATOR_END, maxsplit=1) +data = before + SEPARATOR_START + HELP + things + " " + SEPARATOR_END + after +with open("build.rs", "w") as f: + f.write(data) diff --git a/embassy-executor/src/lib.rs b/embassy-executor/src/lib.rs index ac7dbb03..d8ac4893 100644 --- a/embassy-executor/src/lib.rs +++ b/embassy-executor/src/lib.rs @@ -40,6 +40,11 @@ pub mod raw; mod spawner; pub use spawner::*; +mod config { + #![allow(unused)] + include!(concat!(env!("OUT_DIR"), "/config.rs")); +} + /// Implementation details for embassy macros. /// Do not use. Used for macros and HALs only. Not covered by semver guarantees. #[doc(hidden)] @@ -86,7 +91,7 @@ pub mod _export { let align_offset = (ptr as usize).next_multiple_of(layout.align()) - (ptr as usize); if align_offset + layout.size() > bytes_left { - panic!("arena full"); + panic!("embassy-executor: task arena is full. You must increase the arena size, see the documentation for details: https://docs.embassy.dev/embassy-executor/"); } let res = unsafe { ptr.add(align_offset) }; @@ -98,8 +103,7 @@ pub mod _export { } } - const ARENA_SIZE: usize = 16 * 1024; - static ARENA: Arena = Arena::new(); + static ARENA: Arena<{ crate::config::TASK_ARENA_SIZE }> = Arena::new(); pub struct TaskPoolRef { // type-erased `&'static mut TaskPool`