diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7751a46..d18b194 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: @@ -25,11 +26,22 @@ jobs: - name: Audit production sources 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 '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 "Worker 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 "Worker must not depend on ESP-IDF allocation internals" + exit 1 + fi + build-examples: runs-on: ubuntu-latest needs: source-audit @@ -72,6 +84,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 @@ -152,12 +165,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/Worker" rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/" + 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: diff --git a/README.md b/README.md index 49d88fc..6d52b39 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Worker is a FreeRTOS task and cooperative job execution library for ESP32. -Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application. +Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage. [![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases) @@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati - **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task. - **Automatic cleanup** — callers never need to reap completed jobs. -- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`. +- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement. +- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums. +- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives. - **Safe recurring jobs** — `every()` applies the interval after each callback. -- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference. - **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`. -- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history. +- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions. -## Install +## Dependency + +Worker `v0.2.0` requires Strata `v0.1.1`. ### PlatformIO @@ -37,14 +40,19 @@ build_unflags = -std=gnu++11 ``` +Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency. + ### Arduino IDE -Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory. +Worker 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/Worker ``` +Use Strata `v0.1.1` or a compatible later release. + ## Quick start ```cpp @@ -59,7 +67,7 @@ void setup() { WorkerResult initResult = worker.init(); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } @@ -84,23 +92,62 @@ void loop() { } ``` -No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically. +No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically. + +## Memory policy + +Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape: + +```cpp +WorkerConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.taskStack = Strata::Placement::PreferExternal; + +Worker worker; +worker.init(config); +``` + +`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks. + +Worker's default policy preserves the old `WorkerStackType::Auto` behavior: + +```cpp +allocation = Strata::Placement::Default; +taskStack = Strata::Placement::PreferExternal; +``` + +`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory. + +A job can override only its own stack placement: + +```cpp +WorkerJobConfig job; +job.stackPlacement = Strata::Placement::Internal; +worker.once(job, [](WorkerJobContext &) {}); +``` + +`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default. + +The cleanup task can be overridden independently when needed: + +```cpp +config.cleanupTaskStackPlacement = Strata::Placement::Internal; +``` ## Cleanup model -Worker creates one long-lived internal cleanup task during `init()`. +Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`. When a job callback finishes, the job: 1. releases its stored callback; -2. queues its handle and immutable allocation type; -3. suspends itself. +2. records its final state and stack high-water mark; +3. queues its job record to the cleanup task; +4. reaches the external-deletion handoff and suspends. -The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns. +The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns. -This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself. - -`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup. +`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup. ## Important notes @@ -110,28 +157,45 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task - A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing. - The destructor waits without a timeout so tasks cannot outlive Worker internals. - `every(intervalMs, callback)` delays after each callback. -- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM. -- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes. +- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`. - `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity. - `clearFinished()` is retained only as a deprecated compatibility no-op. - Completion synchronization uses a small bounded token window and never retains callbacks or full completed records. -- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts. +- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly. +- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation. +- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary. + +## Diagnostics + +`WorkerJobDiag` separates requested policy from actual storage: + +```cpp +WorkerJobDiag diag; +if (worker.getJobDiagnostics(jobId, diag)) { + Serial.printf( + "requested=%s actual=%s\n", + Strata::toString(diag.requestedStackPlacement), + Strata::toString(diag.stackRegion)); +} +``` + +`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime. ## Examples | Example | Description | | --- | --- | | `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. | -| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. | +| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. | | `Events` | Event callback and error event handling. | | `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. | -| `Diagnostics` | Current job and cleanup-task diagnostics. | +| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. | | `BindableCallbacks` | `std::bind` with private class methods. | -| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. | +| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. | Start with: -```txt +```text examples/Basic ``` @@ -139,20 +203,27 @@ examples/Basic | Document | Description | | --- | --- | -| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. | -| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. | -| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. | +| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. | +| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. | +| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. | | [`docs/examples.md`](docs/examples.md) | Example descriptions. | -| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. | +| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. | ## API overview ```cpp +WorkerConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.taskStack = Strata::Placement::PreferExternal; + Worker worker; -worker.init(); +worker.init(config); worker.onEvent([](WorkerEvent event) {}); -WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {}); +WorkerJobConfig jobConfig; +jobConfig.stackPlacement = Strata::Placement::Internal; + +WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {}); WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {}); worker.sleep(loop.jobId, 5000); @@ -170,11 +241,11 @@ worker.getJobDiagnostics(loop.jobId, jobDiag); | Framework | Arduino ESP32 | | Platform | `espressif32` / PIOArduino | | Language | C++20 | -| Filesystem | none | -| PSRAM | Optional task stacks through ESP-IDF capability APIs | -| Dependencies | none | -| Exceptions | Not used | -| Status | Early-stage `0.1.0` | +| Memory layer | Strata `v0.1.1` | +| External memory | Optional through Strata placement policies | +| Dependencies | Strata `v0.1.1` | +| Exceptions | Not intentionally used by Worker APIs | +| Status | `v0.2.0` API | ## License @@ -182,4 +253,4 @@ MIT — see [`LICENSE.md`](LICENSE.md). ## ZekStack -Part of the ZekStack ESP32 library stack. +Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries. diff --git a/docs/api.md b/docs/api.md index 2ca6fb9..1819226 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ # API Reference -This page summarizes the public API declared in `src/Worker.h`. +This page summarizes the public API declared in `src/Worker.h` for Worker `v0.2.0`. ## Results @@ -10,44 +10,64 @@ Worker does not intentionally throw exceptions. Operations report normal failure | --- | --- | | `result` | `true` on success, `false` on failure. | | `status` | Machine-readable `WorkerStatus`. | -| `message` | Human-readable status. | +| `message` | Non-owning static human-readable status string. | | `jobId` | Returned by `WorkerJobResult` after a job was created. | +Use `result.message` directly. `message` is no longer a `std::string` in `v0.2.0`. + `WorkerStatus` values are `Ok`, `NotInitialized`, `AlreadyInitialized`, `InvalidArgument`, `OutOfMemory`, `TaskCreateFailed`, `JobNotFound`, `Busy`, `Timeout`, and `InternalError`. ## Worker | Method | Purpose | | --- | --- | -| `init(config)` | Initialize Worker and its cleanup task. | +| `init(config)` | Initialize Worker, Strata-backed storage, cleanup queue, and cleanup task. | | `onEvent(callback)` | Register a synchronous event callback. | | `once(callback)` | Start an automatically cleaned one-off task. | | `once(config, callback)` | Start a configured automatically cleaned one-off task. | | `every(intervalMs, callback)` | Start a recurring task with internal delay. | | `every(intervalMs, config, callback)` | Start a configured recurring task. | | `stop(jobId)` | Request cooperative stop. Cleanup continues automatically. | -| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the task stack and TCB are released. | +| `stopAndWait(jobId, timeoutMs)` | Request stop and wait until the Strata task owner releases its stack and TCB. | | `sleep(jobId, durationMs)` | Request that a job sleeps. | | `waitFor(jobId)` | Optionally wait until physical task cleanup completes. | | `waitFor(jobId, timeoutMs)` | Wait with timeout until physical task cleanup completes. | | `clearFinished()` | Deprecated compatibility no-op. Worker cleans jobs automatically. | -| `getDiagnostics()` | Return current runtime and cleanup-task state. | +| `getDiagnostics()` | Return current runtime, cleanup infrastructure, and memory-region state. | | `getJobDiagnostics(jobId, out)` | Fill diagnostics for a currently active job. | | `end(timeoutMs)` | Stop jobs, drain cleanup, and stop Worker infrastructure. | `once()` and `every()` are safe for fire-and-forget use. A caller never needs `waitFor()` or `clearFinished()` to release Worker-owned resources. +## Memory configuration + +`WorkerConfig` embeds `Strata::MemoryPolicy`: + +```cpp +WorkerConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.taskStack = Strata::Placement::PreferExternal; +``` + +`WorkerJobConfig::stackPlacement` is `std::optional`. `std::nullopt` inherits the Worker task-stack policy. An explicit `Placement::Default` asks Strata for backend-default placement and is not an inheritance sentinel. + +The cleanup task can use `WorkerConfig::cleanupTaskStackPlacement` as an optional override; otherwise it also inherits `memory.taskStack`. + ## Cleanup lifecycle -Worker owns every task it creates. A completed job follows this lifecycle: +Worker owns every job task through `Strata::FreeRTOS::Task`. A completed job follows this lifecycle: 1. The callback returns and its stored `std::function` is released. -2. The job queues its task handle to the Worker cleanup task. -3. The job task suspends itself. -4. The cleanup task deletes it externally with `vTaskDelete()` or `vTaskDeleteWithCaps()` as appropriate. -5. Worker records a small bounded completion token and removes the full active job record. +2. Worker records final state and stack high-water mark. +3. The job queues its record through `Strata::FreeRTOS::Queue`. +4. The job reaches the external-deletion handoff and suspends. +5. The cleanup task externally resets the job's `Strata::FreeRTOS::Task`. +6. Strata deletes the FreeRTOS task and releases the placed stack and internal task control block. +7. Worker records a small bounded completion token and removes the full active job record. + +`waitFor()` and `stopAndWait()` succeed only after the Strata task reset has completed. They are synchronization APIs, not cleanup APIs. -`waitFor()` and `stopAndWait()` succeed only after step 4. They are synchronization APIs, not cleanup APIs. +The cleanup task itself follows the same ownership rule: `end()` waits for its handoff, externally resets its Strata task owner, and then resets the Strata cleanup queue. ## Events @@ -55,10 +75,12 @@ Register an event callback with `onEvent()`. ```cpp worker.onEvent([](WorkerEvent event) { - Serial.printf("Worker event occurred: %s", event.message); + Serial.printf("Worker event occurred: %s", event.message); }); ``` +Worker stores the callback behind Strata-backed shared ownership so event dispatch can snapshot ownership without copying the `std::function` target while holding the Worker mutex. + Completion events are emitted after physical task deletion completes. ## Job context @@ -83,7 +105,19 @@ The context is valid only during callback execution. - active, running, sleeping, stopping, and cleanup-queued job counts; - cleanup-task running state; -- cleanup queue depth and high-water mark. +- cleanup queue depth and high-water mark; +- `cleanupTaskStackPlacement` and observed `cleanupTaskStackRegion`; +- `cleanupQueueStoragePlacement` and observed `cleanupQueueStorageRegion`. + +`WorkerJobDiag` reports: + +- current job identity/state/name; +- stack size, priority, and affinity; +- `requestedStackPlacement` as the resolved Strata placement intent; +- `stackRegion` as the observed Strata memory region; +- run/timing counters and stack high-water mark. + +Requested placement and observed region are intentionally different. A `PreferExternal` stack may legally report `Region::Internal` after fallback. Worker does not retain lifetime job counters. `WorkerJobDiag` is available only while a job is active. After automatic cleanup removes the active record, `getJobDiagnostics()` returns `JobNotFound`. diff --git a/docs/configuration.md b/docs/configuration.md index 9bf120b..feea145 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,32 +1,67 @@ # Configuration -`WorkerConfig` controls job defaults and Worker-owned cleanup infrastructure. +`WorkerConfig` controls job defaults, Worker-owned cleanup infrastructure, and the shared ZekStack `Strata::MemoryPolicy`. + +## Memory policy + +```cpp +WorkerConfig config; +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.taskStack = Strata::Placement::PreferExternal; +``` + +Worker's defaults are: + +```cpp +allocation = Strata::Placement::Default; +taskStack = Strata::Placement::PreferExternal; +``` + +This preserves the pre-`v0.2.0` `WorkerStackType::Auto` behavior for task stacks while adopting the common Strata vocabulary. + +`memory.allocation` applies to ordinary movable Worker-owned storage: job records, registry/completion container backing, and cleanup-queue item storage. FreeRTOS control blocks and mutex metadata remain internal through Strata because safety requirements override caller preference. + +`memory.taskStack` is the default placement inherited by job stacks and the cleanup-task stack. `PreferExternal` may fall back to internal memory; `RequireExternal` fails if external memory cannot satisfy the request. + +## WorkerConfig | Field | Default | Meaning | | --- | --- | --- | +| `memory.allocation` | `Strata::Placement::Default` | Default placement for movable Worker-owned allocations. | +| `memory.taskStack` | `Strata::Placement::PreferExternal` | Default placement for Worker-owned task stacks. | | `defaultStackSize` | `4096` | Default job task stack size in bytes. | | `defaultPriority` | `1` | Default job task priority. | -| `defaultCoreId` | `tskNO_AFFINITY` | Default job core affinity. | -| `defaultStackType` | `WorkerStackType::Auto` | Default stack memory preference. | +| `defaultCoreId` | `tskNO_AFFINITY` | Default job core affinity for overloads that use Worker defaults. | | `maxConcurrentJobs` | `8` | Maximum active jobs and cleanup queue capacity. | -| `cleanupTaskStackSize` | `3072` | Internal-RAM cleanup task stack size in bytes. | +| `cleanupTaskStackSize` | `3072` | Cleanup task stack size in bytes. | | `cleanupTaskPriority` | `1` | Cleanup task priority. | | `cleanupTaskCoreId` | `tskNO_AFFINITY` | Cleanup task core affinity. | +| `cleanupTaskStackPlacement` | `std::nullopt` | Optional cleanup-stack override; `nullopt` inherits `memory.taskStack`. | Worker rejects new jobs with `WorkerStatus::Busy` when `maxConcurrentJobs` is reached. This bound guarantees one cleanup queue slot for every task Worker allows to exist. -`WorkerJobConfig` controls a single job. +## WorkerJobConfig | Field | Default | Meaning | | --- | --- | --- | | `stackSize` | `0` | `0` uses the Worker default. | | `priority` | `0` | `0` uses the Worker default. | | `coreId` | `tskNO_AFFINITY` | FreeRTOS core affinity. | -| `stackType` | `WorkerStackType::Auto` | `Auto`, `Internal`, or `Psram`. | +| `stackPlacement` | `std::nullopt` | Optional Strata placement override. `nullopt` inherits `WorkerConfig::memory.taskStack`. | | `name` | `nullptr` | Optional task name copied into fixed Worker storage. | -Stack sizes are byte counts on ESP32. Worker rejects stack sizes below 1024 bytes or sizes that are not aligned to `sizeof(StackType_t)`. +`Placement::Default` never means inherit. If `stackPlacement` contains `Strata::Placement::Default`, Worker explicitly asks Strata for backend-default placement. Inheritance is represented only by `std::nullopt`. + +Stack sizes remain byte counts on ESP32. Worker preserves the existing contract and rejects stack sizes below 1024 bytes or values not aligned to `sizeof(StackType_t)`. + +## Migration from v0.1.0 -`WorkerStackType::Auto` uses PSRAM stacks when ESP-IDF task-capability support and PSRAM are available. It falls back to internal RAM otherwise. +| v0.1.0 | v0.2.0 | +| --- | --- | +| `WorkerStackType::Auto` | `Strata::Placement::PreferExternal` | +| `WorkerStackType::Internal` | `Strata::Placement::Internal` | +| `WorkerStackType::Psram` | `Strata::Placement::RequireExternal` | +| `WorkerConfig::defaultStackType` | `WorkerConfig::memory.taskStack` | +| `WorkerJobConfig::stackType` | `WorkerJobConfig::stackPlacement` | -`WorkerStackType::Psram` requires PSRAM task stack support. Job creation fails if it is unavailable. Worker always deletes a capability-created task externally with `vTaskDeleteWithCaps()`. +`WorkerStackType` is intentionally removed rather than retained as a compatibility alias so Worker establishes the same configuration vocabulary later ZekStack migrations will use. diff --git a/docs/examples.md b/docs/examples.md index 5205762..1f311ba 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,29 +1,15 @@ # Examples -## Basic - -Shows initialization, a fire-and-forget one-off job, a recurring job, optional waiting, and cooperative stop. - -## JobConfig - -Shows stack size, priority, core affinity, internal stack selection, and PSRAM stack requests. - -## Events - -Shows synchronous Worker event reporting. Job completion events are emitted after physical task cleanup. - -## SleepAndWait - -Shows `ctx.sleep()`, external `worker.sleep(jobId, durationMs)`, optional `waitFor()`, and timeout behavior. - -## Diagnostics - -Shows current active-job counts, cleanup queue state, cleanup-task health, and active per-job diagnostics. - -## BindableCallbacks - -Shows `std::bind` with private class methods so application classes can own job behavior. - -## TaskCleanupSentinel - -Runs fire-and-forget one-shot jobs and verifies callback capture destruction, active record cleanup, internal heap stability, PSRAM stability, and task-count recovery after allocator warm-up. +Worker examples are compiled in CI with both PIOArduino and Arduino CLI across ESP32, ESP32-S3, ESP32-C3, and ESP32-P4. CI installs the pinned Strata `v0.1.1` dependency before building them. + +| Example | What it demonstrates | +| --- | --- | +| `Basic` | Initialization, one-off/recurring jobs, wait, and cooperative stop. | +| `JobConfig` | Worker `MemoryPolicy`, inherited task placement, and per-job `Internal` / `RequireExternal` overrides. | +| `Events` | Synchronous Worker events and normal error reporting. | +| `SleepAndWait` | Cooperative sleep, external sleep requests, wait, and timeout behavior. | +| `Diagnostics` | Requested Strata placement versus observed stack/cleanup memory regions. | +| `BindableCallbacks` | `std::bind` and private method callbacks. | +| `TaskCleanupSentinel` | Repeated fire-and-forget cleanup while Worker general allocations and task stacks use `PreferExternal`. | + +Start with `Basic`, then use `JobConfig` and `Diagnostics` when integrating Worker memory policy into an application. diff --git a/docs/getting-started.md b/docs/getting-started.md index adfbeac..c8d11f0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,8 @@ -# Getting Started +# Getting started -Include `Worker.h`, create a `Worker` instance, and call `init()` before creating jobs. +Worker `v0.2.0` requires Strata `v0.1.1` and C++20. + +PlatformIO resolves the pinned Strata dependency from Worker's `library.json`. For Arduino IDE/manual installs, place both repositories in the Arduino libraries directory. ```cpp #include @@ -9,37 +11,43 @@ Include `Worker.h`, create a `Worker` instance, and call `init()` before creatin Worker worker; void setup() { - Serial.begin(115200); + Serial.begin(115200); + + WorkerConfig config; + config.memory.allocation = Strata::Placement::Default; + config.memory.taskStack = Strata::Placement::PreferExternal; + + WorkerResult initResult = worker.init(config); + if (!initResult) { + Serial.println(initResult.message); + return; + } + + worker.once([](WorkerJobContext &ctx) { + Serial.printf("job=%u\n", static_cast(ctx.id())); + }); +} - WorkerResult result = worker.init(); - if (!result) { - Serial.println(result.message.c_str()); - return; - } +void loop() { + delay(1000); } ``` -Create a fire-and-forget one-off job with `once()`. +The default Worker task-stack policy is `PreferExternal`, which preserves the old automatic external-stack preference while allowing internal fallback. General allocations use `Placement::Default` unless configured otherwise. + +For a task that must stay internal: ```cpp -worker.once([](WorkerJobContext &ctx) { - Serial.printf("job id=%u\n", static_cast(ctx.id())); -}); +WorkerJobConfig job; +job.stackPlacement = Strata::Placement::Internal; +worker.once(job, [](WorkerJobContext &) {}); ``` -No cleanup call is required. Worker releases the callback, task stack, TCB, and job record automatically. - -Create a recurring job with `every()`. +For an application that wants all movable Worker storage and normal Worker task stacks to prefer external memory: ```cpp -worker.every(1000, [](WorkerJobContext &ctx) { - Serial.println("runs every second"); - if (ctx.runCount() >= 5) { - ctx.stop(); - } -}); +config.memory.allocation = Strata::Placement::PreferExternal; +config.memory.taskStack = Strata::Placement::PreferExternal; ``` -`every()` delays internally after the callback returns. Do not add a delay only to protect the system from spinning. - -Use `waitFor()` only when application logic needs synchronization with physical task cleanup. +Worker cleanup is automatic. `waitFor()` and `stopAndWait()` are synchronization tools only; callers never free task stacks, TCBs, queues, mutexes, or job records themselves. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a3d0f72..0a49ddb 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -8,16 +8,34 @@ Common causes: - Worker was not initialized. - The callback is empty. -- Stack size is below 1024 bytes or is not aligned. -- `WorkerStackType::Psram` was requested without PSRAM task stack support. +- Stack size is below 1024 bytes or is not aligned to `sizeof(StackType_t)`. +- `Strata::Placement::RequireExternal` was requested but external memory is unavailable or cannot satisfy the task stack. - `maxConcurrentJobs` was reached. Retry later or raise the configured bound. - Worker cleanup infrastructure could not be created during `init()`. +Use `PreferExternal` when internal fallback is acceptable. Use `RequireExternal` only when failure is preferable to consuming internal memory. + +## `init()` fails after changing memory policy + +`WorkerConfig::memory` and `cleanupTaskStackPlacement` are validated by Worker before infrastructure creation. + +If `memory.allocation = RequireExternal`, the cleanup queue item storage must be allocated externally. If `memory.taskStack = RequireExternal`, both normal job stacks and the cleanup-task stack inherit that strict requirement unless overridden. + +For flash/cache-sensitive application work, explicitly use `Strata::Placement::Internal` for the relevant job stack. + +## Requested placement differs from observed region + +This is expected for `PreferExternal`. Inspect `WorkerJobDiag::requestedStackPlacement` and `WorkerJobDiag::stackRegion` separately. + +`PreferExternal` may report `Region::Internal` after fallback. `RequireExternal` never falls back. + +`WorkerDiag` exposes the same requested/observed split for cleanup-task stack and cleanup-queue storage. + ## `stopAndWait()` or `end()` times out Cancellation is cooperative. Worker wakes sleeping jobs, but a callback must return before its task can be cleaned. Check `ctx.shouldStop()` inside long-running callbacks. -`waitFor()` and `stopAndWait()` wait for external task deletion, including stack and TCB release. They may take slightly longer than callback completion. +`waitFor()` and `stopAndWait()` wait for external Strata task reset, including stack and TCB release. They may take slightly longer than callback completion. ## Active jobs never return to zero @@ -35,4 +53,4 @@ The interval starts after the callback returns. A 200 ms callback with `every(10 ## Stack diagnostics are zero -Some FreeRTOS configurations do not expose stack high-water mark support. Worker reports `0` in that case. +Worker uses Strata's FreeRTOS task high-water-mark API. A zero value can mean the task has already left the active diagnostics window or no usable measurement was available at the observation point. diff --git a/examples/Basic/Basic.ino b/examples/Basic/Basic.ino index ac8ef88..1cf9f73 100644 --- a/examples/Basic/Basic.ino +++ b/examples/Basic/Basic.ino @@ -9,7 +9,7 @@ void setup() { WorkerResult initResult = worker.init(); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } diff --git a/examples/BindableCallbacks/BindableCallbacks.ino b/examples/BindableCallbacks/BindableCallbacks.ino index 5c81c81..3ac9ff4 100644 --- a/examples/BindableCallbacks/BindableCallbacks.ino +++ b/examples/BindableCallbacks/BindableCallbacks.ino @@ -33,7 +33,7 @@ void setup() { WorkerResult initResult = worker.init(); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } diff --git a/examples/Diagnostics/Diagnostics.ino b/examples/Diagnostics/Diagnostics.ino index 4942cc9..2dc4162 100644 --- a/examples/Diagnostics/Diagnostics.ino +++ b/examples/Diagnostics/Diagnostics.ino @@ -7,29 +7,33 @@ WorkerJobId jobId = 0; void printDiagnostics() { WorkerDiag diag = worker.getDiagnostics(); Serial.printf( - "active=%u running=%u sleeping=%u stopping=%u cleanup=%u queue=%u cleanupTask=%s\n", + "active=%u running=%u sleeping=%u stopping=%u cleanup=%u queue=%u cleanupTask=%s queueRegion=%s cleanupStackRegion=%s\n", static_cast(diag.activeJobCount), static_cast(diag.runningJobCount), static_cast(diag.sleepingJobCount), static_cast(diag.stoppingJobCount), static_cast(diag.cleanupQueuedCount), static_cast(diag.cleanupQueueDepth), - diag.cleanupTaskRunning ? "running" : "stopped" + diag.cleanupTaskRunning ? "running" : "stopped", + Strata::toString(diag.cleanupQueueStorageRegion), + Strata::toString(diag.cleanupTaskStackRegion) ); WorkerJobDiag jobDiag; WorkerResult result = worker.getJobDiagnostics(jobId, jobDiag); if (result) { Serial.printf( - "job=%u state=%s name=%s runs=%u stack=%u\n", + "job=%u state=%s name=%s runs=%u stack=%u requested=%s region=%s\n", static_cast(jobDiag.jobId), worker.jobStateToString(jobDiag.state), jobDiag.name, static_cast(jobDiag.runCount), - static_cast(jobDiag.stackSize) + static_cast(jobDiag.stackSize), + Strata::toString(jobDiag.requestedStackPlacement), + Strata::toString(jobDiag.stackRegion) ); } else { - Serial.printf("job diagnostics unavailable: %s\n", result.message.c_str()); + Serial.printf("job diagnostics unavailable: %s\n", result.message); } } @@ -38,7 +42,7 @@ void setup() { WorkerResult initResult = worker.init(); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } diff --git a/examples/Events/Events.ino b/examples/Events/Events.ino index 84990e1..50439ba 100644 --- a/examples/Events/Events.ino +++ b/examples/Events/Events.ino @@ -15,7 +15,7 @@ void setup() { WorkerResult initResult = worker.init(); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } diff --git a/examples/JobConfig/JobConfig.ino b/examples/JobConfig/JobConfig.ino index 022a6d3..e593fe8 100644 --- a/examples/JobConfig/JobConfig.ino +++ b/examples/JobConfig/JobConfig.ino @@ -7,14 +7,15 @@ void setup() { Serial.begin(115200); WorkerConfig config; + config.memory.allocation = Strata::Placement::Default; + config.memory.taskStack = Strata::Placement::PreferExternal; config.defaultStackSize = 4096; config.defaultPriority = 1; config.defaultCoreId = tskNO_AFFINITY; - config.defaultStackType = WorkerStackType::Auto; WorkerResult initResult = worker.init(config); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } @@ -23,23 +24,23 @@ void setup() { importantJob.stackSize = 8192; importantJob.priority = 2; importantJob.coreId = tskNO_AFFINITY; - importantJob.stackType = WorkerStackType::Internal; + importantJob.stackPlacement = Strata::Placement::Internal; worker.once(importantJob, [](WorkerJobContext &ctx) { Serial.printf("configured job id=%u\n", static_cast(ctx.id())); }); - WorkerJobConfig psramJob; - psramJob.name = "psram"; - psramJob.stackSize = 8192; - psramJob.stackType = WorkerStackType::Psram; + WorkerJobConfig externalJob; + externalJob.name = "external"; + externalJob.stackSize = 8192; + externalJob.stackPlacement = Strata::Placement::RequireExternal; - WorkerJobResult psramResult = worker.once(psramJob, [](WorkerJobContext &ctx) { - Serial.printf("PSRAM stack job id=%u\n", static_cast(ctx.id())); + WorkerJobResult externalResult = worker.once(externalJob, [](WorkerJobContext &ctx) { + Serial.printf("external stack job id=%u\n", static_cast(ctx.id())); }); - if (!psramResult) { - Serial.println(psramResult.message.c_str()); + if (!externalResult) { + Serial.println(externalResult.message); } } diff --git a/examples/SleepAndWait/SleepAndWait.ino b/examples/SleepAndWait/SleepAndWait.ino index 7ef2b5a..45643b5 100644 --- a/examples/SleepAndWait/SleepAndWait.ino +++ b/examples/SleepAndWait/SleepAndWait.ino @@ -8,7 +8,7 @@ void setup() { WorkerResult initResult = worker.init(); if (!initResult) { - Serial.println(initResult.message.c_str()); + Serial.println(initResult.message); return; } @@ -25,7 +25,7 @@ void setup() { if (sleeper) { worker.sleep(sleeper.jobId, 1500); WorkerResult waitResult = worker.waitFor(sleeper.jobId, 10000); - Serial.println(waitResult.message.c_str()); + Serial.println(waitResult.message); } WorkerJobResult blocking = worker.every(1000, [](WorkerJobContext &) { @@ -34,7 +34,7 @@ void setup() { if (blocking) { WorkerResult stopResult = worker.stopAndWait(blocking.jobId, 100); - Serial.println(stopResult.message.c_str()); + Serial.println(stopResult.message); } } diff --git a/examples/TaskCleanupSentinel/TaskCleanupSentinel.ino b/examples/TaskCleanupSentinel/TaskCleanupSentinel.ino index 4e26200..1160da1 100644 --- a/examples/TaskCleanupSentinel/TaskCleanupSentinel.ino +++ b/examples/TaskCleanupSentinel/TaskCleanupSentinel.ino @@ -63,7 +63,10 @@ void runFireAndForgetBatch(size_t jobCount, std::atomic &destroyed) { void setup() { Serial.begin(115200); - WorkerResult initResult = worker.init(); + WorkerConfig config; + config.memory.allocation = Strata::Placement::PreferExternal; + config.memory.taskStack = Strata::Placement::PreferExternal; + WorkerResult initResult = worker.init(config); assert(initResult); std::atomic warmupDestroyed{0}; diff --git a/library.json b/library.json index ec0d93a..843dc13 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Worker", - "version": "0.1.0", + "version": "0.2.0", "description": "FreeRTOS task and cooperative job execution library for ESP32.", "keywords": [ "esp32", @@ -22,6 +22,9 @@ "license": "MIT", "frameworks": "arduino", "platforms": "espressif32", + "dependencies": { + "Strata": "https://github.com/ZekStack/strata.git#v0.1.1" + }, "build": { "srcDir": "src", "includeDir": "src", diff --git a/library.properties b/library.properties index b4c7fdc..d99f417 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=Worker -version=0.1.0 +version=0.2.0 author=zekageri maintainer=zekageri sentence=FreeRTOS task and cooperative job execution library for ESP32. -paragraph=Provides task-per-job execution, recurring jobs with safe internal delays, cooperative stop/sleep controls, events, and diagnostics. +paragraph=Provides Strata-backed task-per-job execution, recurring jobs, cooperative stop/sleep controls, events, diagnostics, and automatic cleanup. category=Timing url=https://github.com/ZekStack/worker repository=https://github.com/ZekStack/worker.git diff --git a/src/Worker.cpp b/src/Worker.cpp index bfd6c17..fa2a49c 100644 --- a/src/Worker.cpp +++ b/src/Worker.cpp @@ -1,25 +1,22 @@ #include "Worker.h" -#include "internal/WorkerMutex.h" -#include "internal/WorkerTaskSupport.h" +#include +#include +#include #include #include #include #include -#include +#include #include -#include - -extern "C" { -#include -} namespace { constexpr WorkerJobId kInvalidJobId = 0; constexpr uint32_t kWaitPollMs = 10; constexpr size_t kMaxTaskNameLength = 32; constexpr size_t kCompletionCapacity = 16; +constexpr size_t kMinStackSizeBytes = 1024; constexpr const char *kCleanupTaskName = "worker-cleanup"; uint32_t nowMs() { @@ -43,6 +40,10 @@ TickType_t waitTicks(uint32_t durationMs) { return durationMs == UINT32_MAX ? portMAX_DELAY : pdMS_TO_TICKS(durationMs); } +bool isValidStackSize(size_t stackBytes) { + return stackBytes >= kMinStackSizeBytes && (stackBytes % sizeof(StackType_t)) == 0; +} + bool isExecutionCompleteState(WorkerJobState state) { switch (state) { case WorkerJobState::CallbackComplete: @@ -68,13 +69,39 @@ void copyTaskName(char *destination, size_t destinationSize, const char *source) std::strncpy(destination, source, destinationSize - 1); destination[destinationSize - 1] = '\0'; } -} // namespace -enum class WorkerTaskAllocation : uint8_t { - Internal, - WithCaps, +class WorkerLock { + public: + explicit WorkerLock(Strata::FreeRTOS::RecursiveMutex &mutex) + : _mutex(mutex), _locked(mutex.lock()) { + } + + ~WorkerLock() { + if (_locked) { + _mutex.unlock(); + } + } + + WorkerLock(const WorkerLock &) = delete; + WorkerLock &operator=(const WorkerLock &) = delete; + + explicit operator bool() const { + return _locked; + } + + private: + Strata::FreeRTOS::RecursiveMutex &_mutex; + bool _locked = false; }; +[[noreturn]] void suspendForever() { + vTaskSuspend(nullptr); + for (;;) { + vTaskDelay(portMAX_DELAY); + } +} +} // namespace + struct WorkerJobRecord { WorkerImpl *owner = nullptr; WorkerJobId id = kInvalidJobId; @@ -85,10 +112,8 @@ struct WorkerJobRecord { uint32_t stackSize = 0; UBaseType_t priority = 0; BaseType_t coreId = tskNO_AFFINITY; - WorkerStackType requestedStackType = WorkerStackType::Auto; - WorkerStackType actualStackType = WorkerStackType::Internal; - WorkerTaskAllocation allocation = WorkerTaskAllocation::Internal; - TaskHandle_t taskHandle = nullptr; + Strata::Placement requestedStackPlacement = Strata::Placement::Default; + Strata::FreeRTOS::Task task; std::atomic stopRequested{false}; std::atomic readyForDelete{false}; WorkerJobState state = WorkerJobState::Created; @@ -111,20 +136,26 @@ struct WorkerCompletion { struct WorkerCleanupRequest { WorkerJobId jobId = kInvalidJobId; - TaskHandle_t taskHandle = nullptr; WorkerJobRecord *record = nullptr; - bool withCaps = false; bool stopCleanupTask = false; }; +using WorkerJobPtr = Strata::UniquePtr; +using WorkerJobs = Strata::Vector; +using WorkerCompletions = Strata::Vector; +using WorkerCleanupQueue = Strata::FreeRTOS::Queue; + struct WorkerImpl { + WorkerImpl() noexcept : mutex(Strata::FreeRTOS::RecursiveMutex::create()) { + } + WorkerConfig config{}; - WorkerMutex mutex; - std::vector> jobs; - std::vector completions; - WorkerEventCallback onEvent; - QueueHandle_t cleanupQueue = nullptr; - TaskHandle_t cleanupTaskHandle = nullptr; + Strata::FreeRTOS::RecursiveMutex mutex; + std::optional jobs; + std::optional completions; + std::shared_ptr onEvent; + WorkerCleanupQueue cleanupQueue; + Strata::FreeRTOS::Task cleanupTask; bool cleanupTaskRunning = false; bool cleanupTaskStopRequested = false; std::atomic cleanupTaskReadyForDelete{false}; @@ -133,16 +164,25 @@ struct WorkerImpl { bool ending = false; WorkerJobId nextJobId = 1; + void initializeStorage(Strata::Placement placement, size_t maxConcurrentJobs) { + jobs.reset(); + completions.reset(); + jobs.emplace(Strata::Allocator{placement}); + completions.emplace(Strata::Allocator{placement}); + jobs->reserve(maxConcurrentJobs); + completions->reserve(kCompletionCapacity); + } + WorkerResult emitResult(WorkerResult result, WorkerJobId jobId = kInvalidJobId) { if (!result) { - emitEvent(WorkerEventType::Error, result.status, jobId, result.message.c_str()); + emitEvent(WorkerEventType::Error, result.status, jobId, result.message); } return result; } WorkerJobResult emitJobResult(WorkerJobResult result) { if (!result) { - emitEvent(WorkerEventType::Error, result.status, result.jobId, result.message.c_str()); + emitEvent(WorkerEventType::Error, result.status, result.jobId, result.message); } return result; } @@ -153,7 +193,7 @@ struct WorkerImpl { WorkerJobId jobId, const char *message ) { - WorkerEventCallback callback; + std::shared_ptr callback; { WorkerLock lock(mutex); if (!lock) { @@ -161,13 +201,16 @@ struct WorkerImpl { } callback = onEvent; } - if (callback) { - callback(WorkerEvent{type, status, jobId, message != nullptr ? message : "event"}); + if (callback && *callback) { + (*callback)(WorkerEvent{type, status, jobId, message != nullptr ? message : "event"}); } } WorkerJobRecord *findJob(WorkerJobId jobId) { - for (auto &job : jobs) { + if (!jobs) { + return nullptr; + } + for (auto &job : *jobs) { if (job && job->id == jobId) { return job.get(); } @@ -176,52 +219,62 @@ struct WorkerImpl { } bool hasCompletion(WorkerJobId jobId) const { + if (!completions) { + return false; + } return std::any_of( - completions.begin(), - completions.end(), + completions->begin(), + completions->end(), [jobId](const WorkerCompletion &completion) { return completion.jobId == jobId; } ); } bool consumeCompletion(WorkerJobId jobId, WorkerJobState &finalState) { + if (!completions) { + return false; + } auto it = std::find_if( - completions.begin(), - completions.end(), + completions->begin(), + completions->end(), [jobId](const WorkerCompletion &completion) { return completion.jobId == jobId; } ); - if (it == completions.end()) { + if (it == completions->end()) { return false; } finalState = it->finalState; - completions.erase(it); + completions->erase(it); return true; } void recordCompletion(WorkerJobId jobId, WorkerJobState finalState) { - completions.erase( + if (!completions) { + return; + } + completions->erase( std::remove_if( - completions.begin(), - completions.end(), + completions->begin(), + completions->end(), [jobId](const WorkerCompletion &completion) { return completion.jobId == jobId; } ), - completions.end() + completions->end() ); - if (completions.size() >= kCompletionCapacity) { - completions.erase(completions.begin()); + if (completions->size() >= kCompletionCapacity) { + completions->erase(completions->begin()); } - completions.push_back(WorkerCompletion{jobId, finalState}); + completions->push_back(WorkerCompletion{jobId, finalState}); } void eraseJob(WorkerJobId jobId) { - jobs.erase( + if (!jobs) { + return; + } + jobs->erase( std::remove_if( - jobs.begin(), - jobs.end(), - [jobId](const std::unique_ptr &job) { - return job && job->id == jobId; - } + jobs->begin(), + jobs->end(), + [jobId](const WorkerJobPtr &job) { return job && job->id == jobId; } ), - jobs.end() + jobs->end() ); } @@ -282,7 +335,6 @@ struct WorkerImpl { jobConfig.stackSize = config.defaultStackSize; jobConfig.priority = config.defaultPriority; jobConfig.coreId = config.defaultCoreId; - jobConfig.stackType = config.defaultStackType; return jobConfig; } @@ -297,6 +349,14 @@ struct WorkerImpl { return resolved; } + Strata::Placement resolveJobStackPlacement(const WorkerJobConfig &jobConfig) const { + return jobConfig.stackPlacement.value_or(config.memory.taskStack); + } + + Strata::Placement resolveCleanupStackPlacement(const WorkerConfig &incomingConfig) const { + return incomingConfig.cleanupTaskStackPlacement.value_or(incomingConfig.memory.taskStack); + } + void markRunStart(WorkerJobRecord *job) { WorkerLock lock(mutex); if (!lock || job == nullptr) { @@ -387,24 +447,19 @@ struct WorkerImpl { return finalState; } - void prepareCleanup( - WorkerJobRecord *job, - WorkerJobState finalState, - TaskHandle_t taskHandle - ) { + void prepareCleanup(WorkerJobRecord *job, WorkerJobState finalState) { WorkerLock lock(mutex); if (!lock || job == nullptr) { return; } - job->stackHighWaterMarkBytes = worker_task_support::currentStackHighWaterMarkBytes(); + job->stackHighWaterMarkBytes = job->task.stackHighWaterMarkBytes(); job->finalState = finalState; job->state = WorkerJobState::CallbackComplete; job->finishedAtMs = nowMs(); - job->taskHandle = taskHandle; } - void queueCleanup(WorkerJobRecord *job, TaskHandle_t taskHandle) { - if (job == nullptr || cleanupQueue == nullptr) { + void queueCleanup(WorkerJobRecord *job) { + if (job == nullptr || !cleanupQueue) { return; } { @@ -414,18 +469,13 @@ struct WorkerImpl { } } - const WorkerCleanupRequest request{ - job->id, - taskHandle, - job, - job->allocation == WorkerTaskAllocation::WithCaps, - false, - }; - while (xQueueSend(cleanupQueue, &request, portMAX_DELAY) != pdPASS) { + const WorkerCleanupRequest request{job->id, job, false}; + while (!cleanupQueue.send(request, portMAX_DELAY)) { vTaskDelay(1); } - const uint32_t queueDepth = static_cast(uxQueueMessagesWaiting(cleanupQueue)); + const uint32_t queueDepth = + static_cast(uxQueueMessagesWaiting(cleanupQueue.handle())); WorkerLock lock(mutex); if (lock) { cleanupQueueHighWaterMark = std::max(cleanupQueueHighWaterMark, queueDepth); @@ -484,33 +534,24 @@ struct WorkerImpl { "interval must be greater than zero" )); } + if (incomingConfig.stackPlacement && + !Strata::validPlacement(*incomingConfig.stackPlacement)) { + return emitJobResult(WorkerJobResult::failure( + WorkerStatus::InvalidArgument, + "invalid stack placement" + )); + } const WorkerJobConfig jobConfig = resolveJobConfig(incomingConfig); - if (!worker_task_support::isValidStackSize(jobConfig.stackSize)) { + if (!isValidStackSize(jobConfig.stackSize)) { return emitJobResult(WorkerJobResult::failure( WorkerStatus::InvalidArgument, "stack size must be at least 1024 bytes and aligned" )); } + const Strata::Placement stackPlacement = resolveJobStackPlacement(jobConfig); - bool usePsramStack = false; - WorkerStackType actualStackType = WorkerStackType::Internal; - if (jobConfig.stackType == WorkerStackType::Psram) { - if (!worker_task_support::hasExternalStackSupport()) { - return emitJobResult(WorkerJobResult::failure( - WorkerStatus::TaskCreateFailed, - "PSRAM task stacks are not available" - )); - } - usePsramStack = true; - actualStackType = WorkerStackType::Psram; - } else if (jobConfig.stackType == WorkerStackType::Auto && - worker_task_support::hasExternalStackSupport()) { - usePsramStack = true; - actualStackType = WorkerStackType::Psram; - } - - std::unique_ptr job(new (std::nothrow) WorkerJobRecord()); + auto job = Strata::makeUnique(config.memory.allocation); if (!job) { return emitJobResult(WorkerJobResult::failure( WorkerStatus::OutOfMemory, @@ -525,11 +566,7 @@ struct WorkerImpl { job->stackSize = jobConfig.stackSize; job->priority = jobConfig.priority; job->coreId = jobConfig.coreId; - job->requestedStackType = jobConfig.stackType; - job->actualStackType = actualStackType; - job->allocation = usePsramStack - ? WorkerTaskAllocation::WithCaps - : WorkerTaskAllocation::Internal; + job->requestedStackPlacement = stackPlacement; if (jobConfig.name != nullptr && *jobConfig.name != '\0') { copyTaskName(job->name, sizeof(job->name), jobConfig.name); } @@ -543,7 +580,7 @@ struct WorkerImpl { "failed to lock worker registry" )); } - if (!initialized || cleanupQueue == nullptr || cleanupTaskHandle == nullptr) { + if (!initialized || !cleanupQueue || !cleanupTask) { return emitJobResult(WorkerJobResult::failure( WorkerStatus::NotInitialized, "worker is not initialized" @@ -555,7 +592,7 @@ struct WorkerImpl { "worker is ending" )); } - if (jobs.size() >= config.maxConcurrentJobs) { + if (!jobs || jobs->size() >= config.maxConcurrentJobs) { return emitJobResult(WorkerJobResult::failure( WorkerStatus::Busy, "maximum concurrent jobs reached" @@ -565,20 +602,20 @@ struct WorkerImpl { job->id = allocateJobId(); WorkerJobRecord *jobRecord = job.get(); const WorkerJobId jobId = job->id; - jobs.push_back(std::move(job)); + jobs->push_back(std::move(job)); - TaskHandle_t handle = nullptr; - const BaseType_t created = worker_task_support::createTask( + auto task = Strata::FreeRTOS::Task::create( &WorkerImpl::taskEntry, - jobRecord->name, - jobRecord->stackSize, jobRecord, - jobRecord->priority, - &handle, - jobRecord->coreId, - usePsramStack + Strata::FreeRTOS::TaskConfig{ + .name = jobRecord->name, + .stackBytes = jobRecord->stackSize, + .stackPlacement = stackPlacement, + .priority = jobRecord->priority, + .affinity = jobRecord->coreId, + } ); - if (created != pdPASS || handle == nullptr) { + if (!task) { eraseJob(jobId); result = WorkerJobResult::failure( WorkerStatus::TaskCreateFailed, @@ -586,7 +623,8 @@ struct WorkerImpl { jobId ); } else { - jobRecord->taskHandle = handle; + jobRecord->task = std::move(task); + xTaskNotifyGive(jobRecord->task.handle()); result = WorkerJobResult::success(jobId, "job started"); } } @@ -594,48 +632,48 @@ struct WorkerImpl { } bool initializeCleanupInfrastructure(const WorkerConfig &incomingConfig) { - cleanupQueue = xQueueCreate( - static_cast(incomingConfig.maxConcurrentJobs), - sizeof(WorkerCleanupRequest) - ); - if (cleanupQueue == nullptr) { + cleanupQueue = WorkerCleanupQueue::create({ + .length = incomingConfig.maxConcurrentJobs, + .storagePlacement = incomingConfig.memory.allocation, + .usage = Strata::FreeRTOS::QueueUsage::TaskOnly, + }); + if (!cleanupQueue) { return false; } + cleanupTaskReadyForDelete.store(false); cleanupTaskStopRequested = false; cleanupTaskRunning = false; cleanupQueueHighWaterMark = 0; - cleanupTaskHandle = nullptr; - const BaseType_t created = worker_task_support::createInternalTask( + const Strata::Placement stackPlacement = resolveCleanupStackPlacement(incomingConfig); + auto task = Strata::FreeRTOS::Task::create( &WorkerImpl::cleanupTaskEntry, - kCleanupTaskName, - incomingConfig.cleanupTaskStackSize, this, - incomingConfig.cleanupTaskPriority, - &cleanupTaskHandle, - incomingConfig.cleanupTaskCoreId + Strata::FreeRTOS::TaskConfig{ + .name = kCleanupTaskName, + .stackBytes = incomingConfig.cleanupTaskStackSize, + .stackPlacement = stackPlacement, + .priority = incomingConfig.cleanupTaskPriority, + .affinity = incomingConfig.cleanupTaskCoreId, + } ); - if (created != pdPASS || cleanupTaskHandle == nullptr) { - vQueueDelete(cleanupQueue); - cleanupQueue = nullptr; - cleanupTaskHandle = nullptr; + if (!task) { + cleanupQueue.reset(); return false; } + cleanupTask = std::move(task); + xTaskNotifyGive(cleanupTask.handle()); return true; } WorkerResult stopCleanupInfrastructure(uint32_t startMs, uint32_t timeoutMs) { - QueueHandle_t queue = nullptr; - TaskHandle_t handle = nullptr; bool sendStop = false; { WorkerLock lock(mutex); if (!lock) { return WorkerResult::failure(WorkerStatus::InternalError, "failed to lock worker"); } - queue = cleanupQueue; - handle = cleanupTaskHandle; - if (queue == nullptr || handle == nullptr) { + if (!cleanupQueue || !cleanupTask) { return WorkerResult::success("cleanup task already stopped"); } if (!cleanupTaskStopRequested) { @@ -645,14 +683,8 @@ struct WorkerImpl { } if (sendStop) { - const WorkerCleanupRequest stopRequest{ - kInvalidJobId, - nullptr, - nullptr, - false, - true, - }; - if (xQueueSend(queue, &stopRequest, 0) != pdPASS) { + const WorkerCleanupRequest stopRequest{kInvalidJobId, nullptr, true}; + if (!cleanupQueue.send(stopRequest, 0)) { WorkerLock lock(mutex); if (lock) { cleanupTaskStopRequested = false; @@ -664,54 +696,50 @@ struct WorkerImpl { } } - while (!cleanupTaskReadyForDelete.load()) { + while (!cleanupTaskReadyForDelete.load(std::memory_order_acquire)) { if (elapsedSince(startMs, timeoutMs)) { return WorkerResult::failure(WorkerStatus::Timeout, "worker end timed out"); } vTaskDelay(pdMS_TO_TICKS(kWaitPollMs)); } - worker_task_support::deleteTask(handle, false); + vTaskSuspend(cleanupTask.handle()); + cleanupTask.reset(); + cleanupQueue.reset(); { WorkerLock lock(mutex); if (lock) { - cleanupTaskHandle = nullptr; cleanupTaskRunning = false; cleanupTaskStopRequested = false; cleanupTaskReadyForDelete.store(false); - cleanupQueue = nullptr; } } - vQueueDelete(queue); return WorkerResult::success("cleanup task stopped"); } static void taskEntry(void *arg) { auto *job = static_cast(arg); if (job == nullptr || job->owner == nullptr) { - vTaskDelete(nullptr); - return; + suspendForever(); } + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); WorkerImpl *owner = job->owner; - const TaskHandle_t currentTask = xTaskGetCurrentTaskHandle(); const WorkerJobState finalState = owner->executeJob(job); - owner->prepareCleanup(job, finalState, currentTask); - owner->queueCleanup(job, currentTask); + owner->prepareCleanup(job, finalState); + owner->queueCleanup(job); job->readyForDelete.store(true, std::memory_order_release); - vTaskSuspend(nullptr); - for (;;) { - vTaskDelay(portMAX_DELAY); - } + suspendForever(); } static void cleanupTaskEntry(void *arg) { auto *owner = static_cast(arg); if (owner == nullptr) { - vTaskDelete(nullptr); - return; + suspendForever(); } + + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); { WorkerLock lock(owner->mutex); if (lock) { @@ -721,7 +749,7 @@ struct WorkerImpl { for (;;) { WorkerCleanupRequest request; - if (xQueueReceive(owner->cleanupQueue, &request, portMAX_DELAY) != pdPASS) { + if (!owner->cleanupQueue.receive(request, portMAX_DELAY)) { continue; } if (request.stopCleanupTask) { @@ -732,19 +760,17 @@ struct WorkerImpl { } } owner->cleanupTaskReadyForDelete.store(true, std::memory_order_release); - vTaskSuspend(nullptr); - for (;;) { - vTaskDelay(portMAX_DELAY); - } + suspendForever(); } - if (request.record == nullptr || request.taskHandle == nullptr) { + if (request.record == nullptr || !request.record->task) { continue; } while (!request.record->readyForDelete.load(std::memory_order_acquire)) { taskYIELD(); } - worker_task_support::deleteTask(request.taskHandle, request.withCaps); + vTaskSuspend(request.record->task.handle()); + request.record->task.reset(); owner->completeCleanup(request); } } @@ -836,7 +862,7 @@ uint64_t WorkerJobContext::lastRunAtMs() const { return lock ? _record->lastRunAtMs : 0; } -Worker::Worker() : _impl(new (std::nothrow) WorkerImpl()) { +Worker::Worker() : _impl(Strata::makeUnique(Strata::Placement::Internal)) { } Worker::~Worker() { @@ -849,8 +875,19 @@ WorkerResult Worker::init(const WorkerConfig &config) { if (!_impl) { return WorkerResult::failure(WorkerStatus::OutOfMemory, "failed to allocate worker"); } - if (!worker_task_support::isValidStackSize(config.defaultStackSize) || - !worker_task_support::isValidStackSize(config.cleanupTaskStackSize)) { + if (!_impl->mutex) { + return WorkerResult::failure(WorkerStatus::OutOfMemory, "failed to allocate worker mutex"); + } + if (!Strata::validMemoryPolicy(config.memory) || + (config.cleanupTaskStackPlacement && + !Strata::validPlacement(*config.cleanupTaskStackPlacement))) { + return _impl->emitResult(WorkerResult::failure( + WorkerStatus::InvalidArgument, + "invalid memory placement" + )); + } + if (!isValidStackSize(config.defaultStackSize) || + !isValidStackSize(config.cleanupTaskStackSize)) { return _impl->emitResult(WorkerResult::failure( WorkerStatus::InvalidArgument, "task stack sizes must be at least 1024 bytes and aligned" @@ -880,8 +917,7 @@ WorkerResult Worker::init(const WorkerConfig &config) { _impl->config = config; _impl->ending = false; _impl->nextJobId = 1; - _impl->jobs.clear(); - _impl->completions.clear(); + _impl->initializeStorage(config.memory.allocation, config.maxConcurrentJobs); if (!_impl->initializeCleanupInfrastructure(config)) { failure = WorkerResult::failure( WorkerStatus::TaskCreateFailed, @@ -904,9 +940,16 @@ void Worker::onEvent(WorkerEventCallback callback) { if (!_impl) { return; } + std::shared_ptr holder; + if (callback) { + holder = Strata::makeShared( + Strata::Placement::Internal, + std::move(callback) + ); + } WorkerLock lock(_impl->mutex); if (lock) { - _impl->onEvent = std::move(callback); + _impl->onEvent = std::move(holder); } } @@ -974,7 +1017,7 @@ WorkerResult Worker::stop(WorkerJobId jobId) { } else { job->stopRequested.store(true); job->state = WorkerJobState::Stopping; - handle = job->taskHandle; + handle = job->task.handle(); } } } @@ -1012,7 +1055,7 @@ WorkerResult Worker::stopAndWait(WorkerJobId jobId, uint32_t timeoutMs) { } else if (!isExecutionCompleteState(job->state)) { job->stopRequested.store(true); job->state = WorkerJobState::Stopping; - handle = job->taskHandle; + handle = job->task.handle(); } } } @@ -1062,7 +1105,7 @@ WorkerResult Worker::sleep(WorkerJobId jobId, uint32_t durationMs) { job->sleepDurationMs = std::max(existingRemainingMs, durationMs); job->hasSleepDeadline = true; job->state = WorkerJobState::Sleeping; - handle = job->taskHandle; + handle = job->task.handle(); } } } @@ -1103,37 +1146,45 @@ WorkerDiag Worker::getDiagnostics() { return diag; } - diag.activeJobCount = static_cast(_impl->jobs.size()); + diag.activeJobCount = _impl->jobs ? static_cast(_impl->jobs->size()) : 0; diag.cleanupTaskRunning = _impl->cleanupTaskRunning; diag.cleanupQueueHighWaterMark = _impl->cleanupQueueHighWaterMark; - if (_impl->cleanupQueue != nullptr) { + if (_impl->cleanupQueue) { diag.cleanupQueueDepth = - static_cast(uxQueueMessagesWaiting(_impl->cleanupQueue)); + static_cast(uxQueueMessagesWaiting(_impl->cleanupQueue.handle())); + diag.cleanupQueueStoragePlacement = _impl->cleanupQueue.storagePlacement(); + diag.cleanupQueueStorageRegion = _impl->cleanupQueue.storageRegion(); } - for (const auto &job : _impl->jobs) { - if (!job) { - continue; - } - switch (job->state) { - case WorkerJobState::Running: - diag.runningJobCount++; - break; - case WorkerJobState::Sleeping: - diag.sleepingJobCount++; - break; - case WorkerJobState::Stopping: - diag.stoppingJobCount++; - break; - case WorkerJobState::CallbackComplete: - case WorkerJobState::CleanupQueued: - diag.cleanupQueuedCount++; - break; - case WorkerJobState::Created: - case WorkerJobState::CleanupComplete: - case WorkerJobState::Stopped: - case WorkerJobState::Finished: - case WorkerJobState::Failed: - break; + if (_impl->cleanupTask) { + diag.cleanupTaskStackPlacement = _impl->cleanupTask.stackPlacement(); + diag.cleanupTaskStackRegion = _impl->cleanupTask.stackRegion(); + } + if (_impl->jobs) { + for (const auto &job : *_impl->jobs) { + if (!job) { + continue; + } + switch (job->state) { + case WorkerJobState::Running: + diag.runningJobCount++; + break; + case WorkerJobState::Sleeping: + diag.sleepingJobCount++; + break; + case WorkerJobState::Stopping: + diag.stoppingJobCount++; + break; + case WorkerJobState::CallbackComplete: + case WorkerJobState::CleanupQueued: + diag.cleanupQueuedCount++; + break; + case WorkerJobState::Created: + case WorkerJobState::CleanupComplete: + case WorkerJobState::Stopped: + case WorkerJobState::Finished: + case WorkerJobState::Failed: + break; + } } } return diag; @@ -1162,13 +1213,15 @@ WorkerResult Worker::getJobDiagnostics(WorkerJobId jobId, WorkerJobDiag &out) { out.stackSize = job->stackSize; out.priority = job->priority; out.coreId = job->coreId; - out.requestedStackType = job->requestedStackType; - out.actualStackType = job->actualStackType; + out.requestedStackPlacement = job->requestedStackPlacement; + out.stackRegion = job->task ? job->task.stackRegion() : Strata::Region::Unknown; out.runCount = job->runCount; out.startedAtMs = job->startedAtMs; out.lastRunAtMs = job->lastRunAtMs; out.finishedAtMs = job->finishedAtMs; - out.stackHighWaterMarkBytes = job->stackHighWaterMarkBytes; + out.stackHighWaterMarkBytes = job->task + ? job->task.stackHighWaterMarkBytes() + : job->stackHighWaterMarkBytes; } } } @@ -1184,7 +1237,6 @@ WorkerResult Worker::end(uint32_t timeoutMs) { } const uint32_t startMs = nowMs(); - std::vector handles; { WorkerLock lock(_impl->mutex); if (!lock) { @@ -1194,20 +1246,19 @@ WorkerResult Worker::end(uint32_t timeoutMs) { return WorkerResult::success("worker not initialized"); } _impl->ending = true; - for (auto &job : _impl->jobs) { - if (!job || isExecutionCompleteState(job->state)) { - continue; - } - job->stopRequested.store(true); - job->state = WorkerJobState::Stopping; - if (job->taskHandle != nullptr) { - handles.push_back(job->taskHandle); + if (_impl->jobs) { + for (auto &job : *_impl->jobs) { + if (!job || isExecutionCompleteState(job->state)) { + continue; + } + job->stopRequested.store(true); + job->state = WorkerJobState::Stopping; + if (job->task) { + xTaskNotifyGive(job->task.handle()); + } } } } - for (TaskHandle_t handle : handles) { - xTaskNotifyGive(handle); - } while (true) { bool jobsEmpty = false; @@ -1216,7 +1267,7 @@ WorkerResult Worker::end(uint32_t timeoutMs) { if (!lock) { return WorkerResult::failure(WorkerStatus::InternalError, "failed to lock worker"); } - jobsEmpty = _impl->jobs.empty(); + jobsEmpty = !_impl->jobs || _impl->jobs->empty(); } if (jobsEmpty) { break; @@ -1237,8 +1288,12 @@ WorkerResult Worker::end(uint32_t timeoutMs) { { WorkerLock lock(_impl->mutex); if (lock) { - _impl->jobs.clear(); - _impl->completions.clear(); + if (_impl->jobs) { + _impl->jobs->clear(); + } + if (_impl->completions) { + _impl->completions->clear(); + } _impl->nextJobId = 1; _impl->initialized = false; _impl->ending = false; diff --git a/src/Worker.h b/src/Worker.h index 9ef39d3..fd61b0c 100644 --- a/src/Worker.h +++ b/src/Worker.h @@ -1,11 +1,12 @@ #pragma once #include +#include + #include #include #include -#include -#include +#include #include #include @@ -33,12 +34,6 @@ enum class WorkerStatus : uint8_t { InternalError, }; -enum class WorkerStackType : uint8_t { - Auto, - Internal, - Psram, -}; - enum class WorkerEventType : uint8_t { Info, Warning, @@ -70,29 +65,34 @@ struct WorkerEvent { }; struct WorkerConfig { + Strata::MemoryPolicy memory{ + .allocation = Strata::Placement::Default, + .taskStack = Strata::Placement::PreferExternal, + }; + uint32_t defaultStackSize = 4096; UBaseType_t defaultPriority = 1; BaseType_t defaultCoreId = tskNO_AFFINITY; - WorkerStackType defaultStackType = WorkerStackType::Auto; size_t maxConcurrentJobs = 8; uint32_t cleanupTaskStackSize = 3072; UBaseType_t cleanupTaskPriority = 1; BaseType_t cleanupTaskCoreId = tskNO_AFFINITY; + std::optional cleanupTaskStackPlacement{}; }; struct WorkerJobConfig { uint32_t stackSize = 0; UBaseType_t priority = 0; BaseType_t coreId = tskNO_AFFINITY; - WorkerStackType stackType = WorkerStackType::Auto; + std::optional stackPlacement{}; const char *name = nullptr; }; struct WorkerResult { bool result = false; WorkerStatus status = WorkerStatus::InternalError; - std::string message; + const char *message = "error"; explicit operator bool() const { return result; @@ -118,6 +118,10 @@ struct WorkerDiag { bool cleanupTaskRunning = false; uint32_t cleanupQueueDepth = 0; uint32_t cleanupQueueHighWaterMark = 0; + Strata::Placement cleanupTaskStackPlacement = Strata::Placement::Default; + Strata::Region cleanupTaskStackRegion = Strata::Region::Unknown; + Strata::Placement cleanupQueueStoragePlacement = Strata::Placement::Default; + Strata::Region cleanupQueueStorageRegion = Strata::Region::Unknown; }; struct WorkerJobDiag { @@ -127,8 +131,8 @@ struct WorkerJobDiag { uint32_t stackSize = 0; UBaseType_t priority = 0; BaseType_t coreId = tskNO_AFFINITY; - WorkerStackType requestedStackType = WorkerStackType::Auto; - WorkerStackType actualStackType = WorkerStackType::Internal; + Strata::Placement requestedStackPlacement = Strata::Placement::Default; + Strata::Region stackRegion = Strata::Region::Unknown; uint32_t runCount = 0; uint64_t startedAtMs = 0; uint64_t lastRunAtMs = 0; @@ -198,5 +202,5 @@ class Worker { const char *jobStateToString(WorkerJobState state) const; private: - std::unique_ptr _impl; + Strata::UniquePtr _impl; }; diff --git a/src/internal/WorkerMutex.h b/src/internal/WorkerMutex.h deleted file mode 100644 index bda0648..0000000 --- a/src/internal/WorkerMutex.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include -#include -#include - -class WorkerMutex { - public: - WorkerMutex() { - _handle = xSemaphoreCreateRecursiveMutex(); - } - - ~WorkerMutex() { - if (_handle != nullptr) { - vSemaphoreDelete(_handle); - } - } - - WorkerMutex(const WorkerMutex &) = delete; - WorkerMutex &operator=(const WorkerMutex &) = 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 WorkerLock { - public: - explicit WorkerLock(WorkerMutex &mutex) : _mutex(mutex), _locked(mutex.lock()) { - } - - ~WorkerLock() { - if (_locked) { - _mutex.unlock(); - } - } - - WorkerLock(const WorkerLock &) = delete; - WorkerLock &operator=(const WorkerLock &) = delete; - - explicit operator bool() const { - return _locked; - } - - private: - WorkerMutex &_mutex; - bool _locked = false; -}; diff --git a/src/internal/WorkerTaskSupport.h b/src/internal/WorkerTaskSupport.h deleted file mode 100644 index 862a665..0000000 --- a/src/internal/WorkerTaskSupport.h +++ /dev/null @@ -1,140 +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 WORKER_HAS_IDF_TASK_CAPS 1 -#else -#define WORKER_HAS_IDF_TASK_CAPS 0 -#endif - -#if WORKER_HAS_IDF_TASK_CAPS && defined(configSUPPORT_STATIC_ALLOCATION) && \ - (configSUPPORT_STATIC_ALLOCATION == 1) && defined(MALLOC_CAP_SPIRAM) -#define WORKER_CAN_USE_EXTERNAL_STACKS 1 -#else -#define WORKER_CAN_USE_EXTERNAL_STACKS 0 -#endif - -namespace worker_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 WORKER_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)) * sizeof(StackType_t); -#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 -) { - if (!isValidStackSize(stackBytes)) { - return pdFAIL; - } - if (usePsramStack) { -#if WORKER_CAN_USE_EXTERNAL_STACKS - if (!hasExternalStackSupport()) { - return pdFAIL; - } - return xTaskCreatePinnedToCoreWithCaps( - entry, - name, - static_cast(stackBytes), - arg, - priority, - handle, - coreId, - kExternalStackCaps - ); -#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 BaseType_t createInternalTask( - TaskFunction_t entry, - const char *name, - size_t stackBytes, - void *arg, - UBaseType_t priority, - TaskHandle_t *handle, - BaseType_t coreId -) { - return createTask(entry, name, stackBytes, arg, priority, handle, coreId, false); -} - -inline void deleteTask(TaskHandle_t handle, bool withCaps) { - if (handle == nullptr) { - return; - } -#if WORKER_CAN_USE_EXTERNAL_STACKS - if (withCaps) { - vTaskDeleteWithCaps(handle); - return; - } -#endif - vTaskSuspend(handle); -#if defined(INCLUDE_eTaskGetState) && (INCLUDE_eTaskGetState == 1) - while (eTaskGetState(handle) == eRunning) { - taskYIELD(); - } -#endif - vTaskDelete(handle); -} -} // namespace worker_task_support