From 5191a2af4fe8e9c0f14ad10a0ce5138d8e826b56 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:27:03 +0200 Subject: [PATCH 01/25] Adopt Strata memory policy in Phase API --- src/Phase.h | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Phase.h b/src/Phase.h index 7c308b0..c4362de 100644 --- a/src/Phase.h +++ b/src/Phase.h @@ -1,10 +1,10 @@ #pragma once #include +#include + #include #include -#include -#include #include #include @@ -55,12 +55,6 @@ enum class PhaseNodeType : uint8_t { None, }; -enum class PhaseStackType : uint8_t { - Auto, - Internal, - Psram, -}; - struct PhaseResult { bool result = false; PhaseStatus status = PhaseStatus::InternalError; @@ -75,11 +69,15 @@ struct PhaseResult { }; struct PhaseConfig { + Strata::MemoryPolicy memory{ + .allocation = Strata::Placement::Default, + .taskStack = Strata::Placement::PreferExternal, + }; + uint32_t stackSizeBytes = 4096; UBaseType_t priority = 1; BaseType_t coreId = tskNO_AFFINITY; const char *taskName = "phase-task"; - PhaseStackType stackType = PhaseStackType::Auto; size_t maxNodes = 32; size_t maxDependenciesPerNode = 8; uint32_t defaultInitTimeoutMs = 30000; @@ -119,8 +117,9 @@ struct PhaseDiag { uint32_t changeCount = 0; size_t stackHighWaterMarkBytes = 0; PhaseState state = PhaseState::Idle; - PhaseStackType requestedStackType = PhaseStackType::Auto; - PhaseStackType actualStackType = PhaseStackType::Internal; + Strata::Placement requestedStackPlacement = Strata::Placement::Default; + Strata::Region stackRegion = Strata::Region::Unknown; + Strata::Placement allocationPlacement = Strata::Placement::Default; }; using PhaseCallback = std::function; @@ -303,5 +302,5 @@ class Phase { ); PhaseResult setGroupPollInterval(size_t index, uint32_t intervalMs); - std::unique_ptr _impl; + Strata::UniquePtr _impl; }; From 0f4c4d26eab3651951ba630c789f058724b2488a Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:28:20 +0200 Subject: [PATCH 02/25] Route Phase ownership through Strata --- src/internal/PhaseRuntimeBase.inc | 120 +++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/src/internal/PhaseRuntimeBase.inc b/src/internal/PhaseRuntimeBase.inc index e3681c2..d681f3e 100644 --- a/src/internal/PhaseRuntimeBase.inc +++ b/src/internal/PhaseRuntimeBase.inc @@ -1,17 +1,15 @@ #include "Phase.h" -#include "internal/PhaseMutex.h" -#include "internal/PhaseTaskSupport.h" +#include +#include #include #include #include #include -#include #include -#include +#include #include -#include namespace { constexpr uint32_t kWaitPollMs = 10; @@ -21,6 +19,7 @@ constexpr uint8_t kInitTimeout = 0; constexpr uint8_t kStartTimeout = 1; constexpr uint8_t kStopTimeout = 2; constexpr uint8_t kDeinitTimeout = 3; +constexpr size_t kMinStackSizeBytes = 1024; void copyText(char *destination, size_t destinationSize, const char *source) { if (destination == nullptr || destinationSize == 0) return; @@ -28,6 +27,46 @@ void copyText(char *destination, size_t destinationSize, const char *source) { std::snprintf(destination, destinationSize, "%s", source); } +bool isValidStackSize(size_t stackBytes) { + return stackBytes >= kMinStackSizeBytes && (stackBytes % sizeof(StackType_t)) == 0; +} + +template +class PhasePlacedVector { + public: + PhasePlacedVector() noexcept + : _storage(std::in_place, Strata::Allocator{Strata::Placement::Default}) {} + + void resetPlacement(Strata::Placement placement) { + _storage.reset(); + _storage.emplace(Strata::Allocator{placement}); + } + + bool empty() const { return _storage->empty(); } + size_t size() const { return _storage->size(); } + void clear() { _storage->clear(); } + void reserve(size_t capacity) { _storage->reserve(capacity); } + void resize(size_t size) { _storage->resize(size); } + + T &operator[](size_t index) { return (*_storage)[index]; } + const T &operator[](size_t index) const { return (*_storage)[index]; } + + auto begin() { return _storage->begin(); } + auto end() { return _storage->end(); } + auto begin() const { return _storage->begin(); } + auto end() const { return _storage->end(); } + auto rbegin() { return _storage->rbegin(); } + auto rend() { return _storage->rend(); } + + template + void push_back(U &&value) { + _storage->push_back(std::forward(value)); + } + + private: + std::optional> _storage; +}; + enum class DependencyState : uint8_t { Ready, Waiting, @@ -45,13 +84,39 @@ struct PhaseNodeState { bool skipped = false; bool hasStartCallback = false; }; + +class PhaseLock { + public: + explicit PhaseLock(Strata::FreeRTOS::RecursiveMutex &mutex) + : _mutex(mutex), _locked(mutex.lock()) {} + + ~PhaseLock() { + if (_locked) _mutex.unlock(); + } + + PhaseLock(const PhaseLock &) = delete; + PhaseLock &operator=(const PhaseLock &) = delete; + + explicit operator bool() const { return _locked; } + + private: + Strata::FreeRTOS::RecursiveMutex &_mutex; + bool _locked = false; +}; } // namespace struct PhaseNode { + explicit PhaseNode(Strata::Placement placement = Strata::Placement::Default) + : allocationPlacement(placement), + name(Strata::Allocator{placement}), + dependencyNames(Strata::Allocator{placement}), + dependencies(Strata::Allocator{placement}) {} + + Strata::Placement allocationPlacement = Strata::Placement::Default; PhaseNodeType type = PhaseNodeType::Step; - std::string name; - std::vector dependencyNames; - std::vector dependencies; + Strata::String name; + Strata::Vector dependencyNames; + Strata::Vector dependencies; PhaseCallback initCallback; PhaseCallback deinitCallback; PhaseCallback startCallback; @@ -78,19 +143,18 @@ struct PhaseNode { }; struct PhaseImpl { + PhaseImpl() noexcept : mutex(Strata::FreeRTOS::RecursiveMutex::create()) {} + PhaseConfig config{}; - PhaseMutex mutex; - std::vector nodes; - std::vector initOrder; - std::vector startOrder; - std::vector validationMarks; + Strata::FreeRTOS::RecursiveMutex mutex; + PhasePlacedVector nodes; + PhasePlacedVector initOrder; + PhasePlacedVector startOrder; + PhasePlacedVector validationMarks; std::shared_ptr changeCallback; std::shared_ptr readyCallback; std::shared_ptr failedCallback; - TaskHandle_t taskHandle = nullptr; - SemaphoreHandle_t taskStarted = nullptr; - SemaphoreHandle_t taskExited = nullptr; - bool createdWithCaps = false; + Strata::FreeRTOS::Task task; bool initialized = false; bool registrationClosed = false; bool graphPrepared = false; @@ -98,20 +162,25 @@ struct PhaseImpl { bool stopRequested = false; bool ending = false; bool taskRunning = false; - bool taskExitComplete = false; + bool taskExitReady = false; bool paused = false; - bool deleteImplOnExit = false; std::array pauseReason{}; PhaseState currentState = PhaseState::Idle; - PhaseStackType actualStackType = PhaseStackType::Internal; + Strata::Region stackRegion = Strata::Region::Unknown; uint32_t bootCount = 0; uint32_t rollbackCount = 0; uint32_t changeCount = 0; size_t stackHighWaterMarkBytes = 0; - ~PhaseImpl() { - if (taskStarted != nullptr) vSemaphoreDelete(taskStarted); - if (taskExited != nullptr) vSemaphoreDelete(taskExited); + void initializeStorage(Strata::Placement placement, const PhaseConfig &incomingConfig) { + nodes.resetPlacement(placement); + initOrder.resetPlacement(placement); + startOrder.resetPlacement(placement); + validationMarks.resetPlacement(placement); + nodes.reserve(incomingConfig.maxNodes); + initOrder.reserve(incomingConfig.maxNodes); + startOrder.reserve(incomingConfig.maxNodes); + validationMarks.resize(incomingConfig.maxNodes); } static void taskEntry(void *arg) { @@ -123,7 +192,7 @@ struct PhaseImpl { { PhaseLock lock(mutex); if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - handle = taskHandle; + handle = task.handle(); } if (handle == nullptr) { return PhaseResult::failure(PhaseStatus::NotInitialized, "phase task is not available"); @@ -256,7 +325,7 @@ struct PhaseImpl { return PhaseResult::failure(PhaseStatus::InvalidCallback, "init callback is required"); } node.dependencies.clear(); - for (const std::string &dependencyName : node.dependencyNames) { + for (const Strata::String &dependencyName : node.dependencyNames) { const size_t dependencyIndex = findNodeIndex(dependencyName.c_str()); if (dependencyIndex >= nodes.size()) { return PhaseResult::failure(PhaseStatus::MissingDependency, "dependency was not registered"); @@ -432,4 +501,3 @@ struct PhaseImpl { emitChange(state, nodes[index].type, nodes[index].name.c_str(), result.message, result); return PhaseResult::success("optional node failed"); } - From 10b2420e1a6d3b3f0b660ef590beb8cc913a245c Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:28:43 +0200 Subject: [PATCH 03/25] Create Phase task through Strata --- src/internal/PhaseInit.inc | 113 +++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/src/internal/PhaseInit.inc b/src/internal/PhaseInit.inc index fbc2c4d..c01d111 100644 --- a/src/internal/PhaseInit.inc +++ b/src/internal/PhaseInit.inc @@ -1,19 +1,22 @@ -Phase::Phase() : _impl(new (std::nothrow) PhaseImpl()) {} +Phase::Phase() : _impl(Strata::makeUnique(Strata::Placement::Internal)) {} Phase::~Phase() { if (!_impl) return; bool calledFromPhaseTask = false; { PhaseLock lock(_impl->mutex); - if (lock && _impl->initialized && _impl->taskHandle == xTaskGetCurrentTaskHandle()) { + if (lock && _impl->initialized && + _impl->task.handle() == xTaskGetCurrentTaskHandle()) { _impl->ending = true; _impl->stopRequested = true; _impl->startRequested = false; - _impl->deleteImplOnExit = true; calledFromPhaseTask = true; } } if (calledFromPhaseTask) { + // A Strata-owned static task must be reclaimed from another task context. + // Destruction from a Phase callback is unsupported; release ownership here + // to avoid freeing the stack while it is still executing. (void)_impl.release(); return; } @@ -22,7 +25,20 @@ Phase::~Phase() { PhaseResult Phase::init(const PhaseConfig &config) { if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - SemaphoreHandle_t taskStarted = nullptr; + if (!_impl->mutex) { + return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase mutex allocation failed"); + } + if (!Strata::validMemoryPolicy(config.memory)) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid memory placement"); + } + if (config.maxNodes == 0 || config.maxDependenciesPerNode == 0) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid phase limits"); + } + if (!isValidStackSize(config.stackSizeBytes)) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid stack size"); + } + + TaskHandle_t taskHandle = nullptr; { PhaseLock lock(_impl->mutex); if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); @@ -32,81 +48,54 @@ PhaseResult Phase::init(const PhaseConfig &config) { if (_impl->initialized) { return PhaseResult::failure(PhaseStatus::AlreadyInitialized, "phase is already initialized"); } - if (config.maxNodes == 0 || config.maxDependenciesPerNode == 0) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid phase limits"); - } - if (!phase_task_support::isValidStackSize(config.stackSizeBytes)) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid stack size"); - } - if (_impl->taskStarted == nullptr) _impl->taskStarted = xSemaphoreCreateBinary(); - if (_impl->taskExited == nullptr) _impl->taskExited = xSemaphoreCreateBinary(); - if (_impl->taskStarted == nullptr || _impl->taskExited == nullptr) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase semaphore allocation failed"); - } - while (xSemaphoreTake(_impl->taskStarted, 0) == pdTRUE) {} - while (xSemaphoreTake(_impl->taskExited, 0) == pdTRUE) {} + _impl->config = config; - _impl->nodes.clear(); - _impl->initOrder.clear(); - _impl->startOrder.clear(); - _impl->validationMarks.clear(); - _impl->nodes.reserve(config.maxNodes); - _impl->initOrder.reserve(config.maxNodes); - _impl->startOrder.reserve(config.maxNodes); - _impl->validationMarks.resize(config.maxNodes, 0); - _impl->actualStackType = PhaseStackType::Internal; + _impl->initializeStorage(config.memory.allocation, config); _impl->registrationClosed = false; _impl->graphPrepared = false; _impl->startRequested = false; _impl->stopRequested = false; _impl->ending = false; _impl->taskRunning = false; - _impl->taskExitComplete = false; + _impl->taskExitReady = false; _impl->paused = false; - _impl->deleteImplOnExit = false; _impl->pauseReason.fill('\0'); - bool createdWithCaps = false; - bool usePsram = config.stackType == PhaseStackType::Psram || - (config.stackType == PhaseStackType::Auto && phase_task_support::hasExternalStackSupport()); - BaseType_t created = phase_task_support::createTask( + _impl->stackHighWaterMarkBytes = 0; + _impl->stackRegion = Strata::Region::Unknown; + + auto task = Strata::FreeRTOS::Task::create( PhaseImpl::taskEntry, - config.taskName, - config.stackSizeBytes, _impl.get(), - config.priority, - &_impl->taskHandle, - config.coreId, - usePsram, - createdWithCaps + Strata::FreeRTOS::TaskConfig{ + .name = config.taskName, + .stackBytes = config.stackSizeBytes, + .stackPlacement = config.memory.taskStack, + .priority = config.priority, + .affinity = config.coreId, + } ); - if (created != pdPASS && config.stackType == PhaseStackType::Auto && usePsram) { - usePsram = false; - created = phase_task_support::createTask( - PhaseImpl::taskEntry, - config.taskName, - config.stackSizeBytes, - _impl.get(), - config.priority, - &_impl->taskHandle, - config.coreId, - false, - createdWithCaps - ); - } - if (created != pdPASS) { - _impl->taskHandle = nullptr; + if (!task) { return PhaseResult::failure(PhaseStatus::TaskCreateFailed, "phase task create failed"); } - _impl->createdWithCaps = createdWithCaps; - _impl->actualStackType = usePsram && createdWithCaps ? PhaseStackType::Psram : PhaseStackType::Internal; + _impl->task = std::move(task); + _impl->stackRegion = _impl->task.stackRegion(); _impl->initialized = true; _impl->currentState = PhaseState::Idle; - taskStarted = _impl->taskStarted; + taskHandle = _impl->task.handle(); } - if (xSemaphoreTake(taskStarted, pdMS_TO_TICKS(kTaskStartTimeoutMs)) != pdTRUE) { - (void)end(kTaskStartTimeoutMs); - return PhaseResult::failure(PhaseStatus::Timeout, "phase task start timed out"); + + if (taskHandle != nullptr) xTaskNotifyGive(taskHandle); + const uint32_t startMs = millis(); + while (true) { + { + PhaseLock lock(_impl->mutex); + if (lock && _impl->taskRunning) break; + } + if (millis() - startMs >= kTaskStartTimeoutMs) { + (void)end(kTaskStartTimeoutMs); + return PhaseResult::failure(PhaseStatus::Timeout, "phase task start timed out"); + } + vTaskDelay(pdMS_TO_TICKS(1)); } return PhaseResult::success("phase initialized"); } - From 301a7af87b5c60b283973563a6281bfe0aa95de9 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:29:22 +0200 Subject: [PATCH 04/25] Use external Strata task teardown handoff --- src/internal/PhaseRuntimeLifecycle.inc | 29 ++++++-------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/internal/PhaseRuntimeLifecycle.inc b/src/internal/PhaseRuntimeLifecycle.inc index 620c57c..a6dbb7f 100644 --- a/src/internal/PhaseRuntimeLifecycle.inc +++ b/src/internal/PhaseRuntimeLifecycle.inc @@ -275,11 +275,11 @@ } void taskLoop() { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); { PhaseLock lock(mutex); if (lock) taskRunning = true; } - if (taskStarted != nullptr) xSemaphoreGive(taskStarted); while (!isEnding()) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if (isEnding()) break; @@ -298,34 +298,17 @@ if (localStart && !localStop) (void)runBoot(); } if (shouldStop()) (void)runShutdown(); - bool localCreatedWithCaps = false; - bool localDeleteImpl = false; - SemaphoreHandle_t localTaskExited = nullptr; { PhaseLock lock(mutex); if (lock) { currentState = PhaseState::Ended; + stackHighWaterMarkBytes = task.stackHighWaterMarkBytes(); + stackRegion = task.stackRegion(); taskRunning = false; - stackHighWaterMarkBytes = phase_task_support::currentStackHighWaterMarkBytes(); - localCreatedWithCaps = createdWithCaps; - localDeleteImpl = deleteImplOnExit; - localTaskExited = taskExited; + taskExitReady = true; } } - if (localDeleteImpl) { - PhaseImpl *self = this; - delete self; - } else { - if (localTaskExited != nullptr) xSemaphoreGive(localTaskExited); - { - PhaseLock lock(mutex); - if (lock) { - taskExitComplete = true; - taskHandle = nullptr; - } - } - } - phase_task_support::deleteCurrentTask(localCreatedWithCaps); + vTaskSuspend(nullptr); + for (;;) vTaskDelay(portMAX_DELAY); } }; - From 802d3c9665250f5ccebfebaa098c02bb221afce9 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:29:57 +0200 Subject: [PATCH 05/25] Use Strata diagnostics and callback ownership --- src/internal/PhaseControl.inc | 53 +++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/internal/PhaseControl.inc b/src/internal/PhaseControl.inc index bcdcb53..6c4db25 100644 --- a/src/internal/PhaseControl.inc +++ b/src/internal/PhaseControl.inc @@ -47,38 +47,39 @@ PhaseResult Phase::stop() { PhaseResult Phase::end(uint32_t timeoutMs) { if (!_impl) return PhaseResult::success("phase ended"); TaskHandle_t handle = nullptr; - SemaphoreHandle_t taskExited = nullptr; { PhaseLock lock(_impl->mutex); if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); if (!_impl->initialized) return PhaseResult::success("phase ended"); - if (_impl->taskHandle != nullptr && _impl->taskHandle == xTaskGetCurrentTaskHandle()) { + handle = _impl->task.handle(); + if (handle != nullptr && handle == xTaskGetCurrentTaskHandle()) { return PhaseResult::failure(PhaseStatus::Busy, "end cannot be called from the Phase task"); } _impl->ending = true; _impl->stopRequested = true; _impl->startRequested = false; - handle = _impl->taskHandle; - taskExited = _impl->taskExited; } if (handle != nullptr) xTaskNotifyGive(handle); + const uint32_t startMs = millis(); while (true) { + bool readyForDelete = false; { PhaseLock lock(_impl->mutex); - if (lock && _impl->taskExitComplete) { - _impl->initialized = false; - _impl->currentState = PhaseState::Ended; - return PhaseResult::success("phase ended"); - } + if (lock) readyForDelete = _impl->taskExitReady; + } + if (readyForDelete) { + _impl->task.reset(); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + _impl->initialized = false; + _impl->currentState = PhaseState::Ended; + return PhaseResult::success("phase ended"); } if (timeoutMs > 0 && millis() - startMs >= timeoutMs) { return PhaseResult::failure(PhaseStatus::Timeout, "phase end timed out"); } - const TickType_t waitTicks = timeoutMs == 0 ? pdMS_TO_TICKS(kWaitPollMs) : - pdMS_TO_TICKS(std::min(kWaitPollMs, timeoutMs)); - if (taskExited != nullptr) (void)xSemaphoreTake(taskExited, waitTicks); - else vTaskDelay(waitTicks); + vTaskDelay(pdMS_TO_TICKS(kWaitPollMs)); } } @@ -140,15 +141,21 @@ PhaseDiag Phase::getDiagnostics() { diag.changeCount = _impl->changeCount; diag.stackHighWaterMarkBytes = _impl->stackHighWaterMarkBytes; diag.state = _impl->currentState; - diag.requestedStackType = _impl->config.stackType; - diag.actualStackType = _impl->actualStackType; + diag.requestedStackPlacement = _impl->config.memory.taskStack; + diag.stackRegion = _impl->stackRegion; + diag.allocationPlacement = _impl->config.memory.allocation; return diag; } void Phase::onChange(PhaseChangeCallback callback) { if (!_impl) return; std::shared_ptr replacement; - if (callback) replacement = std::make_shared(std::move(callback)); + if (callback) { + replacement = Strata::makeShared( + Strata::Placement::Internal, + std::move(callback) + ); + } PhaseLock lock(_impl->mutex); if (lock) _impl->changeCallback = std::move(replacement); } @@ -156,7 +163,12 @@ void Phase::onChange(PhaseChangeCallback callback) { void Phase::onReady(PhaseReadyCallback callback) { if (!_impl) return; std::shared_ptr replacement; - if (callback) replacement = std::make_shared(std::move(callback)); + if (callback) { + replacement = Strata::makeShared( + Strata::Placement::Internal, + std::move(callback) + ); + } PhaseLock lock(_impl->mutex); if (lock) _impl->readyCallback = std::move(replacement); } @@ -164,7 +176,12 @@ void Phase::onReady(PhaseReadyCallback callback) { void Phase::onFailed(PhaseFailedCallback callback) { if (!_impl) return; std::shared_ptr replacement; - if (callback) replacement = std::make_shared(std::move(callback)); + if (callback) { + replacement = Strata::makeShared( + Strata::Placement::Internal, + std::move(callback) + ); + } PhaseLock lock(_impl->mutex); if (lock) _impl->failedCallback = std::move(replacement); } From 983d90996d28e3d8105f5e64de68a462854b8690 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:30:38 +0200 Subject: [PATCH 06/25] Place Phase graph nodes through Strata --- src/internal/PhaseRegistration.inc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/internal/PhaseRegistration.inc b/src/internal/PhaseRegistration.inc index 652776c..41b5b88 100644 --- a/src/internal/PhaseRegistration.inc +++ b/src/internal/PhaseRegistration.inc @@ -18,7 +18,7 @@ PhaseStepBuilder Phase::addStep(const char *name, PhaseCallback initCallback, Ph if (_impl->findNodeIndex(name) < _impl->nodes.size()) { return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name")); } - PhaseNode node; + PhaseNode node(_impl->config.memory.allocation); node.type = PhaseNodeType::Step; node.name = name; node.dependencyNames.reserve(_impl->config.maxDependenciesPerNode); @@ -47,7 +47,7 @@ PhaseGroupBuilder Phase::addGroup(const char *name) { if (_impl->findNodeIndex(name) < _impl->nodes.size()) { return PhaseGroupBuilder(this, 0, PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name")); } - PhaseNode node; + PhaseNode node(_impl->config.memory.allocation); node.type = PhaseNodeType::Group; node.name = name; node.dependencyNames.reserve(_impl->config.maxDependenciesPerNode); @@ -72,7 +72,9 @@ PhaseResult Phase::addDependency(size_t index, const char *name) { return PhaseResult::failure(PhaseStatus::TooManyDependencies, "too many dependencies"); } if (std::find(node.dependencyNames.begin(), node.dependencyNames.end(), name) == node.dependencyNames.end()) { - node.dependencyNames.emplace_back(name); + Strata::String dependency{Strata::Allocator{node.allocationPlacement}}; + dependency = name; + node.dependencyNames.push_back(std::move(dependency)); _impl->graphPrepared = false; } return PhaseResult::success("dependency added"); From d91d00c2067e53f3071137b3b4c614dc57ddc6c4 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:30:44 +0200 Subject: [PATCH 07/25] Remove Phase-specific mutex wrapper --- src/internal/PhaseMutex.h | 57 --------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 src/internal/PhaseMutex.h diff --git a/src/internal/PhaseMutex.h b/src/internal/PhaseMutex.h deleted file mode 100644 index d6ebff2..0000000 --- a/src/internal/PhaseMutex.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include -#include -#include - -class PhaseMutex { - public: - PhaseMutex() { - _handle = xSemaphoreCreateRecursiveMutex(); - } - - ~PhaseMutex() { - if (_handle != nullptr) { - vSemaphoreDelete(_handle); - } - } - - PhaseMutex(const PhaseMutex &) = delete; - PhaseMutex &operator=(const PhaseMutex &) = delete; - - bool lock(TickType_t timeout = portMAX_DELAY) { - return _handle != nullptr && xSemaphoreTakeRecursive(_handle, timeout) == pdTRUE; - } - - void unlock() { - if (_handle != nullptr) { - xSemaphoreGiveRecursive(_handle); - } - } - - private: - SemaphoreHandle_t _handle = nullptr; -}; - -class PhaseLock { - public: - explicit PhaseLock(PhaseMutex &mutex) : _mutex(mutex), _locked(mutex.lock()) { - } - - ~PhaseLock() { - if (_locked) { - _mutex.unlock(); - } - } - - PhaseLock(const PhaseLock &) = delete; - PhaseLock &operator=(const PhaseLock &) = delete; - - explicit operator bool() const { - return _locked; - } - - private: - PhaseMutex &_mutex; - bool _locked = false; -}; From 4e05d964608214b26e173df4c1f0ccca866d5fd1 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:30:48 +0200 Subject: [PATCH 08/25] Remove Phase-specific task allocation support --- src/internal/PhaseTaskSupport.h | 106 -------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 src/internal/PhaseTaskSupport.h diff --git a/src/internal/PhaseTaskSupport.h b/src/internal/PhaseTaskSupport.h deleted file mode 100644 index da48fdb..0000000 --- a/src/internal/PhaseTaskSupport.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include -#include - -extern "C" { -#include "esp_heap_caps.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -} - -#if __has_include("freertos/idf_additions.h") -extern "C" { -#include "freertos/idf_additions.h" -} -#define PHASE_HAS_IDF_TASK_CAPS 1 -#else -#define PHASE_HAS_IDF_TASK_CAPS 0 -#endif - -#if PHASE_HAS_IDF_TASK_CAPS && defined(configSUPPORT_STATIC_ALLOCATION) && \ - (configSUPPORT_STATIC_ALLOCATION == 1) && defined(MALLOC_CAP_SPIRAM) -#define PHASE_CAN_USE_EXTERNAL_STACKS 1 -#else -#define PHASE_CAN_USE_EXTERNAL_STACKS 0 -#endif - -namespace phase_task_support { -constexpr size_t kMinStackSizeBytes = 1024; - -#if defined(MALLOC_CAP_SPIRAM) -constexpr UBaseType_t kExternalStackCaps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; -#else -constexpr UBaseType_t kExternalStackCaps = MALLOC_CAP_8BIT; -#endif - -inline bool hasExternalStackSupport() { -#if PHASE_CAN_USE_EXTERNAL_STACKS - return heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0; -#else - return false; -#endif -} - -inline bool isValidStackSize(size_t stackBytes) { - return stackBytes >= kMinStackSizeBytes && (stackBytes % sizeof(StackType_t)) == 0; -} - -inline size_t currentStackHighWaterMarkBytes() { -#if defined(INCLUDE_uxTaskGetStackHighWaterMark) && (INCLUDE_uxTaskGetStackHighWaterMark == 1) - return static_cast(uxTaskGetStackHighWaterMark(nullptr)); -#else - return 0; -#endif -} - -inline BaseType_t createTask( - TaskFunction_t entry, - const char *name, - size_t stackBytes, - void *arg, - UBaseType_t priority, - TaskHandle_t *handle, - BaseType_t coreId, - bool usePsramStack, - bool &createdWithCaps -) { - createdWithCaps = false; - if (!isValidStackSize(stackBytes)) return pdFAIL; - if (usePsramStack) { -#if PHASE_CAN_USE_EXTERNAL_STACKS - if (!hasExternalStackSupport()) return pdFAIL; - const BaseType_t created = xTaskCreatePinnedToCoreWithCaps( - entry, - name, - static_cast(stackBytes), - arg, - priority, - handle, - coreId, - kExternalStackCaps - ); - createdWithCaps = created == pdPASS; - return created; -#else - return pdFAIL; -#endif - } - if (coreId == tskNO_AFFINITY) { - return xTaskCreate(entry, name, static_cast(stackBytes), arg, priority, handle); - } - return xTaskCreatePinnedToCore(entry, name, static_cast(stackBytes), arg, priority, handle, coreId); -} - -inline void deleteCurrentTask(bool withCaps) { -#if PHASE_CAN_USE_EXTERNAL_STACKS - if (withCaps) { - vTaskDeleteWithCaps(xTaskGetCurrentTaskHandle()); - return; - } -#else - (void)withCaps; -#endif - vTaskDelete(nullptr); -} -} // namespace phase_task_support From be0663240015e3c80de67ece408131232a7334ad Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:31:50 +0200 Subject: [PATCH 09/25] Add host Strata API test double --- tests/host/stubs/Strata.h | 119 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/host/stubs/Strata.h diff --git a/tests/host/stubs/Strata.h b/tests/host/stubs/Strata.h new file mode 100644 index 0000000..6cc62e0 --- /dev/null +++ b/tests/host/stubs/Strata.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Strata { + +enum class Placement : std::uint8_t { + Default, + Internal, + PreferExternal, + RequireExternal, +}; + +enum class Region : std::uint8_t { + Unknown, + Internal, + External, +}; + +constexpr bool validPlacement(Placement placement) noexcept { + switch (placement) { + case Placement::Default: + case Placement::Internal: + case Placement::PreferExternal: + case Placement::RequireExternal: return true; + } + return false; +} + +constexpr const char *toString(Placement placement) noexcept { + switch (placement) { + case Placement::Default: return "default"; + case Placement::Internal: return "internal"; + case Placement::PreferExternal: return "prefer-external"; + case Placement::RequireExternal: return "require-external"; + } + return "unknown"; +} + +constexpr const char *toString(Region region) noexcept { + switch (region) { + case Region::Unknown: return "unknown"; + case Region::Internal: return "internal"; + case Region::External: return "external"; + } + return "unknown"; +} + +struct MemoryPolicy { + Placement allocation{Placement::Default}; + Placement taskStack{Placement::Internal}; +}; + +constexpr bool validMemoryPolicy(const MemoryPolicy &policy) noexcept { + return validPlacement(policy.allocation) && validPlacement(policy.taskStack); +} + +template +class Allocator { + public: + using value_type = T; + + explicit Allocator(Placement placement = Placement::Default) noexcept : _placement(placement) {} + + template + Allocator(const Allocator &other) noexcept : _placement(other.placement()) {} + + T *allocate(std::size_t count) { + return std::allocator{}.allocate(count); + } + + void deallocate(T *ptr, std::size_t count) noexcept { + std::allocator{}.deallocate(ptr, count); + } + + Placement placement() const noexcept { return _placement; } + + template + struct rebind { using other = Allocator; }; + + private: + Placement _placement; +}; + +template +constexpr bool operator==(const Allocator &lhs, const Allocator &rhs) noexcept { + return lhs.placement() == rhs.placement(); +} + +template +constexpr bool operator!=(const Allocator &lhs, const Allocator &rhs) noexcept { + return !(lhs == rhs); +} + +template +using Vector = std::vector>; + +using String = std::basic_string, Allocator>; + +template +using UniquePtr = std::unique_ptr; + +template +UniquePtr makeUnique(Placement, Args &&...args) noexcept { + return UniquePtr{new (std::nothrow) T(std::forward(args)...)}; +} + +template +std::shared_ptr makeShared(Placement placement, Args &&...args) { + return std::allocate_shared(Allocator{placement}, std::forward(args)...); +} + +} // namespace Strata From 008d1f78d92d630878d747db264b535422e25a4d Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:31:59 +0200 Subject: [PATCH 10/25] Add host Strata mutex test double --- tests/host/stubs/strata/freertos/Mutex.h | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/host/stubs/strata/freertos/Mutex.h diff --git a/tests/host/stubs/strata/freertos/Mutex.h b/tests/host/stubs/strata/freertos/Mutex.h new file mode 100644 index 0000000..9e37c5a --- /dev/null +++ b/tests/host/stubs/strata/freertos/Mutex.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +namespace Strata::FreeRTOS { + +class RecursiveMutex { + public: + RecursiveMutex() noexcept = default; + ~RecursiveMutex() noexcept { reset(); } + RecursiveMutex(const RecursiveMutex &) = delete; + RecursiveMutex &operator=(const RecursiveMutex &) = delete; + RecursiveMutex(RecursiveMutex &&other) noexcept : _handle(std::exchange(other._handle, nullptr)) {} + RecursiveMutex &operator=(RecursiveMutex &&other) noexcept { + if (this != &other) { + reset(); + _handle = std::exchange(other._handle, nullptr); + } + return *this; + } + + static RecursiveMutex create() noexcept { + RecursiveMutex mutex; + mutex._handle = xSemaphoreCreateRecursiveMutex(); + return mutex; + } + + void reset() noexcept { + if (_handle != nullptr) { + vSemaphoreDelete(_handle); + _handle = nullptr; + } + } + + bool lock(TickType_t ticks = portMAX_DELAY) noexcept { + return _handle != nullptr && xSemaphoreTakeRecursive(_handle, ticks) == pdTRUE; + } + + void unlock() noexcept { + if (_handle != nullptr) (void)xSemaphoreGiveRecursive(_handle); + } + + explicit operator bool() const noexcept { return _handle != nullptr; } + + private: + SemaphoreHandle_t _handle = nullptr; +}; + +} // namespace Strata::FreeRTOS From 12d9a9cba3a214f6fc42aae6cac91cf772467463 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:32:10 +0200 Subject: [PATCH 11/25] Add host Strata task test double --- tests/host/stubs/strata/freertos/Task.h | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/host/stubs/strata/freertos/Task.h diff --git a/tests/host/stubs/strata/freertos/Task.h b/tests/host/stubs/strata/freertos/Task.h new file mode 100644 index 0000000..967ef0e --- /dev/null +++ b/tests/host/stubs/strata/freertos/Task.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include + +namespace Strata::FreeRTOS { + +struct TaskConfig { + const char *name{"strata"}; + std::size_t stackBytes{0}; + Placement stackPlacement{Placement::Internal}; + UBaseType_t priority{1}; + BaseType_t affinity{tskNO_AFFINITY}; +}; + +class Task { + public: + Task() noexcept = default; + ~Task() noexcept { reset(); } + Task(const Task &) = delete; + Task &operator=(const Task &) = delete; + Task(Task &&other) noexcept { moveFrom(other); } + Task &operator=(Task &&other) noexcept { + if (this != &other) { + reset(); + moveFrom(other); + } + return *this; + } + + static Task create(TaskFunction_t entry, void *context, const TaskConfig &config) noexcept { + Task task; + TaskHandle_t handle = nullptr; + const BaseType_t created = config.affinity == tskNO_AFFINITY + ? xTaskCreate(entry, config.name, static_cast(config.stackBytes), context, config.priority, &handle) + : xTaskCreatePinnedToCore(entry, config.name, static_cast(config.stackBytes), context, config.priority, &handle, config.affinity); + if (created == pdPASS) { + task._handle = handle; + task._stackBytes = config.stackBytes; + task._placement = config.stackPlacement; + task._region = config.stackPlacement == Placement::Internal ? Region::Internal : Region::External; + } + return task; + } + + void reset() noexcept { + if (_handle != nullptr) { + vTaskDelete(_handle); + _handle = nullptr; + } + } + + TaskHandle_t handle() const noexcept { return _handle; } + explicit operator bool() const noexcept { return _handle != nullptr; } + std::size_t stackSizeBytes() const noexcept { return _stackBytes; } + Placement stackPlacement() const noexcept { return _placement; } + Region stackRegion() const noexcept { return _region; } + std::size_t stackHighWaterMarkBytes() const noexcept { + return _handle != nullptr ? static_cast(uxTaskGetStackHighWaterMark(_handle)) : 0; + } + + private: + void moveFrom(Task &other) noexcept { + _handle = std::exchange(other._handle, nullptr); + _stackBytes = other._stackBytes; + _placement = other._placement; + _region = other._region; + } + + TaskHandle_t _handle = nullptr; + std::size_t _stackBytes = 0; + Placement _placement = Placement::Internal; + Region _region = Region::Unknown; +}; + +} // namespace Strata::FreeRTOS From 6460efeb6b52096244a9578b7baf276c0b3b2629 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:32:19 +0200 Subject: [PATCH 12/25] Support external task teardown in host stubs --- tests/host/stubs/freertos/task.h | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/host/stubs/freertos/task.h b/tests/host/stubs/freertos/task.h index 6cdf6c8..79e5872 100644 --- a/tests/host/stubs/freertos/task.h +++ b/tests/host/stubs/freertos/task.h @@ -8,6 +8,7 @@ BaseType_t xTaskCreatePinnedToCore(TaskFunction_t, const char *, uint32_t, void void xTaskNotifyGive(TaskHandle_t); uint32_t ulTaskNotifyTake(BaseType_t, TickType_t); void vTaskDelay(TickType_t); +void vTaskSuspend(TaskHandle_t); TaskHandle_t xTaskGetCurrentTaskHandle(); void vTaskDelete(TaskHandle_t); UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t); From 9e5c2bb1f06806a98dd2e0255b1cc1de18b83d30 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:32:31 +0200 Subject: [PATCH 13/25] Model suspended Strata task teardown in host tests --- tests/host/task_stubs.cpp | 65 ++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/tests/host/task_stubs.cpp b/tests/host/task_stubs.cpp index 0f268f2..ed5b0b7 100644 --- a/tests/host/task_stubs.cpp +++ b/tests/host/task_stubs.cpp @@ -4,37 +4,88 @@ #include #include +namespace { +struct TaskDeleted {}; +} + struct FakeTask { std::mutex mutex; std::condition_variable cv; uint32_t notifications = 0; + bool deleted = false; }; + thread_local TaskHandle_t gCurrentTask = nullptr; + BaseType_t xTaskCreate(TaskFunction_t entry, const char *, uint32_t, void *arg, UBaseType_t, TaskHandle_t *out) { auto *task = new FakeTask(); *out = task; - std::thread([task, entry, arg] { gCurrentTask = task; entry(arg); gCurrentTask = nullptr; }).detach(); + std::thread([task, entry, arg] { + gCurrentTask = task; + try { + entry(arg); + } catch (const TaskDeleted &) { + } + gCurrentTask = nullptr; + }).detach(); return pdPASS; } + BaseType_t xTaskCreatePinnedToCore(TaskFunction_t entry, const char *name, uint32_t stack, void *arg, UBaseType_t priority, TaskHandle_t *out, BaseType_t) { return xTaskCreate(entry, name, stack, arg, priority, out); } + void xTaskNotifyGive(TaskHandle_t task) { if (!task) return; - { std::lock_guard lock(task->mutex); task->notifications++; } + { + std::lock_guard lock(task->mutex); + task->notifications++; + } task->cv.notify_all(); } + uint32_t ulTaskNotifyTake(BaseType_t clear, TickType_t timeout) { TaskHandle_t task = gCurrentTask; if (!task) return 0; std::unique_lock lock(task->mutex); - if (timeout == portMAX_DELAY) task->cv.wait(lock, [&] { return task->notifications > 0; }); - else if (!task->cv.wait_for(lock, std::chrono::milliseconds(timeout), [&] { return task->notifications > 0; })) return 0; + if (timeout == portMAX_DELAY) { + task->cv.wait(lock, [&] { return task->notifications > 0 || task->deleted; }); + } else if (!task->cv.wait_for( + lock, + std::chrono::milliseconds(timeout), + [&] { return task->notifications > 0 || task->deleted; })) { + return 0; + } + if (task->deleted) throw TaskDeleted{}; uint32_t value = task->notifications; - if (clear) task->notifications = 0; else task->notifications--; + if (clear) task->notifications = 0; + else task->notifications--; return value; } -void vTaskDelay(TickType_t ticks) { std::this_thread::sleep_for(std::chrono::milliseconds(ticks)); } + +void vTaskDelay(TickType_t ticks) { + if (ticks == portMAX_DELAY) ticks = 1; + std::this_thread::sleep_for(std::chrono::milliseconds(ticks)); +} + +void vTaskSuspend(TaskHandle_t handle) { + TaskHandle_t task = handle != nullptr ? handle : gCurrentTask; + if (!task) return; + std::unique_lock lock(task->mutex); + task->cv.wait(lock, [&] { return task->deleted; }); + throw TaskDeleted{}; +} + TaskHandle_t xTaskGetCurrentTaskHandle() { return gCurrentTask; } -void vTaskDelete(TaskHandle_t) {} + +void vTaskDelete(TaskHandle_t handle) { + TaskHandle_t task = handle != nullptr ? handle : gCurrentTask; + if (!task) return; + { + std::lock_guard lock(task->mutex); + task->deleted = true; + } + task->cv.notify_all(); +} + UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t) { return 123; } From 7a9bdaa8ddf8ed7cdbbc886fd2161becc5fc8b8c Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:32:48 +0200 Subject: [PATCH 14/25] Declare Strata dependency for Phase v0.2.0 --- library.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library.json b/library.json index acfe318..1cefba9 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Phase", - "version": "0.1.0", + "version": "0.2.0", "description": "Async application lifecycle orchestration library for ESP32.", "keywords": [ "esp32", @@ -22,6 +22,9 @@ "license": "MIT", "frameworks": "arduino", "platforms": "espressif32", + "dependencies": { + "Strata": "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/ZekStack/strata.git#v0.1.1" + }, "build": { "srcDir": "src", "includeDir": "src", From 0aad512e4a3a037f345c99a72f28aad0eb43671c Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:32:53 +0200 Subject: [PATCH 15/25] Bump Phase metadata to v0.2.0 --- library.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library.properties b/library.properties index ccce000..0d1cbbc 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=Phase -version=0.1.0 +version=0.2.0 author=zekageri maintainer=zekageri sentence=Application lifecycle orchestration library for ESP32. -paragraph=Provides async dependency-ordered boot, optional start and stop steps, virtual readiness groups, cooperative pause and resume, rollback, reverse shutdown, progress callbacks, and result-based errors. +paragraph=Provides async dependency-ordered boot, optional start and stop steps, virtual readiness groups, cooperative pause and resume, rollback, reverse shutdown, progress callbacks, Strata-backed memory placement, and result-based errors. category=Other url=https://github.com/ZekStack/phase repository=https://github.com/ZekStack/phase.git From e53c50fe63ff168611df4834bb1d255b01a7eb8d Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:33:27 +0200 Subject: [PATCH 16/25] Validate Strata-backed Phase builds --- .github/workflows/ci.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d49700c..9825db1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ env: PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip PIOARDUINO_PLATFORM_VERSION: 55.03.39 PIOARDUINO_VERSION: 6.1.19 + STRATA_VERSION: v0.1.1 jobs: source-audit: @@ -26,12 +27,21 @@ jobs: - name: Audit production sources shell: bash run: | + set -e if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then echo "Embedded safety audit failed" exit 1 fi if grep -RInE '#include[[:space:]]+[<"](Fresh|Pulse|Signal|Tempo|Trace|Worker|Vault|Link|Courier|Flow|Lingo)\.h[>"]' src; then - echo "Embedded safety audit failed: Phase must not depend on other ZekStack libraries" + echo "Embedded safety audit failed: Phase must not depend on other ZekStack libraries except Strata" + exit 1 + fi + if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then + echo "Phase allocations and owned FreeRTOS primitives must route through Strata" + exit 1 + fi + if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then + echo "Phase must not depend on ESP-IDF allocation internals" exit 1 fi if [[ "$GITHUB_REF" == refs/tags/v* ]]; then @@ -102,6 +112,7 @@ jobs: --board ${{ matrix.board }} \ --lib="." \ --project-option "platform=${PIOARDUINO_PLATFORM_URL}" \ + --project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \ --project-option "build_unflags=-std=gnu++11" \ --project-option "build_flags=-std=gnu++20" fi @@ -146,12 +157,16 @@ jobs: arduino-cli core update-index arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}" - - name: Add local library to sketchbook + - name: Add local libraries to sketchbook run: | set -e SKETCHBOOK_DIR="${HOME}/Arduino" mkdir -p "$SKETCHBOOK_DIR/libraries/Phase" rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Phase/" + rm -rf "$SKETCHBOOK_DIR/libraries/Strata" + git clone --depth 1 --branch "${STRATA_VERSION}" \ + https://github.com/ZekStack/strata.git \ + "$SKETCHBOOK_DIR/libraries/Strata" - name: Build examples (${{ matrix.board.name }}) env: From d378680b62a451babc6f1d27363a3c1f46f735f2 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:34:19 +0200 Subject: [PATCH 17/25] Test Phase Strata memory policy --- tests/host/test_phase.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/host/test_phase.cpp b/tests/host/test_phase.cpp index 57c4f98..855e394 100644 --- a/tests/host/test_phase.cpp +++ b/tests/host/test_phase.cpp @@ -190,6 +190,29 @@ void testPendingStartCancellationAndRestart() { require(waitForState(phase, PhaseState::Ready), "restart from stopped should become ready"); require(static_cast(phase.end()), "end should succeed"); } + +void testMemoryPolicy() { + PhaseConfig defaults; + require(defaults.memory.allocation == Strata::Placement::Default, "default allocation placement should stay backend-default"); + require(defaults.memory.taskStack == Strata::Placement::PreferExternal, "default task stack should preserve old Auto semantics"); + + Phase phase; + PhaseConfig config; + config.memory.allocation = Strata::Placement::PreferExternal; + config.memory.taskStack = Strata::Placement::Internal; + require(static_cast(phase.init(config)), "internal Strata policy should initialize"); + PhaseDiag diag = phase.getDiagnostics(); + require(diag.requestedStackPlacement == Strata::Placement::Internal, "diagnostics should expose requested stack placement"); + require(diag.stackRegion == Strata::Region::Internal, "diagnostics should expose actual stack region"); + require(diag.allocationPlacement == Strata::Placement::PreferExternal, "diagnostics should expose graph allocation placement"); + require(static_cast(phase.end()), "policy test should end cleanly"); + + Phase invalidPhase; + PhaseConfig invalid; + invalid.memory.taskStack = static_cast(0xFF); + PhaseResult invalidResult = invalidPhase.init(invalid); + require(!invalidResult && invalidResult.status == PhaseStatus::InvalidArgument, "invalid Strata policy should be rejected"); +} } int main() { @@ -199,6 +222,7 @@ int main() { testCallbackSafety(); testLifecycleDoesNotAllocate(); testPendingStartCancellationAndRestart(); + testMemoryPolicy(); std::cout << "Phase host tests passed\n"; return 0; } From b7aa0616cfc8e4ca674ddace2b495847e800c4b8 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:34:55 +0200 Subject: [PATCH 18/25] Document Strata-backed Phase v0.2.0 --- README.md | 169 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 102 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 8cf34e9..6f12d6c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Phase is an async application lifecycle orchestration library for ESP32. -Phase helps you boot and shut down larger Arduino ESP32 applications in a predictable order. It is designed for projects with multiple modules that depend on each other and focuses on dependency-ordered lifecycle steps, readiness gates, cooperative pause/resume, rollback, and result-based errors. +Phase helps you boot and shut down larger Arduino ESP32 applications in a predictable order. It owns lifecycle orchestration and dependency policy while [Strata](https://github.com/ZekStack/strata) owns Phase memory placement and low-level FreeRTOS storage. [![CI](https://github.com/ZekStack/phase/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/phase/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/ZekStack/phase?sort=semver)](https://github.com/ZekStack/phase/releases) @@ -14,9 +14,13 @@ Phase helps you boot and shut down larger Arduino ESP32 applications in a predic * **Dependency order** - steps and groups declare what must be ready first. * **Two-layer lifecycle** - simple modules use init/deinit, advanced modules add start/stop. * **Readiness groups** - wait for virtual gates such as network link or internet access. -* **Production-minded** - thread-safe internals, no exceptions, rollback, diagnostics, and progress callbacks. +* **Consistent memory policy** - `Strata::MemoryPolicy` controls graph allocations and task-stack placement. +* **Strata-owned FreeRTOS storage** - the Phase task stack, task control block, and recursive mutex storage are owned by Strata. +* **Production-minded** - thread-safe internals, rollback, diagnostics, progress callbacks, and allocation-free lifecycle execution after registration. -## Install +## Dependency + +Phase `v0.2.0` requires Strata `v0.1.1`. ### PlatformIO @@ -27,7 +31,7 @@ board = esp32dev framework = arduino lib_deps = - https://github.com/ZekStack/phase.git + https://github.com/ZekStack/phase.git#v0.2.0 build_flags = -std=gnu++20 @@ -35,16 +39,19 @@ build_unflags = -std=gnu++11 ``` -### Arduino IDE +Phase's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it transitively. -Phase is not published to Arduino Library Manager yet. +### Arduino IDE -Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder. +Phase and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory: -```txt +```text +Arduino/libraries/Strata Arduino/libraries/Phase ``` +Use Strata `v0.1.1` or a compatible later release. + ## Quick start ```cpp @@ -54,37 +61,73 @@ Arduino/libraries/Phase Phase phase; void setup() { - Serial.begin(115200); - - PhaseResult initResult = phase.init(); - if (!initResult) { - Serial.println(initResult.message); - return; - } - - phase.add("storage", []() { - Serial.println("storage init"); - }); - - phase.add("network", []() { - Serial.println("network init"); - }).start([]() { - Serial.println("network start"); - }); - - phase.onReady([]() { - Serial.println("app ready"); - }); - - phase.start(); - Serial.println("setup continues while Phase boots"); + Serial.begin(115200); + + PhaseResult initResult = phase.init(); + if (!initResult) { + Serial.println(initResult.message); + return; + } + + phase.add("storage", []() { + Serial.println("storage init"); + }); + + phase.add("network", []() { + Serial.println("network init"); + }).start([]() { + Serial.println("network start"); + }); + + phase.onReady([]() { + Serial.println("app ready"); + }); + + phase.start(); + Serial.println("setup continues while Phase boots"); } void loop() { - delay(1000); + delay(1000); } ``` +## Memory policy + +Phase uses the ZekStack-standard Strata configuration shape: + +```cpp +PhaseConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.taskStack = Strata::Placement::PreferExternal; + +PhaseResult result = phase.init(config); +``` + +`memory.allocation` controls movable Phase-owned graph storage: node records, node/dependency names, dependency indexes, lifecycle order, and validation backing. + +`memory.taskStack` controls the Phase task stack. Task control-block and mutex control storage remain internal through Strata. + +The default policy preserves Phase v0.1.0 behavior: + +```cpp +allocation = Strata::Placement::Default; +taskStack = Strata::Placement::PreferExternal; +``` + +`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails task creation rather than consuming internal memory. + +Diagnostics report requested placement separately from the observed memory region: + +```cpp +PhaseDiag diag = phase.getDiagnostics(); +Serial.printf( + "requested=%s actual=%s\n", + Strata::toString(diag.requestedStackPlacement), + Strata::toString(diag.stackRegion) +); +``` + ## Important notes > [!IMPORTANT] @@ -95,11 +138,26 @@ void loop() { * Group condition polling timeouts are enforced by the Phase task. * Registration closes after a successful `start()` request. * `stop()`, `pause()`, and `resume()` may be called from Phase callbacks. `end()` must be called from another task and returns `Busy` when called from the Phase task. -* The destructor waits for the Phase task to stop using its internal state. Destruction from a Phase callback is deferred safely until the worker exits. -* Registration and graph preparation use `std::vector`, `std::string`, and `std::function`. Node storage, dependency indexes, and lifecycle order are preallocated before the worker starts; lifecycle execution does not allocate. +* A `Phase` object must not be destroyed from one of its own Phase callbacks. Strata task storage can only be reclaimed safely from another task context. +* Graph storage is allocated during initialization/registration and pre-reserved from the configured limits. Lifecycle execution remains allocation-free. +* Callback callables remain `std::function`; allocations performed internally by an arbitrary callable representation are outside Phase's placement contract. * `PhaseChange` string pointers are valid for the complete callback invocation. Event messages and pause reasons are copied into bounded internal snapshots and may be truncated to 191 characters. * Stop/deinit failures are best-effort and are reported through `onChange()` while remaining cleanup continues. -* Phase does not depend on other ZekStack libraries. +* Phase depends only on Strata within the ZekStack library ecosystem. + +## Migrating from v0.1.x + +Phase `v0.2.0` removes `PhaseStackType` and `PhaseConfig::stackType`. + +| v0.1.x | v0.2.0 | +| --- | --- | +| `PhaseStackType::Auto` | `Strata::Placement::PreferExternal` | +| `PhaseStackType::Internal` | `Strata::Placement::Internal` | +| `PhaseStackType::Psram` | `Strata::Placement::RequireExternal` | +| `diag.requestedStackType` | `diag.requestedStackPlacement` | +| `diag.actualStackType` | `diag.stackRegion` | + +For the old default behavior, no configuration change is required: the v0.2.0 default task placement is already `PreferExternal`. ## Examples @@ -112,12 +170,7 @@ void loop() { | `OptionalNodes` | Optional node failure and skipped dependent behavior. | | `BindableCallbacks` | Bind private class methods with lambdas. | | `ManualShutdown` | Request reverse stop/deinit from `loop()`. | - -Start with: - -```txt -examples/Basic -``` +| `MemoryPolicy` | Configure Strata graph/task placement and inspect diagnostics. | ## Documentation @@ -126,7 +179,7 @@ Detailed documentation is available in the `docs/` folder. | Document | Description | | --- | --- | | [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first lifecycle flow. | -| [`docs/configuration.md`](docs/configuration.md) | Task, timeout, limit, and polling options. | +| [`docs/configuration.md`](docs/configuration.md) | Memory, task, timeout, limit, and polling options. | | [`docs/api.md`](docs/api.md) | Public classes, methods, callbacks, and result types. | | [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. | | [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and behavior notes. | @@ -144,8 +197,6 @@ phase.onFailed([](PhaseResult result) {}); phase.start(); ``` -For the full API, see [`docs/api.md`](docs/api.md). - ## Compatibility | Item | Support | @@ -154,26 +205,10 @@ For the full API, see [`docs/api.md`](docs/api.md). | Platform | `espressif32` | | Language | C++20 | | Filesystem | none | -| PSRAM | Optional for task stacks when ESP-IDF support is available | -| Dependencies | none | -| Exceptions | Not used | -| Status | `0.1.0` release candidate | - -## Configuration - -```cpp -PhaseConfig config; -config.stackSizeBytes = 4096; -config.priority = 1; -config.coreId = tskNO_AFFINITY; -config.stackType = PhaseStackType::Auto; -config.defaultInitTimeoutMs = 30000; -config.conditionPollIntervalMs = 100; - -PhaseResult result = phase.init(config); -``` - -For all options, see [`docs/configuration.md`](docs/configuration.md). +| PSRAM | Optional; controlled through Strata placement | +| Dependencies | Strata `v0.1.1` | +| Exceptions | Not intentionally used by Phase | +| Status | `0.2.0` | ## Error handling @@ -183,8 +218,8 @@ Phase reports operation status through `PhaseResult`. PhaseResult result = phase.start(); if (!result) { - Serial.println(result.message); - return; + Serial.println(result.message); + return; } ``` From 8367729679617a750595b5613860d39a7dbdcdd7 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:35:05 +0200 Subject: [PATCH 19/25] Add Phase Strata memory policy example --- examples/MemoryPolicy/MemoryPolicy.ino | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/MemoryPolicy/MemoryPolicy.ino diff --git a/examples/MemoryPolicy/MemoryPolicy.ino b/examples/MemoryPolicy/MemoryPolicy.ino new file mode 100644 index 0000000..feb1a10 --- /dev/null +++ b/examples/MemoryPolicy/MemoryPolicy.ino @@ -0,0 +1,36 @@ +#include +#include + +Phase phase; + +void setup() { + Serial.begin(115200); + + PhaseConfig config; + config.memory.allocation = Strata::Placement::PreferExternal; + config.memory.taskStack = Strata::Placement::PreferExternal; + + PhaseResult result = phase.init(config); + if (!result) { + Serial.println(result.message); + return; + } + + phase.add("app", []() { + Serial.println("app init"); + }); + + PhaseDiag diag = phase.getDiagnostics(); + Serial.printf( + "allocation=%s stack-requested=%s stack-region=%s\n", + Strata::toString(diag.allocationPlacement), + Strata::toString(diag.requestedStackPlacement), + Strata::toString(diag.stackRegion) + ); + + phase.start(); +} + +void loop() { + delay(1000); +} From 71e06c17bf5db8b95e11001927e7619f1a0c7722 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:35:23 +0200 Subject: [PATCH 20/25] Document Phase Strata memory configuration --- docs/configuration.md | 56 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index fee2281..1b62e21 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,18 +1,52 @@ # Configuration -`PhaseConfig` controls the task, lifecycle timeouts, polling, and registration limits. +`PhaseConfig` controls memory placement, the Phase task, lifecycle timeouts, polling, and registration limits. ```cpp PhaseConfig config; +config.memory.allocation = Strata::Placement::Default; +config.memory.taskStack = Strata::Placement::PreferExternal; config.stackSizeBytes = 4096; config.priority = 1; config.coreId = tskNO_AFFINITY; config.taskName = "phase-task"; -config.stackType = PhaseStackType::Auto; PhaseResult result = phase.init(config); ``` +## Memory policy + +Phase v0.2.0 uses `Strata::MemoryPolicy`, the same memory-policy shape used across Strata-backed ZekStack libraries. + +| Field | Default | Purpose | +| --- | --- | --- | +| `memory.allocation` | `Default` | Placement for movable Phase-owned graph storage. | +| `memory.taskStack` | `PreferExternal` | Placement for the Phase task stack. | + +`memory.allocation` controls node records, node and dependency names, dependency indexes, init/start order storage, and graph validation storage. + +`memory.taskStack` controls the Phase task stack. The task control block and recursive mutex control storage remain internal through Strata. + +Available placements are: + +* `Strata::Placement::Default` - use the Strata backend default. +* `Strata::Placement::Internal` - require internal memory. +* `Strata::Placement::PreferExternal` - prefer external memory and fall back to internal memory. +* `Strata::Placement::RequireExternal` - require external memory and fail when it is unavailable. + +The default task placement preserves the old `PhaseStackType::Auto` behavior. + +```cpp +PhaseDiag diag = phase.getDiagnostics(); +Serial.printf( + "requested=%s actual=%s\n", + Strata::toString(diag.requestedStackPlacement), + Strata::toString(diag.stackRegion) +); +``` + +The requested placement and actual region are intentionally separate because `PreferExternal` may fall back to internal memory. + ## Task options | Field | Default | Purpose | @@ -21,9 +55,8 @@ PhaseResult result = phase.init(config); | `priority` | `1` | Phase task priority. | | `coreId` | `tskNO_AFFINITY` | Core affinity. | | `taskName` | `"phase-task"` | FreeRTOS task name. | -| `stackType` | `Auto` | Internal RAM or PSRAM stack preference. | -`PhaseStackType::Auto` prefers PSRAM task stacks when the ESP-IDF support is available and falls back to internal RAM. +Strata owns the Phase task stack and task control block. Phase no longer contains ESP-IDF heap-capability or task-allocation logic. ## Limits @@ -32,7 +65,7 @@ PhaseResult result = phase.init(config); | `maxNodes` | `32` | Maximum total steps and groups. | | `maxDependenciesPerNode` | `8` | Maximum dependencies for one node. | -These limits bound how many nodes and dependencies Phase accepts. Registration still uses dynamic allocation internally through `std::vector`, `std::string`, and `std::function`, so register all nodes during setup and avoid runtime registration. +These limits bound how many nodes and dependencies Phase accepts. Phase pre-reserves graph backing from these limits during initialization/registration so lifecycle execution can remain allocation-free. ## Timeouts @@ -47,7 +80,7 @@ These limits bound how many nodes and dependencies Phase accepts. Registration s Lifecycle callback timeouts are cooperative. Phase measures elapsed time after a callback returns. A callback that never returns cannot be interrupted by Phase. -Because callbacks cannot be interrupted, destroying a `Phase` object waits indefinitely for the Phase task to exit. If a lifecycle callback never returns, the destructor can block forever. +Because Strata owns the static task storage, final task cleanup must happen from another task context. `end()` therefore returns `Busy` when called from the Phase task. A `Phase` object must not be destroyed from one of its own callbacks. Calling `end()` performs final teardown for the current `Phase` instance. A successfully ended instance cannot be initialized again. @@ -72,3 +105,14 @@ phase.addGroup("internet") .condition(hasInternet, 30000) .conditionPollInterval(250); ``` + +## Migrating from v0.1.x + +| v0.1.x | v0.2.0 | +| --- | --- | +| `PhaseStackType::Auto` | `Strata::Placement::PreferExternal` | +| `PhaseStackType::Internal` | `Strata::Placement::Internal` | +| `PhaseStackType::Psram` | `Strata::Placement::RequireExternal` | +| `config.stackType` | `config.memory.taskStack` | +| `diag.requestedStackType` | `diag.requestedStackPlacement` | +| `diag.actualStackType` | `diag.stackRegion` | From 82d292369a8b77f8bc4924f8dd9d7e3262c3a68b Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:36:27 +0200 Subject: [PATCH 21/25] Document Strata-backed Phase API --- docs/api.md | 57 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/docs/api.md b/docs/api.md index 82a50aa..388319b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,10 +1,10 @@ # API Reference -This page summarizes the public API declared in `src/Phase.h`. +This page summarizes the public API declared in `src/Phase.h` for Phase v0.2.0. ## Results -Phase does not use exceptions. Operations return `PhaseResult`. +Phase does not intentionally throw exceptions. Operations return `PhaseResult`. | Field | Meaning | | --- | --- | @@ -14,19 +14,33 @@ Phase does not use exceptions. Operations return `PhaseResult`. `PhaseStatus` values include `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `TooManyNodes`, `TooManyDependencies`, `DuplicateName`, `MissingDependency`, `CircularDependency`, `InvalidCallback`, `RegistrationClosed`, `Busy`, `Timeout`, `CallbackFailed`, `DependencyFailed`, and `InternalError`. +## PhaseConfig + +Phase v0.2.0 uses Strata for memory placement and owned FreeRTOS storage. + +```cpp +PhaseConfig config; +config.memory.allocation = Strata::Placement::Default; +config.memory.taskStack = Strata::Placement::PreferExternal; +``` + +`memory.allocation` controls movable Phase-owned graph storage. `memory.taskStack` controls the Phase task stack. See `configuration.md` for the complete placement contract. + +The old `PhaseStackType` API was removed in v0.2.0. + ## Phase | Method | Purpose | | --- | --- | -| `init(config)` | Create the Phase task and prepare registration. | +| `init(config)` | Validate configuration, create Strata-backed storage/task ownership, and prepare registration. | | `start()` | Request async boot. | | `stop()` | Request reverse stop/deinit. | -| `end(timeoutMs)` | Stop the task and permanently end this Phase instance. | +| `end(timeoutMs)` | Stop the task, externally reclaim Strata task storage, and permanently end this Phase instance. | | `pause(reason)` | Request cooperative pause. | | `resume()` | Continue lifecycle progression. | | `isPaused()` | Return current pause flag. | | `state()` | Return current lifecycle state. | -| `getDiagnostics()` | Return aggregate diagnostics. | +| `getDiagnostics()` | Return aggregate and memory-placement diagnostics. | | `onChange(callback)` | Register progress callback. | | `onReady(callback)` | Register ready callback. | | `onFailed(callback)` | Register terminal failure callback. | @@ -67,15 +81,13 @@ Phase does not use exceptions. Operations return `PhaseResult`. Lifecycle callbacks may return: -```txt +```text void bool PhaseResult ``` -`false` maps to `PhaseStatus::CallbackFailed`. - -Group conditions return `bool`. +`false` maps to `PhaseStatus::CallbackFailed`. Group conditions return `bool`. ## Change events @@ -98,12 +110,33 @@ Callbacks are invoked synchronously from the Phase task. Keep them short. ## Diagnostics -`PhaseDiag` reports node counts, boot count, rollback count, change count, state, stack memory preference, and the task stack high-water mark after the task ends. +`PhaseDiag` reports lifecycle counts plus Strata placement information: -`startedCount` counts only steps whose `start()` callback completed. Steps without a `start()` callback can become ready without being counted as started. +| Field | Meaning | +| --- | --- | +| `nodeCount` | Number of registered nodes. | +| `initializedCount` | Currently initialized steps. | +| `startedCount` | Steps whose start callback completed. | +| `readyCount` | Ready nodes. | +| `failedCount` | Failed nodes. | +| `skippedCount` | Skipped optional nodes. | +| `bootCount` | Number of boot attempts. | +| `rollbackCount` | Number of rollbacks. | +| `changeCount` | Number of emitted change events. | +| `stackHighWaterMarkBytes` | Phase task stack high-water mark captured during final teardown. | +| `state` | Current lifecycle state. | +| `requestedStackPlacement` | Requested `Strata::Placement` for the Phase task stack. | +| `stackRegion` | Observed `Strata::Region` for the task stack. | +| `allocationPlacement` | Requested placement for Phase-owned graph storage. | + +Requested placement and observed region are separate because `PreferExternal` may fall back to internal memory. ## Stop and end behavior `stop()` is a no-op when Phase is idle, stopped, or failed. It requests shutdown when Phase is booting, starting, ready, or actively paused. If `start()` was queued but the Phase task has not started booting yet, `stop()` cancels that pending start. -`end()` is final teardown for a Phase instance. After `end()` succeeds, create a new `Phase` object instead of calling `init()` again on the same object. +`end()` must run outside the Phase task. The Phase task performs lifecycle shutdown, records diagnostics, publishes an external-deletion handoff, and suspends. The caller then releases the Strata-owned static task stack and task control block. + +`end()` returns `Busy` when called from a Phase callback. A `Phase` object must not be destroyed from one of its own Phase callbacks. + +After `end()` succeeds, create a new `Phase` object instead of calling `init()` again on the same object. From 577dd5be54ad50a417d58dbae0134a744bffd468 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:36:59 +0200 Subject: [PATCH 22/25] Update Phase getting started for Strata --- docs/getting-started.md | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 99c8f39..b923c90 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,10 +1,17 @@ # Getting Started -Phase runs application lifecycle work from its own FreeRTOS task. +Phase runs application lifecycle work from its own Strata-backed FreeRTOS task. Phase `v0.2.0` requires Strata `v0.1.1`. + +PlatformIO resolves Strata from Phase's `library.json`. Arduino IDE users should install both repositories: + +```text +Arduino/libraries/Strata +Arduino/libraries/Phase +``` The usual flow is: -```txt +```text phase.init() phase.add(...) phase.addGroup(...) @@ -47,11 +54,22 @@ void loop() { } ``` +The default v0.2.0 memory policy keeps ordinary graph allocation on the Strata backend default and preserves the old automatic stack behavior by preferring external memory with internal fallback: + +```cpp +PhaseConfig config; +config.memory.allocation = Strata::Placement::Default; +config.memory.taskStack = Strata::Placement::PreferExternal; +phase.init(config); +``` + +For explicit placement, set either field to `Internal`, `PreferExternal`, or `RequireExternal`. See `configuration.md` for the full contract. + ## Steps A step is a real module. It can have: -```txt +```text init deinit start @@ -78,6 +96,8 @@ phase.add("database", initDatabase, deinitDatabase) During the init wave, step nodes initialize after their step dependencies have initialized. Groups are not evaluated in the init wave. During the start/readiness wave, steps start and groups are evaluated only after their dependencies are ready. +Phase-owned node records, names, dependency names/indexes, and ordering/validation backing use `config.memory.allocation`. + ## Groups A group is a virtual readiness gate. It does not own resources. @@ -112,7 +132,7 @@ Pause is cooperative. It takes effect before the next lifecycle action or the ne `stop()` requests reverse shutdown: -```txt +```text started steps stop in reverse start order initialized steps deinitialize in reverse init order groups reset internally @@ -124,4 +144,6 @@ phase.stop(); Calling `stop()` while Phase is idle, stopped, or failed is a success no-op. If `pause()` was called before `start()`, a pre-start `stop()` does not clear that pause; the later `start()` still waits for `resume()`. -`end()` is final teardown. After `end()` succeeds, create a new `Phase` object instead of reusing the same instance. +`end()` is final teardown. The Phase task publishes an external-deletion handoff and suspends; the calling task then releases the Strata-owned task stack and control block. `end()` therefore cannot be called from a Phase callback and returns `Busy` in that context. + +After `end()` succeeds, create a new `Phase` object instead of reusing the same instance. From 83b7ad7e6b85e437bdda2bb2f1707f860d679e27 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:37:08 +0200 Subject: [PATCH 23/25] Document Phase memory policy example --- docs/examples.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/examples.md b/docs/examples.md index 6c1b901..5e5bae5 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -11,11 +11,14 @@ The repository includes Arduino examples under `examples/`. | `OptionalNodes` | Optional failure and skipped optional dependents. | | `BindableCallbacks` | Calling private methods through lambdas. | | `ManualShutdown` | Requesting shutdown after ready. | +| `MemoryPolicy` | Configuring Strata graph/task placement and inspecting actual stack region. | ## Recommended order Start with `Basic`, then read `Dependencies` and `Groups`. +Use `MemoryPolicy` when integrating Phase into a larger Strata-backed application. It shows both `memory.allocation` and `memory.taskStack`, plus the requested-placement/actual-region diagnostic split. + `PauseResume` and `OptionalNodes` cover behavior that matters in larger products. `BindableCallbacks` shows how to keep module internals private while still registering lifecycle callbacks. From 90740c5e1ba87193a7ffccda0aa738703651db10 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:37:34 +0200 Subject: [PATCH 24/25] Document Phase Strata placement troubleshooting --- docs/troubleshooting.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 81ac739..907a644 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -10,13 +10,43 @@ PhaseResult result = phase.init(); If `pause()` was called before `start()`, boot waits until `resume()`. +## `init()` fails with `TaskCreateFailed` + +Check the requested Strata task placement and available memory. + +```cpp +PhaseConfig config; +config.memory.taskStack = Strata::Placement::PreferExternal; +``` + +`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` does not fall back; task creation fails instead. + +If the device does not have usable external RAM, use `Internal` or `PreferExternal`. + +## The task did not land where expected + +Requested placement and actual region are separate diagnostics: + +```cpp +PhaseDiag diag = phase.getDiagnostics(); +Serial.printf( + "requested=%s region=%s\n", + Strata::toString(diag.requestedStackPlacement), + Strata::toString(diag.stackRegion) +); +``` + +Seeing `Internal` after requesting `PreferExternal` is a valid fallback. Use `RequireExternal` only when failure is preferable to consuming internal RAM. + +`allocationPlacement` reports the requested policy for Phase-owned graph storage; it is not a claim that every caller-owned lambda capture is placed there. + ## Registration fails after start Registration closes after `start()`. The expected order is: -```txt +```text init add steps add groups @@ -39,9 +69,13 @@ Lifecycle callbacks are cooperative. Phase can report that a returned callback e Use bounded waits inside callbacks and return a failed `PhaseResult` when the module cannot finish. -Phase destruction waits for the internal task to exit. If a callback never returns, destroying the `Phase` object can block forever. +Phase destruction waits for the internal task to reach its Strata teardown handoff. If a callback never returns, destroying the `Phase` object can block forever. + +`end()` must be called from another task context. It returns `Busy` from a Phase callback because the Strata-owned static task stack cannot safely free itself. + +A `Phase` object must not be destroyed from one of its own callbacks. -`end()` is also terminal. After it succeeds, create a new `Phase` object instead of calling `init()` again. +`end()` is terminal. After it succeeds, create a new `Phase` object instead of calling `init()` again. ## A group timeout expires while waiting for a condition From 9cfa40613bbd0b7ca50ebcf078b2664e45159fe6 Mon Sep 17 00:00:00 2001 From: DrRandom <37899881+zekageri@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:09:54 +0200 Subject: [PATCH 25/25] Fix PhasePlacedVector linkage in host builds --- src/internal/PhaseRuntimeBase.inc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/internal/PhaseRuntimeBase.inc b/src/internal/PhaseRuntimeBase.inc index d681f3e..b5d2caa 100644 --- a/src/internal/PhaseRuntimeBase.inc +++ b/src/internal/PhaseRuntimeBase.inc @@ -30,6 +30,7 @@ void copyText(char *destination, size_t destinationSize, const char *source) { bool isValidStackSize(size_t stackBytes) { return stackBytes >= kMinStackSizeBytes && (stackBytes % sizeof(StackType_t)) == 0; } +} // namespace template class PhasePlacedVector { @@ -67,6 +68,7 @@ class PhasePlacedVector { std::optional> _storage; }; +namespace { enum class DependencyState : uint8_t { Ready, Waiting,